从 n 个数字中选出 m 个不同的数字,保证这 m 个数字是等概率的

问题如上。

这是我被面试的一个题目。

我的第一反应给出的解决的方法是。开启  n 个线程并标记序号,各个线程打印出它的序号。直到有 m 个线程被调度时,停止全部线程。 

打印出的序号即是 m 个等概率出现的数字。

面试官听到这个解决的方法,吸了一口凉气。预计心里在想,这小伙疯了!我当时自知这个解决方式不是面试官想要的。于是说了,假设这个 n 非常大,那么就要另

想办法了,由于不可能在一个进程里产生随意多个线程。

想啊想,过了两分钟。还是没有找到解决的方法。面试官非常 nice 让我回去后再想一想。

事实上这个题细想。有些难度。你可能有一种思路:假设能一次性取出 m 个数字。再保证各数字是随机的。则能够满足等概率。但。

。。

怎样一次性取出 m 个数字呢?

一次性生成多个随机数?假设生成的数字有同样则不是等概率的。由于这个数字比其他的数字出现的次数多,则概率大些(虽然你忽略了它出现多次)。

或者还有思路:

每次出一个,保证取 m 次得到的各数字概率相等。听起来。似乎这样的思路要难些。


[解法一]

我们来模拟这个过程。设有一个长度为 m 的辅助数组 B,用来装选中的数字。数组末满时,依次从长度为 n 的数组 A 中每次取一个数字顺序放入 B 中。直到 B 满了。

设对于兴许的每个元素,其装入 B 中的概率为 x。此元素装入 B 中的操作是将它与 B 中的某个元素置换。

则 B 中已经存在的某个元素,继续在 B 中存在的概率为:(1-x) + x*(m-1)/m。即当前取出的元素不进入 B。被直接舍弃,或者当前取出的元素进入 B ,但置换不发生在这个元素身上。

当前 A 中取出的元素。在 B 中存在的概率为 x。即,仅仅要这个元素被选中,就一定会进入 B。

因为要使用每一个元素的概率相等。则有:

x = (1-x) + x*(m-1)/m。故 x = m/(m+1)。

也即。按上面的操作便能保证每个被留下来的元素的概率相等且为 x ,即 m/(m+1)。


[解法二]

事实上。我们考虑一下,这个模型不就是抽奖的模型吗,有 n 张彩票,n 个人每人一张,怎样选出 m 个人出来中奖。即。我们仅仅须要模拟一个公正的抽奖过程便能得到等概率的 m 个人。

我们都知道,抽奖不分先后。每一个人中奖的机率都一样。因此。最简单的做法是将 n 个人随机化排成一列,再取前 m 个人中奖就可以。

那么,我们借助洗牌算法便能做到。那么,怎样得到一个好的洗牌算法呢?一个能够证明是均匀的方法例如以下:

对于第 i 张牌,它以 i/(i+1) 的概率与前面 i 张牌交换,实际操作时,能够生成一个 0 ~ i 之间的随机数。当其不为 i 时运行交换。交换的操作是:将此牌与前面 i 张牌随机交换。

于是,能够证明。第 i 张牌在位置 i ,也即。它没有发生交换的概率为 1 - i/(i + 1)= 1/(i+1)。

第 i 张牌在前面不论什么一个位置的概率为 i/(i+1)*1/i = 1/(i+1) 。可是,我们还须要证明,前 i 张牌中的随意一张在前面 i 个位置中的随意一个位置的概率为 1/(i+1) 才算是证明全然。假设直接入手。这个证明能够想象是相当复杂的。我们使用数学归纳法证明,按上面的操作,随意第 i 张牌被操作完毕后,总共的 i + 1 张牌中的随意一张在0 ~ i 的随意一个位置上的概率为 1/(i + 1)。

证明:

当仅仅有一张牌(第 0 张牌)时,在位置 0 上的概率为 1。

如果第 i 张牌被操作完毕后。总共的 i + 1 张牌中的随意一张在0 ~ i 的随意一个位置上的概率为 1/(i + 1)。

则对于第 i + 1 张牌被操作后:

前面已经证明过。第 i+1 张牌放置在 0 ~ i+1 中的任何位置的概率为 1/(i+2)。

对于 0 ~ i 中的随意一张牌 x,它原先在 0 ~ i 上任何位置的概率为 1/(i + 1)。

x 被换到第 i+1 位置的概率为 (i+1)/(i+2) * 1/(i+1) = 1/(i+2)。x 如今还在 0 ~ i 位置的概率即 1 减去前者,为 : 1 - 1/(i+2)。

而 0 ~ i 共同拥有 i + 1 个位置,故 x 在随意一个位置的概率为:

(1 - 1/(i+2))*1/(i+1) 结果为 1/(i+2)。

于是就证明原结论。因此。这是一个平衡的洗牌算法。


[解法三]

假设我们每次在面临第 i 个元素,不是像解法一中的,维持一个固定的概率去决定该元素是否留下,而是与当前已处理过的元素个数相关,是否能得到一个解法呢?

设总共已处理的个数为 N。当前正要处理的是第 i 个元素。辅助数组为 B。源数组为 A。操作例如以下:

1.当 i <= m 时。元素留下。

2.当 i > m 时,使用概率 m/N 决定元素的去留。假设元素留下。则它随机与 B 中某个元素 x 置换(丢弃x,保留该元素)

证明等概率:

1.当 i = m + 1 时,此元素留下的概率为 m/(m+1)。B 中随意一个元素留下的概率为:1-m/(m+1) + m/(m+1)*(m-1)/m = m/(m+1)。故此时,B 中全部元素的概率为 m/(m+1)。

2.设当已处理 N 个元素时,B 中的元素的概率为 m/N。

则当已处理 N+1 个元素时,当前元素留下的概率为 m/(N+1)。B 中随意一个元素留下的概率为:m/N*(1-m/(N+1) + m/(N+1)*(m-1)/m) = m/(N+1)。最前面的 m/N 表示此元素在 B 中,否则不在 B 中。

故使用上面的操作方法。留下的元素的概率是相等的。且与总共处理的元素的个数是相关的。

这一模型和解法一中的模型是不同样的。注意差别:

解法一中的模型适用于保留下来的元素概率相等。且永远不变。

解法二中的模型适用于保留下来的元素概率相等。但随着处理的元素的个数添加而改变,这也意味着,当须要从未知数目的数据源中取 m 个数字,使其等概率,这样的方法是很适用的。


[解法四]

我们已经有一个心得了,解法方案好像类似于:面临当前元素时。使用一个概率(这个概率可能是动态变化的。或者不变的)决定去留,若留,则与某个已选择的元素置换。以下再给出一种方法。

设 A 为源数组。B 为辅助数组(装入已选择的元素)。A 长度为 n。B 长度为 m。须要从 A 中取 m 个数字放入 B。使它们等概率。

遍历 A,在面临第 i 个元素 x 时,记 p 为还须要从 A 中选出的元素个数。q 为从 x 向后数,将 A 数完的个数。包含 x。决定 x 被选中的概率设置为 p/q。这也能够达到等概率。

1:第 0 个元素被选中的概率为 :m/n

2:第 1 个元素被选中的概率为 :m/n*(m-1)/(n-1) + (1-m/n)*m/(n-1) = m/n

3:第 2 个元素被选中的概率为:... = m/n

....

依此类推,不管哪个元素被选中的概率都为 m/n。以下,我们证明随意一个元素被选中的概率都为 m/n。

假设按上面的思路去证明将非常复杂。可是有一个非常巧妙的证明方法。

我们看这个问题的模型,实际上,它就是一个抽奖模型,如今有一个箱子里面装着 n 张奖券,写着“中”。或“不中”,当中。写着“中”的有 m 张,如今问。第 k 次抽奖,中奖的概率为

多少?这显然为 m/n!

还记得 "抽奖与顺序无关” 吗?于是。我们独立写出第 k 次中奖的概率的表达式:

C(m,1)*A(n-1,m-1) / A(n,m) = m/n。

故,上面的操作方法,随意一个元素被选中的概率都为 m/n。

解法四是对抽奖的全过程进行概率模拟,而解法二是对抽奖的前置处理进行模拟。

此解法的模型适用于保留下来的元素概率固定且相等。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 这是一个组合问题,可以使用递归来解决。 假设我们已经选出了前i-1个数字,现在需要在剩下的n-i+1个数选出m-i+1个数字。那么我们可以分两种情况考虑: 1. 第i个数字不,那么问题就转化为在剩下的n-i个数选出m-i个数字,递归求解即可。 2. 第i个数,那么问题就转化为在剩下的n-i个数选出m-i个数字,递归求解即可。 最终的结果就是这两种情况的并集。具体实现可以参考下面的代码: ``` def combine(n, m): res = [] def dfs(start, path): if len(path) == m: res.append(path[:]) return for i in range(start, n+1): path.append(i) dfs(i+1, path) path.pop() dfs(1, []) return res ``` 其,dfs函数表示从start开始数字,当前已数字为path,最终需要选出m个数字。如果path已经有m个数字了,就把它加入到结果集。否则,就从start到n个数字i,把它加入到path,然后递归求解剩下的数字,最后把i从path弹出,继续循环。 ### 回答2: 要求从1-n个数选出m个数字进行组合,可以使用回溯算法进行求解。回溯算法是一种经典的深度优先搜索算法,它通过穷举所有可能的情况来求解问题。 具体步骤如下: 1. 定义两个变量,一个用来记录当前组合的数字个数,另一个用来记录当前组合了哪些数字。 2. 从第一个数字开始依次数字,将取的数字加入组合,并将数字个数加一。 3. 递归进入下一层,从下一个数字开始数字,并将取的数字加入组合。 4. 如果数字个数达到了m个,将当前组合输出。 5. 如果当前数字已经完,或者数字个数已经达到了m个,则回溯到上一层。 6. 重复步骤2到5,直至穷举所有组合方式。 下面是代码实现: ``` void combination(int n, int m, int pos, vector<int>& nums, vector<int>& chosen) { // 如果已经够了m个数字,输出组合 if (chosen.size() == m) { for (int num : chosen) { cout << num << " "; } cout << endl; return; } // 如果已经遍历完了所有数字,回溯 if (pos > n) { return; } // 当前数字,并递归进入下一层 chosen.push_back(nums[pos]); combination(n, m, pos + 1, nums, chosen); // 不当前数字,并递归进入下一层 chosen.pop_back(); combination(n, m, pos + 1, nums, chosen); } void printCombinations(int n, int m) { vector<int> nums(n); for (int i = 0; i < n; i++) { nums[i] = i + 1; } vector<int> chosen; combination(n, m, 0, nums, chosen); } ``` 其,n表示所有数字个数,m表示数字个数。调用printCombinations(n, m)函数即可输出所有组合方式。 ### 回答3: 这是一个典型的组合问题,解决这个问题可以使用递归的方法。 首先我们需要定义一个函数来输出所有的组合方式。这个函数有几个参数:$n$表示数字的总个数,$m$表示选出数字个数,$selected$表示已经择的数字集合,$index$表示当前遍历到的数字。 接下来,我们需要在函数体内判断两种情况:当前选出数字个数等于$m$或者已经遍历到了所有的数字。如果已经选出了$m$个数字,我们就输出$selected$数字集合;如果已经遍历到了所有的数字,就返回。 如果以上两个情况都不满足,我们就需要考虑当前数字是否入集合。如果入,我们就在$selected$加入这个数字,并递归调用函数,此时选出数字个数为$m+1$,当前遍历到的数字为$index+1$。如果不入,我们直接递归调用函数,选出数字个数仍为$m$,当前遍历到的数字为$index+1$。 最后我们可以在主函数调用这个函数,传入$n$和$m$,来输出全部的组合方式。 下面是实现函数的Python代码: ```python def print_combination(n, m, selected, index): if len(selected) == m: print(selected) return if index == n: return # 数字 selected.append(index+1) print_combination(n, m+1, selected, index+1) selected.pop() # 不数字 print_combination(n, m, selected, index+1) n = 5 # 数字个数 m = 3 # 选出数字个数 selected = [] # 已数字的集合 index = 0 # 当前遍历到的数字 print_combination(n, m, selected, index) ``` 执行以上代码后,输出的结果如下: ``` [1, 2, 3] [1, 2, 4] [1, 2, 5] [1, 3, 4] [1, 3, 5] [1, 4, 5] [2, 3, 4] [2, 3, 5] [2, 4, 5] [3, 4, 5] ``` 可以看到,程序成功输出了所有的组合方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值