摘要: 黑名单映射在随机算法中的应用
【对数据分析、人工智能、金融科技、风控服务感兴趣的同学,欢迎关注我哈,阅读更多原创文章】 我的网站:潮汐朝夕的生活实验室 我的公众号:潮汐朝夕 我的知乎:潮汐朝夕 我的github:FennelDumplings 我的leetcode:FennelDumplings
各位好,最近我们密集地学习了常见的随机算法,并且解决了不少力扣中的问题。本文我们看两个力扣上的综合性较强的随机算法的问题,710 和 519。
这两道题的解法类似,都用到了黑名单映射的技巧,同时随机性体现在随机队列的使用上。
题目1
给定一个包含 [0,n )
中独特的整数的黑名单 B,写一个函数从 [ 0,n )
中返回一个不在 B 中的随机整数。
对它进行优化使其尽量少调用系统方法 Math.random() 。
提示:
1 2 3 1 <= N <= 1000000000 0 <= B.length < min(100000, N) [0, N) 不包含 N,详细参见 interval notation 。
算法: 随机队列 + 黑名单映射 假想一个随机队列,key 为 [0, N)
,黑名单 B 长度为 nB,将这 nB 个数组映射到队尾的 nB 个元素 [N - nB, N - 1]
。
实现方式为将黑名单元素与队尾节点交换,交换后,原位置视为持有了被交换的队尾节点的元素。
随机数只在 [0, N - nB)
范围取。若取到了一个被交换过的节点,查哈希表将其映射的队尾元素返回。
黑名单映射细节:将黑名单分为两类,大于等于 N - nB
的和小于 N - nB
的。
对于大于等于 N - nB
的这部分不做映射,它们本来就不会访问到。小于 N - nB
的,将其映射到大于等于 N - nB
且不在黑名单中的元素。
代码 (C++) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution {public : Solution(int N, vector <int >& blacklist) { int nB = blacklist.size(); int n = N - nB; dre = std ::default_random_engine(); di = std ::uniform_int_distribution<int >(0 , n - 1 ); mapping = unordered_map <int , int >(); unordered_set <int > B (blacklist.begin(), blacklist.end()) ; for (int b: blacklist) { if (b >= n) continue ; --N; while (B.count(N) > 0 ) --N; mapping[b] = N; } } int pick () { int r = di(dre); if (mapping.count(r) > 0 ) return mapping[r]; else return r; } private : unordered_map <int , int > mapping; std ::default_random_engine dre; std ::uniform_int_distribution<int > di; };
题目2
题中给出一个 n_rows 行 n_cols 列的二维矩阵,且所有值被初始化为 0。要求编写一个 flip 函数,均匀随机的将矩阵中的 0 变为 1,并返回该值的位置下标 [row_id,col_id];同样编写一个 reset 函数,将所有的值都重新置为 0。尽量最少调用随机函数 Math.random(),并且优化时间和空间复杂度。
注意:
1 2 3 4 1 <= n_rows, n_cols <= 10000 0 <= row.id < n_rows 并且 0 <= col.id < n_cols 当矩阵中没有值为 0 时,不可以调用 flip 函数 调用 flip 和 reset 函数的次数加起来不会超过 1000 次
算法1: 哈希表维护黑名单+拒绝采样 如果每次选点都没有限制,则直接均匀采样横纵坐标即可。
由于在每次选点的时候,已经选过的点(已经置为1)不能被再次选中,因此相当于有一个黑名单,其中保存着不能被选中的点。
因此可以用哈希表维护一个黑名单,每次选点之后将选中的点加入黑名单,选点过程用拒绝采样即可。这是比较直接的做法,不过调用 Math.random() 会多一些。
代码 (C++) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Solution {public : Solution(int n_rows, int n_cols) { n = n_rows; m = n_cols; dre = std ::default_random_engine(); fliped = unordered_set <int >(); } vector <int > flip () { std ::uniform_int_distribution<int > di (0 , n * m - 1 ) ; int random_idx = di(dre); while (fliped.count(random_idx) > 0 ) random_idx = di(dre); fliped.insert(random_idx); int x, y; idx2xy(random_idx, x, y); return {x, y}; } void reset () { fliped.clear(); } private : int n, m; unordered_set <int > fliped; std ::default_random_engine dre; void idx2xy (int idx, int & x, int & y) { x = idx / m; y = idx % m; } };
算法2:随机队列 + 黑名单映射 用类似随机队列出队的处理方式:将选中的元素交换到末尾再弹出。
一开始队列中是 [0..m * n - 1]
的数据,数据量 len = m * n
。产生 [0..m * n - 1]
的随机数的方式取出一个随机元素。将它交换到末尾 m * n - 1
,数据量减一。
在第二轮队列中是 [0..len - 1]
的数据,产生 [0..len - 1]
的随机数的方式取出一个随机元素,交换到末尾 len - 1
,数据量减一。
不用将队列显式建出来然后真正的交换,弹出 ,而是就维护一个数据量 len 作为随机数范围的依据。
还需要一个记录之前进行过的交换的数据结构。因为随机出的位置可能之前交换过了,那么需要一个哈希表记录该位置此时此刻持有的数据,也就是这个位置上一次弹出时候,是跟哪个位置的数据交换了。
代码 (C++) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Solution {public : Solution(int n_rows, int n_cols) { n = n_rows; m = n_cols; len = n_rows * n_cols; dre = std ::default_random_engine(); mapping = unordered_map <int , int >(); } vector <int > flip () { std ::uniform_int_distribution<int > di (0 , len - 1 ) ; int random_idx = di(dre); int x, y; if (mapping.count(random_idx) == 0 ) idx2xy(random_idx, x, y); else idx2xy(mapping[random_idx], x, y); if (mapping.count(len - 1 ) == 0 ) mapping[random_idx] = len - 1 ; else mapping[random_idx] = mapping[len - 1 ]; --len; return {x, y}; } void reset () { len = n * m; mapping.clear(); } private : int n, m; int len; unordered_map <int , int > mapping; void idx2xy (int idx, int & x, int & y) { x = idx / m; y = idx % m; } std ::default_random_engine dre; };