概率随机问题【1】相关C语言知识:http://blog.csdn.net/hxz_qlh/article/details/12843131

概率随机问题【3】http://blog.csdn.net/hxz_qlh/article/details/12978771

本节主要受到《编程珠玑》第12章随机取样问题的启发,但不仅仅限于随机取样问题,进一步地,研究讨论了一些在笔试面试中常见的和随机函数以及概率相关的问题。

阅读本文所需的知识:

1.对C语言中或其他语言中等价的rand()、srand()有所了解。本文不讨论种子的设定和伪随机数的问题;

2.中学或以上水平的概率基本概念。


利用随机数函数生成随机数

问题1(《编程珠玑》习题12.1后半段):

给定一个rand(),可以产生从0到RAND_MAX的随机数,其中RAND_MAX很大(常见值:16位int能表示的最大整数32767),写出利用rand()生成[a,b]中任意整数的函数,其中a>=0, b<=RAND_MAX,且b-a<<RAND_MAX.

分析:

这是在编程工作最常见的随机函数的应用,在这里做一个起点再合适不过。把随机数区间的起点从0变为a,同时把一共RAND_MAX+1个数的区间缩小至只含有b-a+1个数的区间,写为 a + rand()%(b-a+1),此时显然最大值是a+(b-a)= b。

进一步地,这个b-a<<RAND_MAX的条件虽然看上去不起眼,其实很重要。

附加思考:

如果b-a和RAND_MAX很接近会发生什么情况?读者不妨先做思考,问题2的分析会做出解答。

这个rand(),其实相当于《编程珠玑》提到的bigrand()。

问题2(笔试题变形):

给定一个随机数函数rand7(),它能以等概率生成1~7这7个整数。请根据rand7()写出类似的rand5()。

分析:

如果直接像问题1中一样,把1+rand7()%5作为rand5()会有什么情况发生?这时确实能产生1~5的随机数没错,可是各个数的概率相等吗?

对于随机数2,既有可能来自于1+1%5,也有可能来自于1+6%5,显然其概率是2/7,而不是1/5,不满足rand5()等概率产生各随机数这一隐含要求。不同于问题1,问题1中一个很大的区间收缩成较小区间时,各个元素映射后的新元素概率虽然概率可能不完全一样,但却是近似相同的。为了满足等概率的要求,可以这么做:

int rand5() {int res;do {res = rand7();} while(t>=6);return res;
} 
虽然保证了1~5的概率都变成了1/5,但是有一个无法避免的缺点是,每当产生了6或者7都要抛弃,相当于这一次运行是“空转”,浪费了时间。
如果对1/5这个概率不明白,可以有两种理解:每次产生6或7就被抛弃,剩下数的概率相等,必然为1/5;或者用更严密的推理:产生1~5的随机数,最终得到某一个的概率为:1/7+(1/7)*(2/7)+(1/7)*(2/7)2+...,无限项求和,结果是1/5。

问题3(笔试题原题)

给定一个随机数函数rand7(),它能以等概率生成1~7这7个整数。请根据rand7()写出类似的rand10()。

分析:

有了问题2的概率基础,把rand7()变成rand10()仅仅需要一点点思考了。

int rand10() {int t1,t2,res;do {t1 = rand7();} while(t1>=6);//t1以等概率成为1/5do {t2 = rand7();} while(t2==7); //t2以1/2概率成为奇数或偶数(丢掉了7,余下6个数,刚好三个奇数三个偶数)res = t1+5*(t2%2);//res是1~10中的任意一个数的概率都是1/10//注意到%和*具有相同的优先级,这里去掉括号结果是错的    return res;
}

问题2和问题3是对《编程珠玑》上使用范围不大的randint(i,j)生成其他范围随机数方法的解答。在掌握了问题2和问题3的解法后,你已经学会随机数区间的收缩和扩张,类似的问题迎刃而解。

问题4(《编程珠玑》习题12.1前半段)

C库函数rand()常返回15个随机位,写出bigrand(),能够返回30个随机位。

分析:

其实问题4和问题3有点像,但是不同之处在于,这次我们的视角是从位出发的,把rand()看做了将15个位每一个位以1/2概率设为0或1,从而生成0~RAND_MAX。从某种意义上来说,按这种理解方式来解这个问题更轻松一些,但是仅局限于2的幂减1这样的数值的区间,比如从0到11...11。在此基础之上把两部分的位拼接起来即可。

//《编程珠玑》的答案
//int bigrand()
//{    return RAND_MAX *rand() +rand();}
//最大值不是30个1,怀疑有错
//我的答案,把先生成的部分左移15位作为高15位
long bigrand()
{    return ((long)RAND_MAX+1)*rand() +rand();}

扩展:拒绝采样

这是一个简单粗暴有效的方法,使用这个方法你可以不用考虑复杂的区间扩展和收缩的问题。

主要思想是只要产生一个目标范围内的随机数,则直接返回。如果产生的随机数不在目标范围内,则丢弃该值,重新取样。由于目标范围内的数字被选中的概率相等,这样一个均匀的分布生成了。

显然rand7至少需要执行2次,否则产生不了1-10的数字。通过运行rand7两次,可以生成1-49的整数,

   1  2  3  4  5  6  7
1  1  2  3  4  5  6  7
2  8  9 10  1  2  3  4
3  5  6  7  8  9 10  1
4  2  3  4  5  6  7  8
5  9 10  1  2  3  4  5
6  6  7  8  9 10  *  *
7  *  *  *  *  *  *  *

由于49不是10的倍数,所以我们需要丢弃一些值,我们想要的数字范围为1-40,不在此范围则丢弃并重新取样。

然后使用两次rand7()分别生成行号和列号,如果对应元素是*,抛弃重来;否则即是结果。

int rand10() {int row, col, idx;do {row = rand7();col = rand7();idx = col + (row-1)*7;} while (idx > 40);return 1 + (idx-1)%10;
}

其实这种方法依然用到了区间扩展:把7扩展成7*7,并把不符合的部分抛弃。从这里可以看出这种方法其实还是有缺陷的:如果用rand7()生成rand50(),那这50个数可是填满这个表格后还有填不下的。


利用随机数函数产生随机事件

用随机事件表示随机事件?经过上面的区间收缩的思考,看上去并不难。把这个问题具体化为:使用rand()表示以M/N的概率发生的随机事件,M<=N,并可用作:if(事件A发生)  ,其中P(A) = M/N,那么表示为:

if(rand()%N< M)...

随机事件

通过这种方式,我们就可以做出让程序“以M/N的概率执行某个命令”这样的设计了。


取样问题:从n个元素中随机选取m个

从概率角度出发

考虑整数0,1,2,...,n-1,可以用上节的方法以概率m/n选取0(推导方式略)。但是对于1,必须考虑之前0是否被选取而以(m-1)/(n-1)或m/(n-1)的概率选取1,后续就更加麻烦。好在迭代是计算机的长项,只需要把这个是否选择当前数的随机事件稍作修改即可,使之变成从r个其余的数中选择s个:

int gen(int m,int n)
{int i, select = m,remaining = n;for(i=0;i<n;i++) {if(rand() % remaining <select) {printf("%d\n",i);select--;}remaining--;}return 0;
}

其概率证明可见于Knuth的The Art of Computer Programming第2卷。进一步地可以优化为:

int genknuth(int m,int n)
{int i;for(i=0;i<n;i++)if(rand()%(n -i) < m) {printf("%d\n",i);m--;}return 0;
}

《编程珠玑》提示,这种算法是“所有n的所有m元子集被选中的概率相等”,这个条件强于“所有元素被选中的概率相同”。下面是习题12.2中提到的“以等概率选择搜有元素,但是有些m元子集被选中的概率比其他子集大”的算法:直接选择1个数,则这个m元集合为它本身即后续的一共m个数,可能包括回绕。

对于这种方法,总要产生n次随机数。进一步可以写为for(i=0;i<n&&m>0;i++),但程序的平均运行时间是否变得更快需要权衡。对应地,习题12.7提供了一种稍微减少随机数产生次数的递归函数:

int randselect(int m,int n) {int r;//assert(m<=n && m>=0);if(m>0) {r = rand()%n;if(r < m) {printf("%d\n",n-1);randselect(m-1,n-1);}elserandselect(m,n-1);}return 0;
}

从集合插入出发

由于集合元素不重复,如果按等概率选择一个随机数,不在集合中就把它插入,反之直接抛弃,直到集合元素个数达到m个,同样可以满足要求,并且用C++的STL很容易实现:

void gensets(int m,int n) {set<int> S;while(S.size() < m)S.insert(rand()%n);set<int>::iterator i;for(i = S.begin();i!=S.end();++i)cout<<*i<<"\n";
}

这个算法的主要问题是,如果抛弃已存在的元素的次数过多,相当于多次产生随机数并进行集合操作,性能将明显下降。比如当n=100而m=99,取第99个元素时,算法“闭着眼睛乱猜整数,直到偶然碰上正确的那个为止”(《编程珠玑(续)》,13.1节)。虽然这种情况会在“从一般到特殊”提供解决方案,但下面的Floyd算法明显规避了产生随机数超过m次的问题。

习题12.9提供了一种基于STL集合的随机数取样方法,可以在最坏情况下也只产生m个随机数:限定当前从中取值的区间的大小,每当产生重复的随机数,就把这一次迭代时不会产生的第一个随机数拿来替换。

int genfloyd(int m,int n){set<int> S;set<int>::iterator i;for(int j = n-m; j<n;j++) {int t = rand()%(j+1);if(S.find(t) == S.end())S.insert(t);elseS.insert(j);}for(i=S.begin();i!=S.end();++i)cout<<*i<<"\n";
}

如果不必基于集合而实现,我自己写了一个原理类似的算法,思想是把“用过”的元素“扔”到下一次迭代的随机数取样区间之外:

int gen(int m,int n) {int *array;int i,j;array = (int *)malloc(sizeof(int) * n);for(i=0;i<n;i++)array[i] = i;while(m>=1) {j = rand()%n;printf("%d\n",array[j]);if(j<n-1)array[j] = array[n-1];m--;n--;}return 0;
}

从“打乱顺序”出发

这是个来源于实际的想法:将所有n个元素打乱,取出前m个。更快的做法是,打乱前m个即可。对应的C++代码如下:

int 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);//randint产生i到n-1之间的随机数int t = x[i];x[i] = x[j];x[j] = t;}//sort(x,x+m);//sort是为了按序输出 for(i=0;i<m;i++)cout<<x[i]<<"\n";
}    

把sort注释掉的这段代码,可以作为随机不重复序列产生器。类似的还有Floyd的算法P。(《编程珠玑(续)》,13.3节)

从一般到特殊

以上讨论的几种方式都不限定m和n的取值,只需m<=n即可。对于特殊的取值,有特殊的解决方案,以下是编程珠玑上的两例:

1.n = 106而m=n-10,这时可以先生成10个元素,然后输出其余的元素。进行这种处理的额外代码可以提高算法的平均速度。

2.n=231而m = 107,m<<n,这时可以先生成1.1*107个元素,排序后去掉重复的(由于n很大,m中出现重复元素的概率很低),得到 107个元素的样本。

附:

(《编程珠玑(续)》习题13.5)Doug McIlory的求N个元素中取M种的第G种情况的组合的算法,原书这个算法我没有理解,也没有看到比较满意的解释,可能用到了某些我不熟悉的组合数性质。原书中没有对其进行进一步解释,并且似乎只是作者的题外话而已。如果看着有困难,这部分代码完全可以跳过。介绍这个算法的原因是用它能在获得随机数G后,直接获得第G种N个元素取M个的取法,相当于只产生了一次随机数。

int comb(int n,int m,int g,int* array){int d = 1,t;while(m>0) {t = combination(n-d,m-1);//combination(n,m)是计算n中取m个的组合数if(g-t<0) {m--;array[m] = d;printf("%d\n",d);}elseg-=t;d++;}return 0;
}

取样问题:从未知总数的元素中选择一个

从事先未知总数为n的对象中随机选择一个的方法。有两种常见的具体问题:

1.读取一个未知行数的文件,随机输出其中的的一行,同时最多只能缓冲一行的内容(《编程珠玑(续)》习题15.3利用了这种形式);

首先想到的是我们做过类似的题目吗?当然,在知道文件行数的情况下,我们可以很容易的用C运行库的rand函数随机的获得一个行数,从而随机的取出一行,但是,当前的情况是不知道行数,这样如何求呢?我们需要一个概念来帮助我们做出猜想,来使得对每一行取出的概率相等,也即随机。这个概念即蓄水池抽样(Reservoir Sampling)。

有了这个概念,我们便有了这样一个解决方案:定义取出的行号为choice,第一次直接以第一行作为取出行 choice ,而后第二次以二分之一概率决定是否用第二行替换 choice ,第三次以三分之一的概率决定是否以第三行替换 choice ……,以此类推,可用伪代码描述如下:

i = 0while more input lineswith probability 1.0/++ichoice = this input lineprint choice

  这种方法的巧妙之处在于成功的构造出了一种方式使得最后可以证明对每一行的取出概率都为1/n(其中n为当前扫描到的文件行数),换句话说对每一行取出的概率均相等,也即完成了随机的选取。

  证明如下:

  回顾这个问题,我们可以对其进行扩展,即:如何从未知或者很大样本空间随机地取k个数?

  类比下即可得到答案,即先把前k个数放入蓄水池,对第k+1,我们以k/(k+1)概率决定是否要把它换入蓄水池,换入时随机的选取一个作为替换项,这样一直做下去,对于任意的样本空间n,对每个数的选取概率都为k/n。也就是说对每个数选取概率相等:

Init : a reservoir with the size: kfor i= k+1 to NM=random(1, i);if( M < k)SWAP the Mth value and ith valueend for

    证明如下:

  蓄水池抽样问题是一类问题,在这里总结一下,并由衷的感叹这种方法之巧妙,不过对于这种思想产生的源头还是发觉不够,如果能够知道为什么以及怎么样想到这个解决方法的,定会更加有意义。

2.对链表进行一趟遍历,随机输出其中的一个结点的元素,只能使用一个临时指针。

解法是,以概率1选择第一个元素存入缓存,以概率1/2用第二个元素替换掉缓存,...,直至遍历完所有元素,最后输出缓存的内容。可以分析出此时所有元素留在缓存的概率均为1/n,比如1,是1*(1/2)*(2/3)*...*((n-1)/n)。

int random_select(void)
{int i,num=1;for(i=1;i<n;i++)//i<n代表某种终止条件,n未知    if(rand()%i ==0)num = i;return num;
}

对于这个问题的常见应用:马尔科夫文本生成器里选择哈希表中某项对应链表的任意一个结点。


概率问题选编

1.(习题12.4)对于集合插入问题,每次产生随机数后都需要在集合中检测该元素是否已经存在,这个测试次数和调用随机数函数的次数相同。对于从n个元素中选择m个元素的问题,平均需要测试多少次才能保证选择了m个元素?

这里先处理特殊的问题,即“赠券收集问题”:必须收集多少张棒球卡才能保证拥有所有的n种卡?

记Pi为从收集了i-1种到i种的概率,Pi=(n-i+1)/n

此时需要收集的卡片数目ni = 1* Pi + 2*(1-Pi)*Pi + 3*(1-Pi)2*Pi + ... + n*(1-Pi)n*Pi + ...,这个无穷级数可以求解为ni=1/Pi

那么所需要总的卡片数目sum(ni)  = n1+n2 +... +nn约为nlnn + γn + O(1)。相关维基百科

对于从n个元素中选择m个,相应地sum(ni)  = n1+n2 +... +nm,约为nlnm+γn。(根据调和数的推导)

2.(习题12.11)每个玩家有一张包含16个覆盖点的纸牌,覆盖点下面隐藏着1~16的随机排列。玩家每次刮开一个点,如果出现3,则判玩家负;如果出现1或2,则判玩家胜。那么,随机选择覆盖点刮开,获胜的概率是多少?

解答:

其实胜负只对应两种排列:1和2都出现在3前则胜,否则负。前者的即*1*2*3*或*2*1*3*,后者为其余四种排列。显然获胜的概率为2/3。

不要把问题复杂化,应该尽量简化。4~16的数字都是没有用的。

参考

1、http://www.cnblogs.com/wuyuegb2312/archive/2013/06/29/3141292.html

概率随机问题【2】 取样与概率相关推荐

  1. c语言 等概率随机数,随机数函数取样与概率

    非常感谢原版作者! 转自:http://www.cnblogs.com/wuyuegb2312/p/3141292.html#title4 本节主要受到<编程珠玑>第12章随机取样问题的启 ...

  2. php 随机几率,php实现根据概率配置随机抽奖

    这是我写的一个比较简单的抽奖算法,并没有很严谨,用于我自己写的wap文字游戏(美味小镇)上的随机食材,可以设定概率值 /** * Created by PhpStorm. * User: tionci ...

  3. python基于随机森林模型的预测概率和标签信息可视化ROC曲线

    python基于随机森林模型的预测概率和标签信息可视化ROC曲线 目录 python基于随机森林模型的预测概率和标签信息可视化ROC曲线

  4. 【erlang】根据奇数位的概率随机求偶数位 概率列表求随机值

    记录一个工作中遇到的小问题,奇数位是概率,偶数位是数值,给一个列表根据概率求数值.列表如:[50,3,30,5,20,10] 比如50%的概率返回3,30%的概率返回5,20%的概率返回10 -mod ...

  5. 等概率随机取数算法的几种实现(洗牌算法)

    等概率随机取数算法的几种实现 最近读了项目中的工具脚本,发现一个随机取数的函数,功能大概是从M个数中不重复的随机取出N个数,算是数组随机排序然后取前N个值的变种. 脚本实现采取原始的方法,每随机取一个 ...

  6. matlab第八章概率计算ppt,Matlab来解决概率统计学ppt

    PPT内容 这是一个关于Matlab来解决概率统计学ppt,主要介绍将利用Matlab来解决概率统计学中的概率分布.数字特征.参数估计以及假设检验等问题.欢迎点击下载哦. 本章将利用Matlab来解决 ...

  7. 概率p输出1,概率1-p输出0,等概率输出0和1 【LeetCode】470. rand7()构造rand10() 系列变形(新浪、字节面试题)

    目录 1. 等概率输出0和1 1.1 题目描述 1.2 解题思路 & 代码 2. 以 1/N 的概率返回 1~N 之间的数 3. 给定函数rand5() 构造rand7() 或 rand7() ...

  8. python概率随机抽奖_Python利用带权重随机数解决抽奖和游戏爆装备

    关于带权随机数 为了帮助理解,先来看三类随机问题的对比: 1.已有n条记录,从中选取m条记录,选取出来的记录前后顺序不管. 实现思路:按行遍历所有记录,约隔n/m条取一个数据即可 2.在1类情况下,还 ...

  9. js抽奖概率随机取出数据(简单示例)

    在平常活动开发当中,经常会碰到抽奖等类似的js功能,那么下面我们随机取数组中的一条来展示出来. ( 一 ) 无概率问题 var gift_ = ['apple pro一台','iphoneX一台',' ...

最新文章

  1. storyboard搭建项目_Storyboard 快速搭建UICollectionView
  2. springboot oauth2登录成功处理器_Spring Boot Security 整合 OAuth2 设计安全API接口服务...
  3. java顺序结构类型,Java类的完整构造执行顺序
  4. Educational Codeforces Round 30 C
  5. 数据结构与算法之美-哈希算法
  6. FireBug实用指南
  7. 【Android】如何查看android cpu是32位还是64位
  8. AdaptiveMaxPool的作用
  9. 两个大整数相乘 C++ 版本 源码
  10. linux 做磁盘配额
  11. matlab2c使用c++实现matlab函数系列教程-ones函数
  12. UML各种图画法总结
  13. 卸载creative cloud
  14. 旧式电话机的高压振铃电路图
  15. steam服务器连接不稳定WIN10,小编操作win10系统steam连接不稳的解决步骤
  16. 抖音上热门运营技巧秘诀
  17. python输入两个整数求最大公约数和最小公倍数_题目:输入两个正整数m和n,求其最大公约数和最小公倍数 。...
  18. 数字逻辑课程设计--数字钟的设计(quartus ii)(内附源代码和实习报告以及6篇实习日志)
  19. 信息检索导论要点整理
  20. Python实现网络爬虫

热门文章

  1. iOS开发常用第三方库
  2. 王伟storm_用画笔征服世界! 暴雪“风暴之子”第八子王炜
  3. 盘点2013中国大陆十大IC设计公司
  4. “大一新生如何参加智能车比赛”帖子回复
  5. Java学习内容之需要辨识的英文单词(持续更新)
  6. 王者荣耀服务器不稳定总是跳频,王者荣耀:有460?来,把这些设置调一下试试!...
  7. JavaScript网页特效-浮现社会主义核心价值观文字动画效果
  8. Python题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月 ,后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?
  9. 【Vue学习】基础语法(六)
  10. npskins CSGO开箱 | CSGO皮肤直接取回网页开箱子网站