随机数发生器和随机数表
其实上文中已经多次提到了
随机数发生器(RNG,Random Number Generator),硬币、骰子等等包括后来提到的LCG等随机算法都是随机数发生器,显然他们可以分成两类:
真随机数发生器(True Random Number Generator)和
伪随机数发生器(Pseudo Random Number Generator),而我们使用RNG的根本目的是为了获取
随机数表(RNT,Random Number Table),也就是上文中所说的随机数列,在游戏领域的话我们通常称为
乱数表,通常我们用这样的表格来呈现:
71 | 81 | 20 | 65 | 14 | 30 | 10 | 17 | 82 | 95 |
20 | 28 | 75 | 68 | 62 | 69 | 72 | 79 | 44 | 43 |
5 | 61 | 77 | 58 | 18 | 47 | 75 | 6 | 78 | 14 |
28 | 44 | 0 | 78 | 19 | 21 | 58 | 5 | 14 | 59 |
98 | 52 | 01 | 77 | 67 | 14 | 90 | 56 | 86 | 7 |
22 | 10 | 94 | 5 | 58 | 11 | 80 | 50 | 54 | 31 |
39 | 80 | 82 | 77 | 32 | 50 | 72 | 56 | 32 | 48 |
91 | 49 | 91 | 45 | 23 | 68 | 47 | 92 | 76 | 86 |
3 | 6 | 11 | 80 | 72 | 75 | 56 | 97 | 88 | 0 |
75 | 56 | 34 | 87 | 63 | 2 | 76 | 11 | 84 | 20 |
这个表格是我在手头几本概率教材里面附的随机数表里面各扒一块拼成的,如果你们在其他人文章里看到完全相同的表格,几乎肯定是抄袭,请通知我。经过上文的介绍,大家应该明白用
伪随机数发生器生成这样的表格是很容易的,而
真随机数发生器又是怎么生成这样的表格的呢?
1.第一种最简单的方法就是直接生成,比如掷骰子生成的正面反面用1和0来记录的话就可以视为二进制数,选择适当的数字串长度划分就可以生成任意范围的随机数。在科研领域我们一般使用基于“量子骰子”的量子随机数发生器(QRNG,Quantum Random Number Generator),另外值得骄傲的是在这一领域,我国处于世界领先地位。
2.另一种是“观测”性质的生成,比方说观测某一限定空间内的微观运动或宏观混沌系统(如大气温度),然后将结果阈值化,举例来说就是这样:
[quote]把50只老鼠放进一个面积约能容纳100只老鼠的扁平且温暖的方盒中,然后不定期进行拍照,在照片上划分10*10的方格,每一格内超过1/2老鼠身体则涂黑,不足1/2则涂白,
那么这张照片就可以当成二维码来扫,这样就能生成随机数表了。[/quote]
随机数表的应用
随机数表在应用层面主要有两大方式:
[quote]提供判断:
最简单的模式就是比如做一个掷硬币的模拟游戏:
1.依次在随机数表中取数字。
2.当取值在1~50之间时输出“正面”。
3.当取值在51~100之间时输出“反面”。
于是根据随机数表中的“71、81、20、65、14……”该程序就生成了“反面、反面、正面、反面、正面……”这样的结果。
实际应用中的取值要更复杂一些,比如很多游戏中的“武器强化系统”:+1的成功率是70%、+2降低到60%、+3是50%这样很常见的设定是如何实现的呢?我们可以写这样的程序:
1.在随机数表中依次取值。
2.判断武器的状态。
3.当武器为+0的时候,取值在1~30之间判定为“失败”,在31~100之间判定为“成功”。
3.当武器为+1的时候,取值在1~40之间判定为“失败”,在41~100之间判定为“成功”。
3.当武器为+2的时候,取值在1~50之间判定为“失败”,在51~100之间判定为“成功”。
于是我们使用上面的随机数表强化一把武器的过程在程序层面就是这样的:第一次取值“71”>30,+1成功。第二次取值“81”>40,+2成功。第三次取值“20”<50,+3失败。第四次取值“65”>50,+3成功。[/quote][quote]随机排序:
我们在实际应用中经常会遇到把若干个元素随机排序的需求,如果使用数学方式(随机数表属于数学范畴)实现的话也被称为洗牌算法(shuffle algorithm)。
假设我们要将100个数的顺序打乱,显然直接用上面的随机数表当序号是不行的,因为有重复的数字,而在随机数发生器中加入剔除相同数值的机制的话又会严重影响效率(因为每生成一个数都要和之前所有数进行“逐一比对”,在数据量很大的时候运算量过高),我们可以用这样的程序去实现“洗牌”:
1.将需要排序的N个数按输入的顺序排列好。
2.选择第N个数字,并在数值总量大于N的随机数表中取值,设所取值为M,则将第N个数字和第M个数字位置对调,若M大于N,则用M除以N并“取余数再加1”作为M再进行操作。
3.选择第N-1个数字,在随机数表中取下一个数作为M进行同“步骤2”的操作。
4.持续操作直到对第1个数字进行操作,排序完毕。
这个程序看起来复杂的多,我们还是用实例来跑一下看看就明白了:
比如我们对“10、20、30、40”四个数字进行随机排序:
首先操作第4个数字“40”,随机数表第一个值是“71”>数列长度4,我们用71除以4得到17余3,再+1=4,那么最终得到的M值就是“4”,因为我们选择的已经是第4个数字了,所以不用进行对调。此时数列仍然为“10、20、30、40”
接下来操作第3个数字“30”,随机数表中取值“81”,81除以4得到20余1,再+1=2,M=“2”,于是我们就把第3个数字“30”和第2个数字“20”进行对调。此时数列变成了“10、30、20、40”
接下来选择此时的第2个数字“30”(别忘了我们刚刚在上一步把2换过来了),在随机数表中取值“20”,20除以4得到5余0,再+1=1,M值=“1”,于是我们就把第2个数字“30”和第1个数字“10”进行对调。此时数列变成了“30、10、20、40”
最后选择第1个数字“30”,在随机数表中取值“65”>数列长度4,于是用65除以4得到16余1,再+1=2,M值=“2”,于是我们就把第1个数字“30”和第2个数字“10”进行对调。此时数列变成了“10、30、20、40”,这也就是我们最终的排序结果。[/quote]当我们利用计算机生成的随机数表来进行上面的操作的时候,常常会有人这样评价“因为
计算机只能生成伪随机”,所以你这个洗牌算法(或者别的机制)不够随机。”
这种评价的逻辑是对的么?
如何用计算机生成真随机
用计算机生成真随机?或许有人看到这里都会怀疑我是不是打错了,毕竟每次谈到随机数的话题,必然会提到的一句话就是:
[quote]
计算机只能生成伪随机。[/quote]然而,相当讽刺的是,如果你带着这句话回到1951年,也就是第一台商用电子计算机Ferranti Mark 1问世的那一年,几乎所有对计算机有所了解的人的反应都会是:
[quote]
什么?计算机还能生成伪随机的?你别骗人了,计算机只能生成真随机![/quote]在Ferranti Mark 1中,阿兰·图灵
(注十)设计了一个
真随机数发生器(TRNG),原理是采集系统内的电气噪声生成一个20比特的随机数字串,然而这一实打实的
真随机却让开发人员非常不满,因为其完全不可控,实际编程应用中对随机数的要求包括可重复性(用于编程调试)和可以在一定范围内进行调整(比如前文提到的LCG就可以通过更改参数来调整周期等)的能力。平方取中过于弱鸡,LCG因为同余运算需要进行大量除法运算,在当时计算机的能力下效率堪忧。于是在那个时代的科研与生产中,最常见的场面就是大家手捧一本巨大的《A Million Random Digits with 100,000 Normal Deviates(百万乱数表)》利用里面的随机数来进行工作,该书是兰德(RAND)公司在1955年利用电脉冲发生器产生的随机数编写的,当年技术人员几乎人手一本,可见对于高质量随机数的需求有多大。
一直到1960年第一个比较可靠的商用伪随机数发生器——IBM公司的RANDU子程序才诞生,但是其依然存在着很大的缺陷(见前文“注八”中的“奥妮酱,卒”,另外学计算机的应该都有印象,好多教材关于伪随机生成器的章节都要把RANDU拉出来当反面教材鞭尸。),并没有完全取代《百万乱数表》。一直到90年代计算机性能的提升和日渐普及的计算机网络对于安全性的极高需求,伪随机数发生器才真正进入了黄金时代。
然而一直到现代,我们的计算机包括智能手机里面,都还有真随机数发生器的存在,主要用于一些对生成速度要求不高但是对于安全性要求极高的场合,比如现代蓝牙芯片中基本都有TRNG,用于配对过程中的通信安全,这一类TRNG又被称为硬件随机数发生器(HRNG,Hardware Random Number Generator)。除了直接生成数据,也有基于“观测”的HRNG,比如1997年的LavaRand,使用摄像头对一个熔岩灯持续拍摄并将图像数字化生成随机数。
[quote]综上,这句话应该改成“
计算机程序只能生成伪随机”,而
广义的“计算机”概念生成真随机是非常自然的事情,且普遍应用于科研和生产的历史几乎和计算机本身的历史一样长。[/quote]并且,即使是“计算机程序生成的伪随机”,我们在实际应用中利用其获取真随机数也是很容易的,还是让Alice和她的朋友们给大家示范一下:
[quote]Alice写了一个小
程序:
1.在后台存储中生成一个“1”,0.1秒后切换成“0”,之后每0.1秒持续如此在“1”和“0”之间切换。
2.每当用鼠标点击屏幕的时候,显示此刻后台存储中的结果。
之后Alice随意的点击屏幕,生成了一串“1、1、0、1、0、1、0、0……”这样的数字。[/quote]这个
程序是
伪随机(pseudo random),然而Alice通过
真随机(stochastic)选择输出结果,得到的就是
真随机(random)的结果,
这一方式最常见的应用就是滚动抽奖,就是那种屏幕上持续滚动手机号之类的,然后嘉宾选择
何时停止,这些手机号的排序和滚动模式通常都是由
伪随机数控制,但是最终抽选则是基于嘉宾的
自由意志,当然这种抽奖模式相对于摇奖机来说想要作假还是很容易的,比如每次嘉宾按停止键时显示事先定好的数字(这种方式下通常要把滚动做的极快让人看不清),但这和随机机制本身无关。
[quote]Bob也写了一个小
程序:
1.当输入奇数的时候,输出“1”。
2.当输入偶数的时候,输出“0”。
然后Bob持续投掷一枚骰子,将生成的点数如“6、4、1、2、2、4、3、1……”输入程序生成了一串“0、0、1、0、0、0、1、1……”这样的数字。[/quote]这个
程序显然也是
伪随机(pseudo random),然而Bob直接把
真随机(stochastic)当作输入值(这一过程中伪随机程序实际上起的作用和“把硬币的正反记为1和0”性质相同的作用,只是一种“
转换模式”),得到了
真随机(random)的结果。
综上,计算机实现真随机有两种方式:
[quote]1.直接使用
硬件随机数发生器(HRNG,Hardware Random Number Generator)生成
真随机(random)。
2.通过在
伪随机数发生器(Pseudo Random Number Generator)中增加
真随机(stochastic)因素将其改造成
真随机数发生器(True Random Number Generator)。[/quote]这里需要补充说明的是第2点,它实际上就是一个
不迭代的伪随机数发生器,起的作用相当于把初始值转换成我们所要的格式(类似把“正面和反面”变成“1和0”),这牺牲了伪随机数发生器原本最大的优势——高效率,所以在实际应用中,
都是在“生成的数据更像真随机”和“保持伪随机高效优势”间力求取得平衡,一般是根据实际需要进行
有限次迭代,在接下来的这个大案例中我们就可以看到这一点。
如何设计一整套随机机制
相信看到这里(还没有右上角)的朋友们,应该已经对随机在应用层面的使用有了一个大体的认识,我们在此再进行一次总结:
[quote]1.设计或选择一个随机数发生器(RNG,Random Number Generator)——根据实际需要选择真随机数发生器或伪随机数发生器(以及发生器的具体形式)。
2.将
初始值(此处的的“初始值”在编程领域我们通常称之为种子(seed),之后我们要经常用到这个概念)输入随机数发生器,生成随机数表。——可以使用另外的机制(包括另一个随机数发生器)生成种子,也可以使用另外的机制(包括另一个随机数生成器)对生成的随机数表进行操作(如筛选或排序)。
3.设计随机数表实现应用要求的模式并应用这些模式进行操作得出结果。——如上文中提到的“提供判断”和“随机排序”[/quote]没有实际编程经验的朋友看到这里还是一头雾水也是很正常的(当然更大的原因是我写的太烂),接下来我们用一个
比较长的实例(什么上面那个排序还不够长吗?)进行讲解:
用户需求及需求分析:[quote]
用户需求:设计某网络游戏中的抽奖机制,奖品总价值比较低,要求程序层面尽量简单,但又不容易被找出规律,需要能支持较大量用户同时抽奖并获得有差异的结果,并且对于不同价值奖品可以提供方便调整的中奖概率设定,最后就是机制层面的用户体验越高越好。需求分析:奖品总价值比较低——
不需要太贵的商用随机数发生器了。程序层面简单——
不能用太复杂的算法。不容易被找出规律——
尽量接近真随机(random)。能支持较大量用户同时抽奖——
随机数生成要尽量高速。获得有差异的结果——
同时抽奖的用户结果要尽量不同。对于不同价值奖品可以提供方便调整的中奖概率设定——
在随机数表的应用方式上要提供调整的余地。机制层面的用户体验越高越好——
尽量让用户觉得抽奖结果更取决于自己。[/quote]
首先现实中的需求分析要这么简单就好了 ,以上是一个比较标准的需求分析流程,里面有几点对于非行业人士可能需要再展开一点:
“简单高效”是大部分随机数发生器在实际应用上的核心标准之一,这也是为什么伪随机数发生器越来越受欢迎,一个网游同时几十万人一起抽奖,靠熔岩灯之类的真随机数发生器肯定是来不及的。
“不容易被找出规律”则是一个相对概念,要根据抽奖的价值来权衡产生的随机数质量(有多接近random)和成本(越好的伪随机数发生器越贵,同时也需要服务器端更多的计算资源),比如抽个某网站会员1、2、3天的话直接排序:“3个人中1天、2个人中2天、1个人中3天、3个人中1天……”都无所谓。但是如果价值比较高的抽奖,比如有些氪金游戏动辄千百元级的抽奖则需要较高质量的随机数以防诸如竞争对手或者工作室之类的掌握一定运算资源的势力进行破解。
“机制层面的用户体验越高越好”这个需要注意是和“中奖率”没有关系的,中奖率是要看运营公司想怎么设定,所谓“机制层面的用户体验”,刚刚说的某网站会员抽奖就是个典型的反例,这个规律一旦被发现,有人抽中3天之后大家就都不想抽了,一定要让用户觉得抽奖结果主要取决于他们自己。
程序设计:[quote]随机发生器:
1.使用“平方取中”方式,每次输入一个四位数种子,进行平方运算,结果如不足八位数则在最前面补充0直到形成八位数,将最终生成的八位数的中间四位继续进行前述的“平方取中”方式,此为1次迭代。
2.共进行10次迭代,并输出这10次的结果作为一个随机数表。
3.等待下一次种子的输入,并重复以上步骤。
种子获取:
当用户点击抽奖时,抓取当时的服务器时间,取“AB分CD秒”的“ABCD”作为四位数种子输入随机发生器。
随机数表使用方式:
1.用户点击抽奖后,用户界面出现10个宝箱,将随机数表中的10次迭代结果依次填入10个宝箱之中。
2.让用户自己点击选择一个宝箱开启。
3.根据宝箱中的的四位随机数值判定中奖结果。
中奖率设定方式:
1.0000、0100、2100、4100、6100、8100这5个会形成短周期的迭代结果设置为价值最低的奖品或“谢谢惠顾”等。
2.确定最稀有奖品的编号,如只有10个的最高奖,可以设置成“X999”,如“999、3999、9999”才可中奖等。
3.其他奖品可以用区间或倍数的形式再剔除掉大奖编号的方式定义,如“0~5000之间除X999外为末等奖”,“每逢11的倍数为四等奖”等等。
[/quote]这里面需要再展开一下的大概只有“中奖率设定和调整方式”:如果想调整中奖率的话直接改数值或区间就好了,比如想让最高奖变成现有概率的两倍(如“每日12:00~13:00大奖双倍哦!”等设定),只要把“X499”如“1499、9499”也设定为最高奖编号即可,非常方便。
案例点评:[quote]
优点:1.用户最终自己选择宝箱的方式极大程度提高了随机性和用户体验。
2.因为选择宝箱的方式极大的提高了随机性,对随机数发生器的要求就大大降低了,此时即可选择“平方取中”这样的极简方式大大提升效率。
3.随机数表和中奖率的设置简单合理,调整非常容易。
缺点:1.以“XX分XX秒”作为种子在“平方取中”方式中有很大的弊端,因为有部分用户有在“00分00秒”进行抽奖的个人癖好,此时迭代结果必然全是0,影响用户体验。
2.取服务器时间为种子,大大增加了服务器同时接受的请求量,容易造成服务器压力过大导致宕机。
改进方案:1.在服务器端设置若干计数器程序,内部以0000~9999循环滚动数字,滚动速度可以对应任何时间间隔如CPU时钟周期等,不必和现实时间周期挂钩。
2.用户发出抽奖请求时,根据用户本地时间、ip地址或网卡mac地址等信息通过一个极简算法(如随便除以一个小质数取余数)分配某一个计数程序为其取当时的数值作为种子。
3.不定期分批重置这些计数程序。[/quote]以上就是一个
简化了无数倍的随机数发生器实际应用案例,如果能完整看完的话,大概对于这个行业也能有更深一步的了解了吧。
综述
这一部分写的真的很累,几乎已经有编写教材的感觉了,当然最头疼的还是“
计算机如何生成真随机”这一部分到底应该放在哪里,实际上这整篇文章最初的出发点就是我关于这个问题的回答,在对这回答打磨雕琢的过程中不断扩展最终形成了这篇文章,按照可读性的标准它本应单独成一部分方便大家简单的获得结论,但是作为一个写东西把严谨置于可读性之上的死脑筋作者,我还是选择把它放在了整篇文章的正中心,这也是我当初构思的逻辑顺序,在此为了我的任性向大家致歉。
哦对了,那张很难看的图在这一部分又得到了扩展,甚至一下变成了两张哦:
原理层面:[img]https://img.nga.178.com/attachments/mon_201802/26/-1vhdQ5-94nvZlT3cS12j-uq.jpg[/img]
应用层面:[img]https://img.nga.178.com/attachments/mon_201802/26/-1vhdQ5-3aw3K1hT3cSui-lb.png[/img]
我觉得对一直看到这里的朋友来说,已经不需要再对这两张图进行什么补充说明了,我们下一部分见!
注释
这一部分的注释极少,原因不是没有什么可注释的,恰恰相反,全篇大部分内容放在前几部分都属于应该放在注释里的东西,然而由于本人水平有限,没法在把问题讲透彻的前提下(如同一开始所说,这是本文的核心目的,我自己选择了为此牺牲可读性)缩短正文内容,再次向在这一部分感到阅读体验很差的读者致歉。
注十:阿兰·图灵相关(施工中) ...
注十:阿兰·图灵相关