zoukankan      html  css  js  c++  java
  • 这真的是初三教科书里的概率题么?

      版权申明:本文为博主窗户(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
    ", 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程序来表示如下链接文件:    

                         

  • 相关阅读:
    poj 2046 Power Strings KMP
    谈谈需求变更的用户签字确认问题
    Oracle 10g客户端 安装(配图)
    如何让有能力的下属承担更多职责
    软件性能测试工具在测试工作中的重要性
    Oracle 10g 服务器端 安装图解
    JS 操作页面基础操作:禁止另存 防止复制 防止选择
    Javascript 长整型 转 C# DateTime
    配置AJAX Enabled WCF在hosting时: Showing the serviceMetadata in an ASP.NET AJAX Service
    ICON资源网站
  • 原文地址:https://www.cnblogs.com/Colin-Cai/p/9790468.html
Copyright © 2011-2022 走看看