代码中的概率学(从理论到仿真的实践)
2015-05-04 13:56
120 查看
某日,我们几个同事在玩一个叫Splendor(璀璨宝石)的游戏。在翻贵族牌的时候,出现了一个极致的情况,连续翻出5块3x3宝石组合的牌。在众人惊呼这个奇迹的时候,突然有人提出,“这个概率有多少啊?”。于是,一帮理工科的死宅,开始了脑区的激活。
这个贵族牌的情况大致是这样的:
有两类牌,一类是2x4宝石组合,另一类是3x3宝石组合。
每类牌各有5张,一共10张。
一局游戏一次随机翻出其中5张牌。
可以看到,总共只有5张3x3宝石牌,如果需要翻出5张3x3宝石牌,那就意味着,每次随机翻牌时必须全部准确命中3x3宝石牌。也就是说,在所有可能的情况中,能够成功的情况只有一种,那就是5张翻出的牌即是全部5张已有的3x3宝石牌。
数学比较好的同学,马上想到概率为:510 ×49 ×38 ×27 ×16 \frac{5}{10}\times\frac{4}{9}\times\frac{3}{8}\times\frac{2}{7}\times\frac{1}{6}
数学更比较好的同学,直接想到了概率为:1C 5 10 \frac 1{C_{10}^5}
这两种方法计算的结果都为:0.003968 0.003968
也就是说,玩1000局游戏,大概只出现3局这样的情况。如果一天玩两局,半年左右才出现一局。
应该可以说,这确实是个极小概率事件了。
作为一名搞工程应用的同学,弄到这样,差不多就行了。
而作为一名搞理论研究的同学,只满足于计算结果是不够的,还必须努力发掘现象背后的普遍规律。
特别是作为一名有优秀数学内涵和编程背景的死宅,如果不能理论和实践两方面严格论证的话,那生活多没有色彩啊。
于是,有个死宅开始了自虐(自high)之路。
这个死宅发现,游戏时常常会出现,5张牌中有一张牌功亏一篑,众人每每叹息,又差一点点。这个现象引起了死宅的思考,这个差一点点的概率又是多少。或者说,5张牌里有4张3x3宝石牌的概率是多少?
最直接的反应就是,可能是“完美”概率的5倍吧?你看,5张里缺一张。
好吧,拿前面那个概率X5看看:0.003968×5=0.01984 0.003968\times5 = 0.01984
真的是这样的吗?
你这个死宅是怎么通过高数考试的?
你以为你说什么我们就信啊?你证明给我看啊!
靠………………
那就证明一下吧。
设卡牌总数为N,符合条件的卡牌数为M,一次随机选出K张牌。计算K张牌中全部为符合条件的牌的概率。
搞个图示说明一下:
O 可以是 X ,也可以是 Y。
+—————————–+
|| O O O O … O O O O || <– N个O,M个X
+—————————–+
取出K个O。
判断是否有K个X?
(这个图示好难看啊!)
(看懂了?不管了~)
从概率论的角度来讨论这个问题。
每一次选牌都是一种排列,也是一个样本点。
所有不同排列的数量,就是所有样本点的数量。
每一个样本点的几率是相等的。
这样一来,要计算概率,只要解决以下两个问题。
样本总数有多少?
符合条件的样本有多少?
即:P n n P_n^n
符合条件的样本数,就是前面K张牌有符合条件的排列数,乘上后面N-K张牌任意的排列数。
即:P k m ×P n−k n−k P_m^k \times P_{n-k}^{n-k}
概率为:符合条件样本数样本总数 =P k m ×P n−k n−k P n n \frac{符合条件样本数}{样本总数} = \frac{P_m^k \times P_{n-k}^{n-k}}{P_n^n}
代入N=10,M=5,K=5,验算一下概率为:0.003968 0.003968
即:P m n P_n^m
符合条件的样本数,就是前面K张牌有符合条件的排列数,乘上后面M-K张牌任意的排列数。
即:P k m ×P m−k n−k P_m^k \times P_{n-k}^{m-k}
概率为:符合条件样本数样本总数 =P k m ×P m−k n−k P m n \frac{符合条件样本数}{样本总数} = \frac{P_m^k \times P_{n-k}^{m-k}}{P_n^m}
代入N=10,M=5,K=5,验算一下概率为:0.003968 0.003968
写到这里,我们回头去检验那个4张牌为3x3宝石牌的概率。
N=10,M=5,K=4,代入计算
概率为:0.023810 0.023810
尼玛,和我的计划有出入啊!
所以说,要学好数学。真理和胡说八道之间还是有很大差距的。
(……)靠!到底谁对啊?
(……)貌似公式君好像有点道理~
……
理论是这样写完了,规律似乎也发现了,任意N,M,K貌似也能代入计算了。
但是,何以证明?你说我们就信啊!
概率这种东西怎么证明啊?
具有优秀数学内涵的死宅告诉你:有一门数学课,叫做“统计学”。
那么统计学里,概率应该怎么证明啊?很简单,我们用的方法是,抽样->调查->统计。简单来说,就是把样本“泼洒”出去,然后“捡”一些回来看看,对比一下正确/错误的比例,再和理论结果验证一下。
如果说得服你,就信我吧,说不服你,就去信上帝吧~
古典的统计学家要怎么说服大众呢?很简单,我们真的去捡样本啊。深入基层,了解群众,手拿纸本,奋笔疾书。要说计算钢珠落入圆圈中的概率,我们就真的抓一把钢珠丢下去,看有多少钢珠掉圆圈里。
不管你信不信,反正我是信了~
现代的统计学家怎么办?也去捡样本?
好吧,我们的时代已经很先进了,我们有计算机这货了,统计学家也进化成高级码农了。
那么,统计学码农应该做什么呢?
很明显好不好,埋头写代码呗!
写代码,这么难听!我们有名字的好不好。我们写的叫做“仿真”程序。
我们模仿现实发生的事情写程序,从而不用再苦逼的去鸟不拉屎的地方搞抽样,然后一字一句的写在古老发黄的记事本上,再拿个算盘或者计算器敲上一个通宵。
(……)虽然说不用再敲算盘或计算器了,但是作为一个码农,还是会在电脑键盘上敲上一个通宵的。
废话了那么多,来看看仿真程序吧。
输出结果:
N 10
M 5
K 5
Analytical : 0.003968
Simulation 1 (Break): 0.003950
Simulation 1 (Loop All): 0.003940
Simulation 2 (First K): 0.003940
Simulation 2 (Random K): 0.004080
N 10
M 5
K 4
Analytical : 0.023810
Simulation 1 (Break): 0.023850
Simulation 1 (Loop All): 0.023800
Simulation 2 (First K): 0.023780
Simulation 2 (Random K): 0.023870
N 10
M 5
K 3
Analytical : 0.083333
Simulation 1 (Break): 0.083320
Simulation 1 (Loop All): 0.083690
Simulation 2 (First K): 0.083000
Simulation 2 (Random K): 0.083910
N 10
M 4
K 4
Analytical : 0.004762
Simulation 1 (Break): 0.004860
Simulation 1 (Loop All): 0.004890
Simulation 2 (First K): 0.004890
Simulation 2 (Random K): 0.004600
N 10
M 4
K 3
Analytical : 0.033333
Simulation 1 (Break): 0.033970
Simulation 1 (Loop All): 0.032990
Simulation 2 (First K): 0.033500
Simulation 2 (Random K): 0.034150
N 10
M 4
K 2
Analytical : 0.133333
Simulation 1 (Break): 0.133760
Simulation 1 (Loop All): 0.132460
Simulation 2 (First K): 0.132750
Simulation 2 (Random K): 0.133990
可以看到仿真方法有两种。
挑选法。每一步都挑选符合条件的,遇到不符合条件的,挑选就可以结束了。这种方法适合条件比较简单,并适合每一个单步可以直接判断的。
模拟法。模仿现实环境,散布样本点,并抽样判断符合条件。这种方法仿真性比较强,适合条件比较复杂,尤其是组合条件比较多的。缺点是,程序运行速度比较慢。
从结果来看,无论N,M,K的值如何变化,理论计算概率值和仿真计算概率值都非常接近。
所以,可以有足够的理由认为:
你应该相信我们,相信科学。
当然,赞美一下上帝也不错哟~
这个贵族牌的情况大致是这样的:
有两类牌,一类是2x4宝石组合,另一类是3x3宝石组合。
每类牌各有5张,一共10张。
一局游戏一次随机翻出其中5张牌。
可以看到,总共只有5张3x3宝石牌,如果需要翻出5张3x3宝石牌,那就意味着,每次随机翻牌时必须全部准确命中3x3宝石牌。也就是说,在所有可能的情况中,能够成功的情况只有一种,那就是5张翻出的牌即是全部5张已有的3x3宝石牌。
数学比较好的同学,马上想到概率为:510 ×49 ×38 ×27 ×16 \frac{5}{10}\times\frac{4}{9}\times\frac{3}{8}\times\frac{2}{7}\times\frac{1}{6}
数学更比较好的同学,直接想到了概率为:1C 5 10 \frac 1{C_{10}^5}
这两种方法计算的结果都为:0.003968 0.003968
也就是说,玩1000局游戏,大概只出现3局这样的情况。如果一天玩两局,半年左右才出现一局。
应该可以说,这确实是个极小概率事件了。
作为一名搞工程应用的同学,弄到这样,差不多就行了。
而作为一名搞理论研究的同学,只满足于计算结果是不够的,还必须努力发掘现象背后的普遍规律。
特别是作为一名有优秀数学内涵和编程背景的死宅,如果不能理论和实践两方面严格论证的话,那生活多没有色彩啊。
于是,有个死宅开始了自虐(自high)之路。
这个死宅发现,游戏时常常会出现,5张牌中有一张牌功亏一篑,众人每每叹息,又差一点点。这个现象引起了死宅的思考,这个差一点点的概率又是多少。或者说,5张牌里有4张3x3宝石牌的概率是多少?
最直接的反应就是,可能是“完美”概率的5倍吧?你看,5张里缺一张。
好吧,拿前面那个概率X5看看:0.003968×5=0.01984 0.003968\times5 = 0.01984
真的是这样的吗?
你这个死宅是怎么通过高数考试的?
你以为你说什么我们就信啊?你证明给我看啊!
靠………………
那就证明一下吧。
设卡牌总数为N,符合条件的卡牌数为M,一次随机选出K张牌。计算K张牌中全部为符合条件的牌的概率。
搞个图示说明一下:
O 可以是 X ,也可以是 Y。
+—————————–+
|| O O O O … O O O O || <– N个O,M个X
+—————————–+
取出K个O。
判断是否有K个X?
(这个图示好难看啊!)
(看懂了?不管了~)
从概率论的角度来讨论这个问题。
每一次选牌都是一种排列,也是一个样本点。
所有不同排列的数量,就是所有样本点的数量。
每一个样本点的几率是相等的。
这样一来,要计算概率,只要解决以下两个问题。
样本总数有多少?
符合条件的样本有多少?
思路1:
样本总数,就是洗牌的排列总数,就是从N张牌中依次取出N张牌的排列总数。即:P n n P_n^n
符合条件的样本数,就是前面K张牌有符合条件的排列数,乘上后面N-K张牌任意的排列数。
即:P k m ×P n−k n−k P_m^k \times P_{n-k}^{n-k}
概率为:符合条件样本数样本总数 =P k m ×P n−k n−k P n n \frac{符合条件样本数}{样本总数} = \frac{P_m^k \times P_{n-k}^{n-k}}{P_n^n}
代入N=10,M=5,K=5,验算一下概率为:0.003968 0.003968
思路2:
样本总数,就是选出的M张牌的排列总数,就是从N张牌中依次取出M张牌的排列总数。即:P m n P_n^m
符合条件的样本数,就是前面K张牌有符合条件的排列数,乘上后面M-K张牌任意的排列数。
即:P k m ×P m−k n−k P_m^k \times P_{n-k}^{m-k}
概率为:符合条件样本数样本总数 =P k m ×P m−k n−k P m n \frac{符合条件样本数}{样本总数} = \frac{P_m^k \times P_{n-k}^{m-k}}{P_n^m}
代入N=10,M=5,K=5,验算一下概率为:0.003968 0.003968
写到这里,我们回头去检验那个4张牌为3x3宝石牌的概率。
N=10,M=5,K=4,代入计算
概率为:0.023810 0.023810
尼玛,和我的计划有出入啊!
所以说,要学好数学。真理和胡说八道之间还是有很大差距的。
(……)靠!到底谁对啊?
(……)貌似公式君好像有点道理~
……
理论是这样写完了,规律似乎也发现了,任意N,M,K貌似也能代入计算了。
但是,何以证明?你说我们就信啊!
概率这种东西怎么证明啊?
具有优秀数学内涵的死宅告诉你:有一门数学课,叫做“统计学”。
那么统计学里,概率应该怎么证明啊?很简单,我们用的方法是,抽样->调查->统计。简单来说,就是把样本“泼洒”出去,然后“捡”一些回来看看,对比一下正确/错误的比例,再和理论结果验证一下。
如果说得服你,就信我吧,说不服你,就去信上帝吧~
古典的统计学家要怎么说服大众呢?很简单,我们真的去捡样本啊。深入基层,了解群众,手拿纸本,奋笔疾书。要说计算钢珠落入圆圈中的概率,我们就真的抓一把钢珠丢下去,看有多少钢珠掉圆圈里。
不管你信不信,反正我是信了~
现代的统计学家怎么办?也去捡样本?
好吧,我们的时代已经很先进了,我们有计算机这货了,统计学家也进化成高级码农了。
那么,统计学码农应该做什么呢?
很明显好不好,埋头写代码呗!
写代码,这么难听!我们有名字的好不好。我们写的叫做“仿真”程序。
我们模仿现实发生的事情写程序,从而不用再苦逼的去鸟不拉屎的地方搞抽样,然后一字一句的写在古老发黄的记事本上,再拿个算盘或者计算器敲上一个通宵。
(……)虽然说不用再敲算盘或计算器了,但是作为一个码农,还是会在电脑键盘上敲上一个通宵的。
废话了那么多,来看看仿真程序吧。
#include <stdio.h> #include <stdlib.h> #include <math.h> #include <time.h> #define N 10 #define M 5 #define K 5 #define TEST_ROUND 100000 ///------------------------------------------------------------------------------ double fact(int n) { #define FACT_MAX_NUMBER 170 if (n < 0 || n > FACT_MAX_NUMBER) return 0; double dRet = 1; for (int i = n ; i > 1 ; --i) dRet *= i; return dRet; } void test_analytical() { double dHits = (fact(M)/fact(M-K))*fact(N-K); double dAll = fact(N); printf("Analytical : %f\n", dHits/dAll); } ///------------------------------------------------------------------------------ bool is_hit1(int k, int n, int m, bool bBreak) { bool bHit = true; while ( k-- ) { int nIndex = rand() % n; if ( nIndex + 1 > m ) { bHit = false; if ( bBreak ) break; } else m--; n--; } return bHit; } void test_probability1(int nSeed, bool bBreak) { srand(nSeed); int nHitTimes = 0; int nCount = TEST_ROUND; while ( nCount-- ) { if ( is_hit1(K, N, M, bBreak) ) nHitTimes++; } double dProbability = (double)nHitTimes/TEST_ROUND; printf("Simulation 1 (%s): %f\n", bBreak ? "Break":"Loop All", dProbability); } ///------------------------------------------------------------------------------ void get_indexs(int vIndexs[], int n, int k) { for ( int ii = 0; ii < n; ii++ ) vIndexs[ii] = 0; while ( k-- ) { int nNumeric = rand() % n; nNumeric++; int nCount = 0; for ( int nPos = 0; nPos < N; nPos++ ) { if ( 0 == vIndexs[nPos] ) { nCount++; if ( nCount == nNumeric ) { vIndexs[nPos] = 1; break; } } } n--; } } bool is_hit2(int vCards[], int n, int k, bool bFirstK) { if ( bFirstK ) { while ( k-- ) { if ( 0 == vCards[k] ) return false; } } else // random K position { int vIndexs ; get_indexs(vIndexs, n, k); for ( int nPos = 0; nPos < n; nPos++ ) { if ( vIndexs[nPos] ) { if ( 0 == vCards[nPos] ) return false; } } } return true; } void test_probability2(int nSeed, bool bFirstK) { srand(nSeed); int vCards ; int nHitTimes = 0; int nCount = TEST_ROUND; while ( nCount-- ) { get_indexs(vCards, N, M); if ( is_hit2(vCards, N, K, bFirstK) ) nHitTimes++; } double dProbability = (double)nHitTimes/TEST_ROUND; printf("Simulation 2 (%s K): %f\n", bFirstK ? "First":"Random", dProbability); } ///------------------------------------------------------------------------------ void main() { test_analytical(); test_probability1(time(NULL), true); test_probability1(time(NULL), false); test_probability2(time(NULL), true); test_probability2(time(NULL), false); }
输出结果:
N 10
M 5
K 5
Analytical : 0.003968
Simulation 1 (Break): 0.003950
Simulation 1 (Loop All): 0.003940
Simulation 2 (First K): 0.003940
Simulation 2 (Random K): 0.004080
N 10
M 5
K 4
Analytical : 0.023810
Simulation 1 (Break): 0.023850
Simulation 1 (Loop All): 0.023800
Simulation 2 (First K): 0.023780
Simulation 2 (Random K): 0.023870
N 10
M 5
K 3
Analytical : 0.083333
Simulation 1 (Break): 0.083320
Simulation 1 (Loop All): 0.083690
Simulation 2 (First K): 0.083000
Simulation 2 (Random K): 0.083910
N 10
M 4
K 4
Analytical : 0.004762
Simulation 1 (Break): 0.004860
Simulation 1 (Loop All): 0.004890
Simulation 2 (First K): 0.004890
Simulation 2 (Random K): 0.004600
N 10
M 4
K 3
Analytical : 0.033333
Simulation 1 (Break): 0.033970
Simulation 1 (Loop All): 0.032990
Simulation 2 (First K): 0.033500
Simulation 2 (Random K): 0.034150
N 10
M 4
K 2
Analytical : 0.133333
Simulation 1 (Break): 0.133760
Simulation 1 (Loop All): 0.132460
Simulation 2 (First K): 0.132750
Simulation 2 (Random K): 0.133990
可以看到仿真方法有两种。
挑选法。每一步都挑选符合条件的,遇到不符合条件的,挑选就可以结束了。这种方法适合条件比较简单,并适合每一个单步可以直接判断的。
模拟法。模仿现实环境,散布样本点,并抽样判断符合条件。这种方法仿真性比较强,适合条件比较复杂,尤其是组合条件比较多的。缺点是,程序运行速度比较慢。
从结果来看,无论N,M,K的值如何变化,理论计算概率值和仿真计算概率值都非常接近。
所以,可以有足够的理由认为:
你应该相信我们,相信科学。
当然,赞美一下上帝也不错哟~
相关文章推荐
- 【SVM理论到实践3】OpenCv中自带的两个例子:线性可分和线性不可分代码解读
- 【整洁代码之设计篇】Clean Design 1 - Single Responsibility Principle理论和实践
- 群蚁算法理论与实践全攻略——旅行商等路径优化问题的新方法【附C#群蚁算法完整项目代码】
- C 语言面向对象编程 理论加实践 代码
- 【C#代码实战】群蚁算法理论与实践全攻略——旅行商等路径优化问题的新方法
- 如何阅读代码?理论+实践 非常棒
- 群蚁算法理论与实践全攻略——旅行商等路径优化问题的新方法【附C#群蚁算法完整项目代码】
- 重构理论及实践——用工厂模式重构c++后台代码
- 重构理论及实践——用工厂模式重构c++后台代码
- [置顶]群蚁算法理论与实践全攻略——旅行商等路径优化问题的新方法【附C#群蚁算法完整项目代码】
- Linux多线程实践(1) --线程理论
- 由微服务,领域事件,分布式事件谈“业务分析三维度理论”的实践
- Java 理论与实践: 哈希
- Java 理论与实践-正确使用 Volatile 变量
- caffe SigmoidCrossEntropyLossLayer 理论代码学习
- 基于ns3的LTE仿真基本架构代码
- Java 理论与实践: 再谈 Urban 性能传言
- COM interop理论/实践
- Java 理论与实践: 您的小数点到哪里去了?
- 现代控制理论-章节组织结构和仿真应用案例详细分析