随机数生成器

一个特例:已知一个随机数生成函数f3(),即以相同的概率随机返回0, 1, 2(返回每个数的概率均为1/3),现要求通过调用f3()实现一个函数f5()。

解答方法:
(1)最基本的思路:
计算机中所有的数都是由二进制表示,所以如果能得到一个函数f2(),以50%的概率得到0或者1,那就可以通过这个函数来生成任何一个随机数产生器了。
f2(){int a; while((a = f3()) > 1); return a;}
然后f5()就能通过f2()实现。

(2)一次简单扩展:
既然任何一个数能用二进制表示,那也能用三进制表示,f3()就是一个生产三进制各位数字的函数,所以可以通过f3()直接得到f5()。伪代码:

int f5() {
    int a;
    do {
        a = 3 * f3() + f3();
    }while(a > 4);
    return a;
}

因为a的范围是[0, 8],且概率相等,当a>4的时候,重新调用两次f3(),所以最后返回的是[0, 4],且概率相等。

(3)两种方法调用f3()函数次数的期望:
方法(2)中一次求得a处于[0, 4]的概率是5/9,每次调用两次f3(),所以期望是2/(5/9) = 18/5 = 3.6次;
方法(1)中a = 4 * f2() + 2 * f2() + f2(),a取值为[0, 7],所以生成一个随机数调用f2()的次数为3/(5/8) = 24/5,而f2()每次只需调用一次f3(),最终的期望也是24/5 = 4.8次,不如方法(2)好。

(4)方法(2)每次成功概率只有5/9,实在是太低。改进一下,每次调用三遍f3(),即a = 9 * f3() + 3 * f3() + f3(),这样,a的取值范围是[0, 26],去除25,26两个值之后对5取模,伪代码:

int f5() {
    int a;
    do {
        a = 9 * f3() + 3 * f3() + f3();
    }while(a > 24);
    return a % 5;
}

期望是多少呢?容易计算为:3/(25/27) = 81/25 = 3.24次,比方法(2)还要好。

(5)方法(4)是不是最好的呢?如果每次再增加调用f3()的次数能使得一次成功吗?答案是否定的,因为3^n = (5 – 2)^n,即3的幂次方一定不能被5整除,所以一定有浪费。但是可以换一种思路,把方法(1)与方法(2)结合,先列伪代码:

int f5() {
    int a;
    do {
        a = 3 * f3() + f3();
        if(a > 4)
            a += 9 * f3();
    }while(a > 24);
    return a % 5;
}

先证明结果正确性,即返回值为[0, 4],且等概率。由最后return语句知,返回值一定是在[0, 4];那概率是否相等呢?首先,do-while语句里a的值有三种情况,第一种是[0, 4],第二种是[5, 8]、[14, 17]、[23, 24],第三种是[25, 26],其中第三种是需要抛弃的。仔细分析,a落入第一种情况的概率是5/9,且每个取值等概率,落入第二种情况的概率是(4/9) * (10 / 12) = 10/27,得到每个数的概率都是1 / 27,而每个数模5的余数分别为:0,1,2,3,4,0,1,2,3,4,所以在第二种情况下,返回[0, 4]也是等概率的。

(6)方法(5)的期望呢?
首先,最外层do-while循环一次成功的概率是25/27;循环体内部if语句执行的概率是4/9,所以循环体内部的执行次数的期望是2+4/9 = 22/9,返回一次随机数f3()执行次数的期望是(22/9) / (25/27) = 66/25 = 2.64次。

但是是真的吗?上述分析没有讨论独立性问题,即如果do-while循环体内if语句没有执行,那while判断条件也一定不满足,直接跳出循环。所以应该这样分析:do-while循环体内if语句没有执行的概率为5/9,执行if语句且跳出循环的概率是10/27,执行if语句但未跳出循环的概率是2/27,所以采用求解期望的直接公式进行计算,f3()调用的次数为:T3 = 2 * 5/9 + 3 * 10/27 + (2/27) * (5 * 5/9 + 6 * 10/27) + … = 20/9 + 2/27 * (3 + 20/9) + … + (2/27)^i * (3 * i + 20/9) + … = 2.6592。

比斜体字中计算的多的原因是,一旦没有跳出while循环,次数应该加3而非22/9。

最后的分析如有不对之处,请不吝指出。

(7) 若已知fn()而求fm()呢?
分两种情况考虑:1)m<=n,即调用一次fn(),得到的范围超过了m,那一次成功的概率为m/n,期望为n/m;2)m>n,即调用一次fn(),得到的范围还不足够m,所以必须多次调用,假设调用k次,那么n^k >= m,这样一次循环成功的概率是m/n^k,期望是n^k / m,另外,k >= log(n)m,进行上取整,调用fn()的次数期望是k * n^k / m。
注意到,当m<=n时,k恰好等于1,所以两种情况可以统一了。

(8)对7的一步优化:
方法(7)中,在调用了k次fn()之后,假设得到的值是x(范围是[0, n^k – 1]),直接将x>m的情况舍弃掉了;实际上有更高效点的利用方法,借鉴方法(5),将x取值范围按m进行分组,只需舍弃最后的不足m个数的那组即可。所以一次循环成功的概率变为了(X – X %m) / X,其中X = n^k,期望也就变成了kX/(X – X%m)。显然,n > m的时候同样成立,k取1即可。

点赞