版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖。如要转贴,必须注明原文网址
http://www.cnblogs.com/Colin-Cai/p/9790468.html
作者:窗户
QQ/微信:6679072
E-mail:[email protected]
北师大版九年级上册第74页有如下这题:
怕图片看不清楚,我抄一遍如下:
将36个球放入标有 1,2,...,12 这 12个号码的 12 个盒子中,然后掷两枚质地均匀的骰子,掷得的点数之和是几,就从几号盒子中摸出一个球。为了尽快将球模完,你觉得应该怎样放球?
这道题目可谓用意深远啊,试分析如下。
可能的解答?
无论如何,我们先得想想题目是什么意思。所谓质地均匀的骰子,解读一下,就是每次掷骰子,掷得1-6点中任何一点的概率均为1/6。
那么,同时掷两枚骰子呢?
假设两枚骰子分别为A、B,那么一起掷的结果可能如下:
A 1点,B 1点
A 1点,B 2点
...
A 6点,B 6点
以上一共36种可能,每种可能概率均等,都是1/36
于是,我们很容易知道,两个骰子一起掷得点数之和的概率:
2点和12点的概率是1/36
3点和11点的概率是2/36(1/18)
4点和10点的概率是3/36(1/12)
5点和9点的概率是4/36(1/9)
6点和8点的概率是5/36
7点的概率是6/36(1/6)
注:两个骰子的点数加在一起不可能是1,所以编号为1的盒子是不可能放球的
题目实际上考虑的是拿光所有的球,所需要掷骰子的次数的数学期望。而题目是希望找到这个数学期望最少的放法。
于是一个“可能”的解答如下:
要想更快的拿完,每个盒子的球数应该是 概率 X 球的总数
于是,
编号为2和12的盒子里面各放1个球
编号为3和11的盒子里面各放2个球
编号为4和10的盒子里面各放3个球
编号为5和9的盒子里面各放4个球
编号为6和8的盒子里面各放5个球
编号为7的盒子里面放6个球
我想,这应该是出题者希望的解答吧,也就是“标准答案”?
否则,这个问题,就太复杂了。很可惜,上述放法并不是最好的。
蒙特卡洛方法
对于一个具体的放法,这个拿完次数的数学期望是多少呢?
一开始我在纸上不断的画啊,只见一大堆排列组合、无穷级数铺天盖地而来。
我的个天啊,先再找条路吧。于是我就去选择蒙特卡洛方法(Monte Carlo Method)。
简单点说,就是用计算机模拟每次掷骰子取球的过程直到取完。实验反复做多次,根据大数定理,对于数学期望所在的任意领域,随着实验次数的增加,平均掷骰子数量落到这个领域内的概率趋向于1。
上面的原理太数学化,自然也超过了初中生的理解范畴。但利用这个原理,我们并不难用任何我们熟悉的语言写出这个模拟实验。
关键就是如何选择取哪个盒子,本文中我们选择可以和题目中一样,使用两个骰子,每个骰子产生1~6平均分布,然后加一起。然后这并不具备一般性,对于一般问题我们可以引入轮盘法。
我们就以本文为例子,我们如何选择这12个盒子。
假设我们有一个随机手段,一次可以产生1~36这36个数的其中一个,并且产生每个数的概率都是1/36
那么,我们可以这样定:
如果产生的数是1,则选择2号盒
如果产生的数是2,则选择12号盒
如果产生的数在3~4里,则选择3号盒
如果产生的数在5~6里,则选择11号盒
...
如果产生的数在31~36里,则选择7号盒
这样,正好符合取每个盒子的概率。
以上的取法仿佛是一个拉斯维加斯的轮盘,所以叫轮盘法。
我们用上面的原理,来做这么一个简化的问题:
假设有三个盒子,每次选择1号盒子和2号盒子的概率为1/10,选择3号盒子的概率是4/5
现在,我们来看10个球不同方法选择完的选取次数的数学期望。
代码用Python很容易写出来:
import random cnt = 0 for i in range(0,10000): a = [1,1,8] while True: cnt += 1 n = random.randint(1,10) k = 0 if n==1 else 1 if n==2 else 2 if a[k]>0: a[k] -= 1; if sum(a)==0: break print(cnt)
上面代码就是用蒙特卡洛方法测1号、2号、3号放的球分别为1、1、8,做10000次实验来统计。
按照之前的“解题逻辑”,1、1、8这种放法应该是数学期望最小的。我们就来验证一下。
执行多次,发现每一次输出的值都在170000左右,那么我们猜测数学期望应该也在17左右。
我们来验证验证1号、2号、3号放的球分别为0、0、10的情况,也就是1号、2号盒子都不放球,10个球全部放在概率最高的3号盒子。
上述代码只需要把第四行后面的数组改成[0,0,10]即可
执行多次,发现每一次输出的值都在1250000附近,那么我们猜测数学期望应该也在12.5左右。
居然比1、1、8的数学期望要小?
再或者真的是小概率事件必然发生?我们看到的是假象?……
反复做过多次实验,当然应该是真相了。然而蒙特卡洛方法毕竟有概率的成分在里面,也就是未必绝对靠谱,于是我们还是要深入去解决这个问题。
递归
鉴于蒙特卡洛方法有些“不靠谱”,我们需要精确计算。
以简单情况为例,
假设我们现在有三个盒子,1号盒子取到的概率为0.1,2号盒子取到的概率为0.1,3号盒子取到的概率为0.8,
现在我们在1号盒子里放0个球(未放球),在2号盒子里放1个球,在3号盒子里放9个球,
我们现在去研究拿完所经历的“掷骰子”次数的数学期望。
我们借用Python的语法,称这里的这个数学期望为mean([0.1,0.1,0.8], [0,1,9])
这里,mean函数带两个参数,第一个是各个盒子概率的列表,第二个是各个盒子所放球数的列表。
我们考虑第一次选择盒子(掷骰子),只可能会有以下三种情况:
选择每个盒子都有个概率,再加上刚刚已经选择过的这一次,
那么,有
mean([0.1,0.1,0.8],[0,1,9]) = mean([0.1,0.1,0.8],[0,1,9]) * 0.1
+ mean([0.1,0.1,0.8],[0,0,9]) * 0.1
+ mean([0.1,0.1,0.8],[0,1,8]) * 0.8
+ 1
等式左右都有mean([0.1,0.1,0.8],[0,1,9]),移下项,再除一下,得到:
mean([0.1,0.1,0.8],[0,1,9]) = (1 + mean([0.1,0.1,0.8],[0,0,9]) * 0.1 + mean([0.1,0.1,0.8],[0,1,8]) * 0.8) / (1 - 0.1)
以上红色字体部分,是遍历所有球数不为0的盒子,将这个盒子里的球减1所得到的问题的数学期望与盒子的概率相乘, 所有这样的值的累和;
以上红色背景部分,是遍历所有的球数位0的盒子,将这个盒子取到的概率累和。
这样就得到一个递归。
另外,也要考虑这个递归的边界。这个倒也容易,也就是当所有盒子都没球的时候,这个数学期望当然为0。
于是就有了以下的代码:
def mean(p, n): if sum(n)==0: return 0 return (1 + sum(list(map(lambda i:0 if n[i]==0 else \ mean(p,list(map(lambda j:n[j] if i!=j else n[j]-1,range(0,len(n)))))\ * p[i],\ range(0,len(n))))))\ / (1 - sum(list(map(lambda x,y:x if y==0 else 0,p,n))))
上面似乎像甄嬛体,说人话:
def mean(p, n): if sum(n)==0: return 0 f1 = 1 f2 = 1 for i in range(len(n)): if n[i]!=0: n2 = n.copy() n2[i] -= 1 f1 += p[i]*mean(p,n2) else: f2 -= p[i] return f1/f2
上面是python3,如果是Python2的话,list是没有copy方法的,需要先导入copy模块,使用copy.copy来复制list
树递归有着太多重复计算,对于36个球,其计算规模何等夸张,显然是不现实的。
为了可以快速的递归,就得做一片缓存来记录之前的计算,从而避免重复计算展开,让计算时间变的可行。考虑到效率,这里我用C语言写,只针对本章开头这个问题,也就是36个球,放在2~12号盒子(1号因为不可能选到,被我排除了)。
#include#include #include <string.h> #include typedef union { uint64_t i; double p; }data_t; data_t * data; double cal_mean(int *n, int sub_pos, int a_cnt, const double *p, const int *seq) { int i, pos = 0; double f1 = 1.0, f2 = 1.0; if(a_cnt == 0) { n[sub_pos]++; return 0.0; } for(i=0;i<11;i++) pos += n[i]*seq[i]; if(data[pos].i) { n[sub_pos]++; return data[pos].p; } for(i=0;i<11;i++) { if(n[i]) { n[i]--; f1 += cal_mean(n,i,a_cnt-1,p,seq) * p[i]; } else { f2 -= p[i]; } } f1 /= f2; data[pos].p = f1; n[sub_pos]++; return f1; } int main(int argc, char**argv) { int i, n[11], seq[11]; double mean, p[11]; data = malloc(sizeof(data_t)*atoi(argv[1])); if(data == NULL) { return 1; } for(i=0;i<11;i++) { p[i] = (6-(i+1)/2)/36.0; } while(1) { if(11 != scanf("%d%d%d%d%d%d%d%d%d%d%d", &n[0],&n[1],&n[2],&n[3],&n[4],&n[5], &n[6],&n[7],&n[8],&n[9],&n[10])) break; for(i=0;i<11;i++) printf("%d ",n[i]); seq[0] = 1; for(i=1;i<11;i++) seq[i] = seq[i-1]*(n[i-1]+1); memset(data,0,sizeof(data_t)*seq[10]*(n[10]+1)); mean = cal_mean(n,0,36,p,seq); printf("%.8lf\n", mean); } free(data); return 0; }
这个程序不细讲,用到了一点点技巧,比如Python里写的时候,递归里用来表示每个盒子球数的列表是复制出来的,但在这个程序里,表示每个盒子球数的数组内存是共用的。另外一点,为了方便,main函数里放每个盒子球数的数组n和每个盒子取到概率的数组p都是按照从盒子概率从大到小顺序的,也就是可以看成顺序是7号盒、6号盒、8号盒、5号盒、9号盒、4号盒、10号盒、3号盒、11号盒、2号盒、12号盒。
验证范围
现在,我们有了数学期望的计算方法。就需要对可能的方法进行验证。
根据排列组合知识,利用插板法,36个一样的球放进11个盒子,所有放法数量应该有
这个数量明显太过于夸张。我们考虑到2号和12号、3号和11号、4号和10号、5号和9号、6号和8号的取到概率是相同的,
考虑到对称性,也就是说,对于这5对盒子,每一对的两个盒子里面的球数互换,数学期望都是一样的。
从而我们的验证范围可以做一个下降,
只可惜这个数量还是不太现实。
如果对于其中两个取到概率不相等的盒子A和B,P(A)>P(B),但A盒球的数量小于B盒球的数量,我们猜测此时取完的数学期望大于A、B两盒球数互换情况下取完的数学期望。
以上命题成立,但证明起来比较复杂,此处略去。
也就是,对于数学期望最小的情况,球数的大小顺序一定和盒子取到概率大小顺序一致(相同概率的盒子球数未必相同)。
按照上述引理,再根据概率相同的盒子的对称性,我们可以得到数学期望最小值发生以下的验证范围:
7号球数 ≥ 6号球数 ≥ 8号球数 ≥ 5号球数 ≥ 9号球数 ≥ 4号球数 ≥ 10号球数 ≥ 3号球数 ≥ 11号球数 ≥ 2号球数 ≥ 12号球数
使用递归不难用Scheme利用递归写出以下的代码列出满足上述条件的所有7号球数、6号球数、...12号球数:
(define (list-all n pieces max-num) (if (= pieces 1) (if (> n max-num) '() (list (list n))) (apply append (map (lambda (a) (map (lambda (x) (cons a x)) (list-all (- n a) (- pieces 1) a))) (range 0 (+ (min n max-num) 1)))))) (define (pr lst) (if (null? lst) (newline) (begin (display (car lst)) (write-char #\space) (pr (cdr lst))))) (for-each pr (list-all 36 11 36))
Shell
计算数学期望的程序和产生验证范围的程序都有了,假设编译后,计算数学期望的程序编译后叫cal-mean,产生验证范围的程序编译后叫make-range
以下shell就可以得到最后的结果
#!/bin/bash ./make-range >input.txt ./cal-mean $(awk '{x=1;for(i=1;i<=NF;i++)x*=$i+1;if(size' input.txt) output.txt sort -k 12 -n output.txt | head -n 1
运行了半个小时,终于得到了最后的结果:
8 6 6 4 4 3 3 1 1 0 0 69.56934392
前面的8、6、6、4、4、3、3、1、1、0、0就分别是7号盒、6号盒、8号盒、5号盒、9号盒、4号盒、10号盒、3号盒、11号盒、2号盒、12号盒里的球数,而69.56934392则是取完的掷骰子次数的数学期望,此数学期望是所有情况下最小。从而这种球的方法也就是题目的解答。
然而,如此复杂的过程得到的最终结果真的是这道初中数学题的原意?出题的老师真的出对了题目?
完整测试程序,用一个shell程序来表示如下链接文件: