1,如何生成概率
主要参考大牛的博客:http://www.cnblogs.com/miloyip/archive/2010/04/21/1717109.html
问题定义
游戏(和一些模拟程序)经常需要使用随机数,去应付不同的游戏(或商业)逻辑。本文分析一个常见问题:有N类物件,设第i类物件的出现概率为P(X=i),如何产生这样的随机变量X?
例如对概率的要求是
P(X=0)=0.12
P(X=1)=0.4
P(X=2)=0.4
P(X=3)=0.07
P(X=4)=0.01
输入数组<0.12, 0.4, 0.4, 0.07, 0.01> 输出符合以上概率的随机数序列,如<1, 4, 2, 1, 2, 2, 1, 0, ...> 。
以下先谈一些统计学背景知识,再给这问题的可行解法。
概率分布
这问题要产生一个随机变量,接近指定的概率分布(probability distribution)。大部份程序语言都提供接近均匀分布(uniformly distributed)的伪随机数产生器(pseudorandom number generator, PRNG),例如JavaScript提供的Math.random()函数,可传回 半开区间的均匀分布伪随机数。
密度分布函数
现在,不仿测试一下JavaScript的Math.random()函数,看看它是否均匀分布。一个变数的分布以密度分布函数(probability density function, PDF)定义,一般写作,随机变量
在区间
上的概率为定积分:
为了把PDF视觉化,可以把X分为若干区间,统计各区间X出现的频率,绘画其直方图(histogram)。笔者写了一个简单的JavaScript框架,用HTML5 Canvas绘画直方图。以下测试代码,可绘画Math.random()的PDF估值(estimate)。
function step() {
var x = Math.random();
var bin = Math.floor(x * frequency.length);//譬如,x=0.45,则bin为4,就是给第四个桶的计数加1
frequency[bin]++;
sampleCount++;
plotPdf(frequency, sampleCount, 1/frequency.length, "Estimated pdf of Math.random (n=" + sampleCount + ")");
}
var frequency = new Array(10);
var sampleCount = 0;
for (var i = 0; i < frequency.length; i++)
frequency[i] = 0;
start("canvas1", step);//不断的循环执行step函数
![](https://img-my.csdn.net/uploads/201205/06/1336295754_4138.png)
累积分布函数
密度分布函数,可以变换为累积分布函数(cumulative distribution function, CDF),代表随机变量X小于x的概率:
在X为连续(continuous)的情况下,CDF可用PDF定义:
在X为离散(discrete)的情况下,CDF可定义为:
以下的pdf2cdf()函数,能把离散的PDF数组,转换为CDF数组。由于浮点小数相加会有误差,最后的值可能少于1,有机会产生bug,函数里强制指定最后一个元素为1。
function pdf2cdf(pdf) {//积累函数的意义在于,0表示落在0号桶的计数,1表示落在0,1号桶的计数...所以落在9号桶的概率肯定为1var cdf = pdf.slice();
for (var i = 1; i < cdf.length - 1; i++)
cdf[i] += cdf[i - 1];
// Force set last cdf to 1, preventing floating-point summing error in the loop.
cdf[cdf.length - 1] = 1;
return cdf;
}
题解
这问题其实正式来说,可称为模拟离散取样(simulated discrete sampling),跟据有限类别的指定概率,来模拟取样。
要制造指定的概率分布随机变量,关键就是如何把均匀分布变换。
逆变换取样
在上节中,显示了CDF的一些特性,例如CDF的范围是,而且是一个单调递增(monotonic increasing)函数。逆变换取样(inverse transform sampling)利用了这些特性,去解决这个问题。逆变换取样方法其实很简单,给一个目标CDF,只要计算其逆函数(inverse function),就可以把均匀的随机变数转换为目标CDF:
![](https://i-blog.csdnimg.cn/blog_migrate/b48b6c1874a2ba1c372de31bad73f17f.png=F_X^%7b-1%7d%28Y%29)
这方法能用在所有CDF(包括连续及离散的)。其数学证明可参考维基百科。
下图显示这个方法的直观解读,在Y轴范围里均匀取样(
),之后向右和CDF取交点,求交点的X轴位置(
),X则是符合CDF的概率分布。
以上只是讲的pdf为连续函数的做法,因为y的取值范围是0-1,所以先通过[0,1]均匀分布,获得y的一个取样,然后代入Fx的反函数即可以求出x的取样
这个方法用在离散的情况就更简单,只需搜寻目标的CDF,找出超过均匀取样的元素即可。代码如下:
functiondiscreteSampling(cdf) {//一定要注意cdf是累计概率
vary = Math.random();
for(varx incdf)
if(y < cdf[x])//
returnx;
return-1; // should never runs here, assuming last element in cdf is 1
}
题目的测试:
var targetPdf = [0.12, 0.4, 0.4, 0.07, 0.01];
var targetCdf = pdf2cdf(targetPdf);
function step() {
var bin = discreteSampling(targetCdf);
frequency[bin]++;
sampleCount++;
plotPdf(frequency, sampleCount, 0.4, "Estimated cdf of discreteSampling (n=" + sampleCount + ")");
}
var frequency = new Array(targetCdf.length);
var sampleCount = 0;
for (var i = 0; i < frequency.length; i++)
frequency[i] = 0;
start("canvas3", step);
截图是:
分析
在离散的情况下(本文题目要求),其时间复杂度是O(N),其中N为类别数目。
读者可能会注意到,这里用了线性搜寻(linear search),如果targetPdf数组是由大至小排列,平均而言会更快找到结果。另外,也可以用二分搜寻(binary search),那么复杂度会降低为O(lg N),这留给读者作为练习。
事实上,这个问题用二分搜寻是标准的方法。那么,还有没有更快的方法呢?答案是肯定的,例如别名方法(alias method)、近似方法等,有兴趣的读者可参考[1]。当然,在N很小的情况下,线性搜寻和二分搜寻也足够。