一、简介
问题描述:程序的输入包含两个整数m和n,其中m<n。输出是0~n-1范围内m个随机整数的有序列表,不允许重复。从概率的角度说,我们希望得到没有重复的有序选择,其中每个选择出现的概率相等。
(1)一般情况下,如果要从r个剩余的整数中选出s个,我们以s/r的概率来选择下一个数。如下伪代码所示:
select = m
remaining = n
for i = [0, n)
if (bigrand() % remaining) < select
print i
select—
remaining--
下面给出一个C++的实现:
void getknuth(int m, int n)
{
for(int i = 0; i < n; i ++)
{
//select m of remaining n - i
if(bigrand() % (n - i) < m)
{
cout << i << " ";
m--;
}
}
cout << endl;
}
(3)另一个解决方法是在一个初始为空的集合里插入随机整数,直到个数足够。伪代码如下:
initialize set S to empty
size = 0
while size < m do
t = bigrand() % n
if t is not in S
insert t into S
size++
print the elements of S in sorted order
利用C++的标准模板库实现如下所示:
void gensets(int m, int n)
{
set<int> S;
set<int>::iterator i;
while (S.size() < m)
{
int t = bigrand() % n;
S.insert(t);
}
for (i = S.begin(); i != S.end(); ++i)
{
cout << *i << " ";
}
cout << endl;
}
(3)生成随机整数的有序子集的另一种方法时把包含整数0~n-1的数组顺序打乱,然后把前m个元素排序输出即可。
for i = [0, n)
swap(i, randint(i, n-1) ) // randint(i, j)从i...j范围内均匀选择的随机整数的函数
其实在这个问题中,我们只需要打乱数组的前m个元素,对应的C++代码如下所示:
void genshuf(int m, int n)
{
int i, j;
int *x = new int[n];
for (i = 0; i < n; i++)
{
x[i] = i;
}
for (i = 0; i < m; i++)
{
j = randint(i, n-1);
int t = x[i];
x[i] = x[j];
x[j] = t;
}
//排序数组中的前m个元素
sort(x, x+m);
for (i = 0; i < m; i++)
{
cout << x[i] << " ";
}
cout << endl;
}
二、原理
(1)正确理解所遇到的问题,即将问题抽象成能用数学或逻辑表示的命题;
(2)提炼出抽象问题,有助于我们把解决方案应用到其他问题中;
(3)考虑尽可能多的解法,多思考,然后会发觉写代码的时间会很短;
(4)实现一种解决方案。
三、习题
1、C库函数rand()通常返回约15个随机位,使用该函数实现函数bigrand()和randint(l,u),前者返回至少30个随机位,后者返回[l,u]范围内的一个随机整数,解答如下:
int bigrand()
{
return RAND_MAX*rand() + rand();
}
int region(int l, int u) //[l, u]
{
return l + rand() % (u - l + 1);
}
2、当m接近于n时,就集合的算法生成的很多随机数都要丢弃,因为他们之前已经存在于集合中了。能否给出一个算法,使得即使在随坏情况下也能使用m个随机数?
#include <iostream>
#include <set>
using namespace std;
void getSet(int m,int n)//在0 -- n-1 中挑选m个 随机数
{
srand(time(NULL));//这个很关键
set<int> S;
for(int i=n-m;i<n;++i)
{
int t=rand()%(i+1);
if(S.find(t) == S.end())
S.insert(t);
else
S.insert(i);
}
set<int>::iterator j;
for(j=S.begin();j!=S.end();++j)
cout<<*j<<" ";
}
int main()
{
getSet(5,10);
return 0;
}
3、如何从n个对象中随机选择一个?具体说来,如何在实现不知道文本文件行数的情况下读取该文件,从中随机选择并输出一行?
我们总是选择第一行,并用二分之一的概率选择第二行,使用三分之一的概率选择第三行,以此类推。在该过程结束的时候,每一行具有相同的选中概率(1/n,其中n是文件的总行数):
i = 0
while more input lines
with probability 1.0/++i
choice = this input line //如果前面做了选择,并不会break,而是直到最后一个为止。
print choice
这里比较有些疑惑的是第一行:总是选第一行 为什么概率还是1/n?
概率=1*(1/2)*(2/3)*(3/4)……(n-1/n) =1/n。
证明:当做第i步选择(选择第i行)时,选择该行的概率为1/i,则不选择的概率为(i-1)/i,对于一篇有n行的文档,现需证明最终选定第i行的概率为1/n。
当最终选择第i行,前(i-1)步的选择对最终结果不会产生影响,第i步选择的概率为1/i,即选择第i行,第(i+1~n)步中均采取不选择的动作,即对于任意j(i+1<=j<=n),当前步的概率为(j-1)/j,那么最终的概率为:(1/i)*((i)/(i+1))*...*((n-1)/n) = 1/n。
以一篇只有6行的文档为例,最终选择第2行的概率为:1/2*(2/3)*(3/4)*(4/5)*(5/6) = 1/6。
扩展:原问题可简化为:如何从n个有序对象中等概率地任意抽取1个,简记为sample(n,1),其中n未知;
若将该问题改为:如何从n个有序对象中等概率地任意抽取m个,简记为sample(n,m),其中n未知;
分析:若n已知,sample(n,m)是普通的抽样问题;当n未知时,可否根据上述算法进行相应的转化求解?
解决方案:将sample(n,m)问题转化为m个sample(n,1)问题,更具体一点是,转化为sample(n,1);sample(n-1,1);sample(n-2,1)....;sample(n-m+1,1)问题。
仍然以一篇6行文档为例,任取其中2行,做法如下:
第一遍,以如下概率选中一行:1(1) 2(1/2) 3(1/3) 4(1/4) 5(1/5) 6(1/6),假设选中第2行,接着概率修改如下:3(1) 4(1/2) 5(1/3) 6(1/4) 1(1/5)。
说明:当选中第2行,从第3行开始修改概率,并将第2行排除在外,继续扫描,这样能保证在剩下的5个数中仍然以等概率抽取其中的一个。