posts - 72,  comments - 79,  trackbacks - 0
  版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖。如要转贴,必须注明原文网址

  http://www.cnblogs.com/Colin-Cai/p/9790468.html 

  作者:窗户

  QQ/微信:6679072

  E-mail:6679072@qq.com 

  北师大版九年级上册第74页有如下这题:

  

  怕图片看不清楚,我抄一遍如下:

  将36个球放入标有 1,2,...,12 这 12个号码的 12 个盒子中,然后掷两枚质地均匀的骰子,掷得的点数之和是几,就从几号盒子中摸出一个球。为了尽快将球模完,你觉得应该怎样放球?

  

  这道题目可谓用意深远啊,试分析如下。

 

 

  可能的解答?

  

  无论如何,我们先得想想题目是什么意思。所谓质地均匀的骰子,解读一下,就是每次掷骰子,掷得1-6点中任何一点的概率均为1/6

  那么,同时掷两枚骰子呢?

  假设两枚骰子分别为AB,那么一起掷的结果可能如下:

  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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
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<x)size=x}END{print size}' input.txt) <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程序来表示如下链接文件:    

                     

 

posted on 2018-10-25 19:06 窗户 阅读(...) 评论(...) 编辑 收藏