引言:写这篇文章的初衷只是想做个笔记,因为这道题代码量有点大,有点抽象,而书上并没有详细的注释。为了加深印象和便于下次复习,做个记录。
原题:把n个骰子扔到地上,所有骰子朝上一面的点数之后为s. 输入n,打印出s所有可能的值出现的概率。(每个骰子6个面,点数从1到6)
解法一:基于递归,时间效率不高
递归的思想一般是分而治之,把n个骰子分为第一个和剩下的n-1个。先计算第一个骰子每个点数出现的次数,再计算剩余n-1个骰子出现的点数之和。求n-1个骰子的点数之的方法和前面讲的一样,即再次把n-1个骰子分成两堆------第一个和剩下的n-2个。n个骰子,每个骰子6个面,总共有6n个组合。这6n个组合之中肯定有重复的,我们知道其范围是n~6n,对于每种情况我们可以用缓存机制记录下来,每当其发生一次我们令其对应的单元加1。
我们定义一个长度为6n-n+1的数组,和为s的点数出现的次数保存到数组第s-n个元素里。为什么是6n-n+1呢?因为n个骰子的和最少是n,最大是6n,介于这两者之间的每一个情况都可能会发生,总共6n-n+1种情况。下面是java源码:
1 private static final int g_maxValue = 6; 2 //基于递归求骰子点数,时间效率不高 3 public static void PrintProbability(int number){ 4 if(number<1) return; 5 int maxSum = number*g_maxValue; 6 int[] pProbabilities = new int[maxSum-number+1]; 7 //初始化,开始统计之前都为0次 8 for(int i=number;i<=maxSum;i++){ 9 pProbabilities[i-number] = 0; 10 } 11 double total = Math.pow(g_maxValue,number); 12 //probability(number,pProbabilities);这个函数计算n~6n每种情况出现的次数 13 probability(number,pProbabilities); 14 for(int i=number;i<=maxSum;i++){ 15 double ratio = pProbabilities[i-number]/total; 16 System.out.println("i: "+i+" ratio: "+ratio); 17 } 18 } 19 public static void probability(int number,int[] pProbabilities){ 20 for(int i=1;i<=g_maxValue;i++){//从第一个骰子开始 21 probability(number,number,i,pProbabilities); 22 } 23 } 24 //总共original个骰子,当前第 current个骰子,当前的和,贯穿始终的数组 25 public static void probability(int original,int current,int sum,int[] pProbabilities){ 26 if(current==1){ 27 pProbabilities[sum-original]++; 28 }else{ 29 for(int i=1;i<=g_maxValue;i++){ 30 probability(original,current-1,sum+i,pProbabilities); 31 } 32 } 33 }
这种方法思路非常简洁,但是递归实现会存在子问题重复求解的情况发生,所以当number很大的时候,其性能会慢的让人不能接受。
解法二:基于循环,时间性能好
递归一般是自顶向下的分析求解,而基于循环的方法则是自底向上。基于循环的一般需要更少的空间和更少的时间,性能较好,但是一般代码比较难懂。
书上的讲解比较简单,代码没有注释,这里本人用java实现了书本上的方法,注释比较详细。
1 //基于循环求骰子点数 2 public static void PrintProbability_1(int number){ 3 if(number<1){ 4 return; 5 } 6 int[][] pProbabilities = new int[2][g_maxValue*number +1]; 7 for(int i=0;i<g_maxValue;i++){//初始化数组 8 pProbabilities[0][i] = 0; 9 pProbabilities[1][i] = 0; 10 } 11 int flag = 0; 12 for(int i=1;i<=g_maxValue;i++){//当第一次抛掷骰子时,有6种可能,每种可能出现一次 13 pProbabilities[flag][i] = 1; 14 } 15 //从第二次开始掷骰子,假设第一个数组中的第n个数字表示骰子和为n出现的次数, 16 //在下一循环中,我们加上一个新骰子,此时和为n的骰子出现次数应该等于上一次循环中骰子点数和为n-1,n-2,n-3,n-4,n-5, 17 //n-6的次数总和,所以我们把另一个数组的第n个数字设为前一个数组对应的n-1,n-2,n-3,n-4,n-5,n-6之和 18 for(int k =2;k<=number;k++){ 19 for(int i=0;i<k;i++){//第k次掷骰子,和最小为k,小于k的情况是不可能发生的!所以另不可能发生的次数设置为0! 20 pProbabilities[1-flag][i] = 0; 21 } 22 for(int i=k;i<=g_maxValue*k;i++){//第k次掷骰子,和最小为k,最大为g_maxValue*k 23 pProbabilities[1-flag][i] = 0;//初始化,因为这个数组要重复使用,上一次的值要清0 24 for(int j=1;j<=i&&j<=g_maxValue;j++){ 25 pProbabilities[1-flag][i] += pProbabilities[flag][i-j]; 26 } 27 } 28 flag = 1-flag; 29 } 30 double total = Math.pow(g_maxValue, number); 31 for(int i=number;i<=g_maxValue*number;i++){ 32 double ratio = pProbabilities[flag][i]/total; 33 System.out.println("sum: "+i+" ratio: "+ratio); 34 } 35 }
运行结果:
sum: 6 ratio: 2.143347050754458E-5
sum: 7 ratio: 1.286008230452675E-4
sum: 8 ratio: 4.501028806584362E-4
sum: 9 ratio: 0.0012002743484224967
sum: 10 ratio: 0.002700617283950617
sum: 11 ratio: 0.005401234567901234
sum: 12 ratio: 0.00977366255144033
sum: 13 ratio: 0.016203703703703703
sum: 14 ratio: 0.02488425925925926
sum: 15 ratio: 0.03570816186556927
sum: 16 ratio: 0.048161008230452676
sum: 17 ratio: 0.061213991769547324
sum: 18 ratio: 0.07353823731138547
sum: 19 ratio: 0.08371913580246913
sum: 20 ratio: 0.09047067901234568
sum: 21 ratio: 0.09284979423868313
sum: 22 ratio: 0.09047067901234568
sum: 23 ratio: 0.08371913580246913
sum: 24 ratio: 0.07353823731138547
sum: 25 ratio: 0.061213991769547324
sum: 26 ratio: 0.048161008230452676
sum: 27 ratio: 0.03570816186556927
sum: 28 ratio: 0.02488425925925926
sum: 29 ratio: 0.016203703703703703
sum: 30 ratio: 0.00977366255144033
sum: 31 ratio: 0.005401234567901234
sum: 32 ratio: 0.002700617283950617
sum: 33 ratio: 0.0012002743484224967
sum: 34 ratio: 4.501028806584362E-4
sum: 35 ratio: 1.286008230452675E-4
sum: 36 ratio: 2.143347050754458E-5