n个骰子的点数(剑指offer 43)
题目:把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出 s 的所有可能的值出现的概率。
玩过麻将的人都知道,骰子一共6个面,每个面上都有一个点数,对应1-6. 所以n个骰子的点数和的最小值为n,最大值为6n。另外根据排列组合的知识,我们还知道n个骰子的所有点数的排列数为6的n次方。要解决这个问题,我们要先统计出每一个点数出现的次数,然后把每一个点数出现的次数除以6的n次方,就能求出没个点数出现的概率。
解法1:基于递归求骰子点数,时间效率不够高
现在我们考虑如何统计每一个点数出现的次数。要想求出n个骰子的点数和,可以先把n个骰子分为两堆:第一堆只有一个,另一个有n-1个。单独的那一个有可能出现从1到6的点数。我们需要计算从1到6的每一个点数和剩下的n-1个骰子来计算点数和。接下来 把剩下的n-1个骰子还是分成两堆,第一堆只有一个,第二堆有n-2个。我们把上一轮那个单独骰子的点数和这一轮单独骰子的点数相加,再和剩下的n-2个筛子来计算点数和。分析到这里,我们不难发现这是一种递归的思路,递归结束的条件就是最后剩下一个骰子。
我们可以定义一个长度为6n-n+1的数组,和为s的点数出现的次数保存到数组第s-n个元素里。基于这种思路,我们可以写出如下代码:
import java.text.NumberFormat; //题目:把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s //的所有可能的值出现的概率。 public class DicesProbability { private static final int maxValue = 6; /** * 方法一:递归解法 */ public static void printProbability1(int number) { if(number<=0) return; //错误 int[] probabilities = new int[maxValue*number-number+1]; //下标为i,对应的值代表点数之和为i+number总共出现的情况次数 //点数从number~maxValue*number,所以数组大小为6*number-number+1 for(int i=0;i<probabilities.length;i++) probabilities[i]=0; //计算不同点数出现的次数 for(int i=1;i<=maxValue;i++) calP(probabilities,number,number-1,i); //第一次掷骰子,总点数只能是1~maxValue(即6) int totalP = (int) Math.pow(maxValue, number); //所有情况总共出现的次数 for( int i=0;i<probabilities.length ;i++) { double ratio = (double)probabilities[i]/totalP; NumberFormat format = NumberFormat.getPercentInstance(); format.setMaximumFractionDigits(2);//设置保留几位小数 System.out.println("点数和为"+(i+number)+"的概率为:"+format.format(ratio)); } } /** * 计算每种点数出现的次数 * @param number:骰子总个数 * @param curNumber:当前剩余骰子个数 * @param sum:各个骰子加起来的总点数 */ private static void calP(int[] probabilities, int number, int curNumber, int sum) { if(curNumber==0) { probabilities[sum-number]++; //总数为sum的情况存放在sum-number下标中 return; } for(int i=1;i<=maxValue;i++) calP(probabilities, number, curNumber-1, sum+i); //相当于剩余的骰子少一个,总点数增加。 }
方法二:基于循环求骰子点数,时间性能好
用数组存放每种骰子点数和出现的次数。令数组中下标为n的元素存放点数和为n的次数。我们设置循环,每个循环多投掷一个骰子,假设某一轮循环中,我们已知了各种点数和出现的次数;在下一轮循环时,我们新投掷了一个骰子,那么此时点数和为n的情况出现的次数就等于上一轮点数和为n-1,n-2,n-3,n-4,n-5,n-6的情况出现次数的总和。从第一个骰子开始,循环n次,就可以求得第n个骰子时各种点数和出现的次数。
我们这里用两个数组来分别存放本轮循环与下一轮循环的各种点数和出现的次数,不断交替使用。
//=========================================== /** * 方法二:基于循环求骰子点数,时间性能好 */ public static void printProbability2(int number) { if(number<=0) return; //错误 int[][] probabilities = new int[2][number*maxValue+1]; //[2]代表用两个数组交替保存,[number*maxValue+1]是指点数为所在下标时,该点数出现的总次数。 //probabilities[*][0]是没用的,只是为了让下标对应点数 for(int i=0;i<2;i++) { for(int j=0;j<number*maxValue;j++) { probabilities[i][j]=0; } } for(int i=1;i<=6;i++) probabilities[0][i]=1; //第一个骰子出现的情况 int flag=0; for(int curNumber=2;curNumber<=number;curNumber++) { //当前是第几个骰子 for(int i=0;i<curNumber;i++) probabilities[1-flag][i]=0; //前面的数据清零 for(int i=curNumber;i<=curNumber*maxValue;i++) { for(int j=1;j<=6 && j<=i ;j++) { probabilities[1-flag][i]+=probabilities[flag][i-j]; } } flag=1-flag; } int totalP = (int) Math.pow(maxValue, number); //所有情况总共出现的次数 for( int i=number;i<= number*6;i++) { double ratio = (double)probabilities[flag][i]/totalP; NumberFormat format = NumberFormat.getPercentInstance(); format.setMaximumFractionDigits(8);//设置保留几位小数 System.out.println("点数和为"+(i+number)+"的概率为:"+format.format(ratio)); } } public static void main(String[] args) { System.out.println("=========方法一============"); for(int i=0;i<=3;i++) { System.out.println("-----骰子数为"+i+"时-----"); printProbability1(i); } System.out.println("-----骰子数为"+11+"时-----"); printProbability1(11); System.out.println("=========方法二============"); for(int i=0;i<=3;i++) { System.out.println("-----骰子数为"+i+"时-----"); printProbability2(i); } System.out.println("-----骰子数为"+11+"时-----"); printProbability1(11); } }