剑指Offer(Java版):n个骰子的点数

题目:把n个骰子仍在地上,所有骰子朝上一面的点数之和为s,输入n,打印出s的所有可能的值出现的概率。

解法一:基于递归求骰子的点数,时间效率不够高

现在我们考虑如何统计每一个点数出现的次数。要向求出n个骰子的点数 和,可以先把n个骰子分为两堆:第一堆只有一个,另一个有n-1个。单独的那一个有可能出现从1到6的点数。我们需要计算从1到6的每一种点数和剩下的 n-1个骰子来计算点数和。接下来把剩下的n-1个骰子还是分成两堆,第一堆只有一个,第二堆有n-2个。我们把上一轮哪个单独骰子的点数和这一轮单独骰 子的点数相加,再和n-2个骰子来计算点数和。分析到这里,我们不难发现这是一种递归的思路,递归结束的条件就是最后只剩下一个骰子。

解法二:基于循环求骰子的点数,时间性能好

可以换一个思路来解决这个问题,我们可以考虑用两个数组来存储骰子点 数的每一个综述出现的次数。在一次循环中,每一个数组中的第n个数字表示骰子和为n出现的次数。在下一轮循环中,我们加上一个新的骰子,此时和为n出现的 次数。下一轮中,我们加上一个新的骰子,此时和为n的骰子出现的次数应该等于上一次循环中骰子点数和为n-1,n-2,n-3,n-4,n-5的次数之 和,所以我们把另一个数组的第n个数字设为前一个数组对应的第n-1,n-2,n-3,n-4,n-5 

 

分析问题

如果是用笨方法,一般人最开始都会想到笨方法,那就是枚举法

举个例子,比如两个骰子,第一个骰子的结果为1,2,3,4,5,6,两个骰子的结果是2,3,4,5,6,7;3,4,5,6,7,8;4,5,6,7,8,9;……7,8,9,10,11,12,共三十六种,用2的平方size的数组记录这36个结果

仔细分析可以发现其实这其中有很多是重复的,所以去除重复,考虑最小的应该是2,也就是n,最大的应该是12,也就是6n ,所以所有的结果应该只有6n-n+1=5n+1 种,如果我们开辟一个最大index=6n的数组也就是size为6n+1的数组就可以放下这所有的结果,但是其中index为0-(n-1)的位置上没 有数放,这里我们有两种解决方案,一种是就让它空着,这样的好处是,结果为s的就可以直接放在index为s的位上,不过如果我们想节省这部分的空间,可 以将所有数据往前移一下,也就是把和为s的放在s-n上即可,这样我们就只需要size为5n+1的数组

所以我们再声明一个结果数组,5n+1大小,通过遍历前面的n平方大小的数组,出现和为s就在5n+1大小的s-n位上加1即可

这样的方式,时间复杂度为n平方,可见并不理想,我们可以降低时间复杂度

首先想到是否能退化问题,比如n个骰子与n-1个骰子之间的关系,比如n个骰子的结果是n-1个骰子的结果分别加上1-6而得,于是n-1个骰子的结果又是n-2个骰子的结果分别再加上1-6所得

但递归的方法并不是很好,很多重复计算,重复计算的问题可以考虑斐波拉契计算过程,我们最后提出一种以空间换时间的方法,也是传统的记录中间结果的方法,根斐波拉契的优化很像,将某些中间结果存起来以减少递归过程的重复计算问题 , 主体计算次数,将次数存到数组中,由于要用到递归,我们最好单独写一个probabilityOfDice函数,probabilityOfDice函数中的参数要包括递归的时候要用到的那些变量,比如总的n,现在的n,以及现在的sum,以及贯穿始终的次数数组,

public static void probabilityOfDice(int n,int curDiceValue,int numOfDices,int curSum,int[] times){  
        if(numOfDices==1){ //如果只有一个骰子
            int sum=curSum+curDiceValue;  
            times[sum-n]++;//n*1 to n*MAX <---> 0 to len  
        }else{  
            int sum=curSum+curDiceValue;  
            for(int i=1;i<=MAX;i++){  
                probabilityOfDice(n,i,numOfDices-1,sum,times);  
            }  
        }  
    } 

而计算次数的时候就是去调用这个 probabilityOfDice 的函数

for(int i=1;i<=MAX;i++){//initial the first dice.  
            probabilityOfDice(n,i,n,0,times);//count the times of each possible sum  
        } 

 

考虑n=2 时的递归过程,首先n=2, numOfDices =2, curSum =1,表明第一个骰子甩出一个1,由于 numOfDices =2表明现在有两个骰子,所以进入else部分,i又从1到6循 环,表明这是进入到第二个骰子在甩了,首先i为1,表明又甩出一个1,这时候nC=1,就将2-n的位置上加1,表明结果为2的次数加1,然后退到上一 层,i++,此时还是第二个骰子在甩,甩出一个2,此时 curSum =3, numOfDices =1,所以在和为3的位置上加1,一直这样,到了和为7的位置上加1的时候,会退 到在上一次循环,这时候表明第一个骰子甩出了一个2,此时进入第二个骰子,依次会出现和为3,4,5,6,7,8的结果,然后再在相应位置上加1即可

 

 

基于这个思路实现代码如下:

package cglib;

 

public class List1
{  
      /**
     * n个骰子的点数
     * 把n个骰子扔在地上,所有骰子朝上一面的点数之和为S。输入n,打印出S的所有可能的值出现的概率。
     * 在以下求解过程中,我们把骰子看作是有序的。
     * 例如当n=2时,我们认为(1,2)和(2,1)是两种不同的情况
     */  
    private static int MAX=8;  
    public static void main(String[] args) {  
        int n=3;  
        printProbabilityOfDice(n);//solution 1,use recursion  
        System.out.println("============");  
        printProbabilityOfDice2(n);//solution 2,use DP  
    }  
 
    public static void printProbabilityOfDice(int n){  
        if(n<1){  
            return;  
        }  
        double total=Math.pow(MAX, n);   
        int len=n*MAX-n*1+1;//the sum of n dices is from n*1 to n*MAX  
        int[] times=new int[len];  
        for(int i=1;i<=MAX;i++){//initial the first dice.  
            probabilityOfDice(n,i,n,0,times);//count the times of each possible sum  
        }  
        for(int i=0;i<len;i++){  
            System.out.println((i+n)+","+times[i]+"/"+total);  
        }  
          
    }  
    public static void probabilityOfDice(int n,int curDiceValue,int numOfDices,int curSum,int[] times){  
        if(numOfDices==1){ //如果只有一个骰子
            int sum=curSum+curDiceValue;  
            times[sum-n]++;//n*1 to n*MAX <---> 0 to len  
        }else{  
            int sum=curSum+curDiceValue;  
            for(int i=1;i<=MAX;i++){  
                probabilityOfDice(n,i,numOfDices-1,sum,times);  
            }  
        }  
    }  
      
    /*
有k-1个骰子时,再增加一个骰子,这个骰子的点数只可能为1、2、3、4、5或6。那k个骰子得到点数和为n的情况有:
(k-1,n-1):第k个骰子投了点数1
(k-1,n-2):第k个骰子投了点数2
(k-1,n-3):第k个骰子投了点数3
....
(k-1,n-6):第k个骰子投了点数6
在k-1个骰子的基础上,再增加一个骰子出现点数和为n的结果只有这6种情况!
所以:f(k,n)=f(k-1,n-1)+f(k-1,n-2)+f(k-1,n-3)+f(k-1,n-4)+f(k-1,n-5)+f(k-1,n-6)
初始化:有1个骰子,f(1,1)=f(1,2)=f(1,3)=f(1,4)=f(1,5)=f(1,6)=1。
     */  
    public static void printProbabilityOfDice2(int n){  
        if(n<1){  
            return;  
        }  
        double total=Math.pow(MAX, n);   
        int maxSum=n*MAX;  
        double[][] f=new double[n+1][n*MAX+1];  
        for(int i=1;i<=MAX;i++){  
            f[1][i]=1;  //一个骰子初始化的时候,次数都是1
        }  
        for(int k=2;k<=n;k++){ //另外的n-1个骰子
            for(int sum=n;sum<=maxSum;sum++){  
                for(int i=1;sum-i>=1&&i<=MAX;i++){  
                    f[k][sum]+=f[k-1][sum-i];  
                }  
            }  
        }  
          
        for(int sum=n;sum<=maxSum;sum++){  
            System.out.println(sum+","+f[n][sum]+"/"+total);  
        }  
    }
         
    } 

 

输出:
3,1/512.0
4,3/512.0
5,6/512.0
6,10/512.0
7,15/512.0
8,21/512.0
9,28/512.0
10,36/512.0
11,42/512.0
12,46/512.0
13,48/512.0
14,48/512.0
15,46/512.0
16,42/512.0
17,36/512.0
18,28/512.0
19,21/512.0
20,15/512.0
21,10/512.0
22,6/512.0
23,3/512.0
24,1/512.0
============
3,0.0/512.0
4,2.0/512.0
5,5.0/512.0
6,9.0/512.0
7,14.0/512.0
8,20.0/512.0
9,27.0/512.0
10,35.0/512.0
11,42.0/512.0
12,46.0/512.0
13,48.0/512.0
14,48.0/512.0
15,46.0/512.0
16,42.0/512.0
17,36.0/512.0
18,28.0/512.0
19,21.0/512.0
20,15.0/512.0
21,10.0/512.0
22,6.0/512.0
23,3.0/512.0
24,1.0/512.0

 

优化方法

我们需要将中间值存起来以减少递归过程中的重复计算问题,可以考虑我们用两个数组ABAB之上得到,B又在A之上再次得到,这样AB互相作为对方的中间值,其实这个思想跟斐波拉契迭代算法中用中间变量保存n-1,n-2的值有异曲同工之妙

我们用一个flag来实现数组AB的轮换,由于要轮转,我们最好声明一个二维数组,这样的话,如果flag=0时,1-flag用的就是数组1,如果flag=1时,1-flag用的就是数组0,

int[][] probabilities = new int[2][];  
        probabilities[0] = new int[g_maxValue * number + 1];  
        probabilities[1] = new int[g_maxValue * number + 1];  

 

我们以probabilities[0]作为初始的数组,那么我们对这个数组进行初始化是要将1-6都赋值为1,说明第一个骰子投完的结果存到了probabilities[0]

for (int i = 1; i <= g_maxValue; i++)  
            probabilities[0][i] = 1;  

然后就是第二个骰子,第二个骰子的结果存到probabilities[1],是以probabilities[0]为基础的,此时和为s的次数就是把probabilities[0]中和为s-1,s-2,s-3,s-4,s-5,s-6的次数加起来即可,

而第k次用k个骰子那么要更新的结果范围就是k到maxValue*k

所以连起来就是

for (int i = 1; i <= g_maxValue; i++)  
            probabilities[0][i] = 1;  
        for (int k = 2; k <= number; ++k) {  
            for (int i = 0; i < k; ++i)  
                probabilities[1 - flag][i] = 0;  
            for (int i = k; i <= g_maxValue * k; ++i) {  
                probabilities[1 - flag][i] = 0;  
                for (int j = 1; j <= i && j <= g_maxValue; ++j)  
                    probabilities[1 - flag][i] += probabilities[flag][i - j];  
            } 

然后就需要把probabilities[1]作为中间值数组,这里我们把flag赋值为1-flag即可

flag = 1 - flag;

 

如下代码 

package cglib;

 

public class List1
{  
      /**
     * n个骰子的点数
     * 把n个骰子扔在地上,所有骰子朝上一面的点数之和为S。输入n,打印出S的所有可能的值出现的概率。
     * 在以下求解过程中,我们把骰子看作是有序的。
     * 例如当n=2时,我们认为(1,2)和(2,1)是两种不同的情况
     */  
    private static int g_maxValue=8;  
    public static void main(String[] args) {  
        int n=3;  
        printProbability(n);//solution 1,use recursion  
        
    }  
 
    public static void printProbability(int number) {  
        if (number < 1)  
            return;   
        int[][] probabilities = new int[2][];  
        probabilities[0] = new int[g_maxValue * number + 1];  
        probabilities[1] = new int[g_maxValue * number + 1];  
        int flag = 0;  
        for (int i = 1; i <= g_maxValue; i++)  
            probabilities[ flag ][i] = 1;  
        for (int k = 2; k <= number; ++k) {  
            for (int i = 0; i < k; ++i)  
                probabilities[1 - flag][i] = 0;  
            for (int i = k; i <= g_maxValue * k; ++i) {  
                probabilities[1 - flag][i] = 0;  
                for (int j = 1; j <= i && j <= g_maxValue; ++j)  
                    probabilities[1 - flag][i] += probabilities[flag][i - j];  
            }  
            flag = 1 - flag;  
        }  
        double total = Math.pow(g_maxValue, number);  
        for (int i = number; i <= g_maxValue * number; i++) {  
            double ratio = (double) probabilities[flag][i] / total;  
            System.out.println(i);  
            System.out.println(ratio);  
        }  
    }  
         
    } 

 

输出:

3
0.001953125
4
0.005859375
5
0.01171875
6
0.01953125
7
0.029296875
8
0.041015625
9
0.0546875
10
0.0703125
11
0.08203125
12
0.08984375
13
0.09375
14
0.09375
15
0.08984375
16
0.08203125
17
0.0703125
18
0.0546875
19
0.041015625
20
0.029296875
21
0.01953125
22
0.01171875
23
0.005859375
24
0.001953125

简单例子:

package cglib;

public class jiekou {
    public int CountNumber(int n, int s) {  
        //n个骰子点数之和范围在n到6n之间,否则数据不合法  
        if(s < n || s > 6*n)   
            return 0;  
        //当有一个骰子时,一次骰子点数为s(1 <= s <= 6)的次数当然是1  
        if(n == 1)   
            return 1;  
        else  
            return CountNumber(n-1, s-6) + CountNumber(n-1, s-5) + CountNumber(n-1, s-4) +   
                      CountNumber(n-1, s-3) +CountNumber(n-1, s-2) + CountNumber(n-1, s-1);  
    }  
    public void listDiceProbability(int n) {  
        int i=0;  
        int nTotal = (int) Math.pow((double)6, (double)n);  
        for(i = n; i <= 6 * n; i++) {  
            System.out.println(n+"个骰子"+"出现骰子点数和s= "+i+","+"出现的次数="+ CountNumber(n,i)+","+"总的排列次数="+ nTotal);  
        }  
    }  
      
        
    public static void main(String[] args){  
         
        jiekou test = new jiekou();  
        test.listDiceProbability(3);  
    }  
        
        }
    
  

输出:

3个骰子出现骰子点数和s= 3,出现的次数=1,总的排列次数=216
3个骰子出现骰子点数和s= 4,出现的次数=3,总的排列次数=216
3个骰子出现骰子点数和s= 5,出现的次数=6,总的排列次数=216
3个骰子出现骰子点数和s= 6,出现的次数=10,总的排列次数=216
3个骰子出现骰子点数和s= 7,出现的次数=15,总的排列次数=216
3个骰子出现骰子点数和s= 8,出现的次数=21,总的排列次数=216
3个骰子出现骰子点数和s= 9,出现的次数=25,总的排列次数=216
3个骰子出现骰子点数和s= 10,出现的次数=27,总的排列次数=216
3个骰子出现骰子点数和s= 11,出现的次数=27,总的排列次数=216
3个骰子出现骰子点数和s= 12,出现的次数=25,总的排列次数=216
3个骰子出现骰子点数和s= 13,出现的次数=21,总的排列次数=216
3个骰子出现骰子点数和s= 14,出现的次数=15,总的排列次数=216
3个骰子出现骰子点数和s= 15,出现的次数=10,总的排列次数=216
3个骰子出现骰子点数和s= 16,出现的次数=6,总的排列次数=216
3个骰子出现骰子点数和s= 17,出现的次数=3,总的排列次数=216
3个骰子出现骰子点数和s= 18,出现的次数=1,总的排列次数=216

 

转载于:https://my.oschina.net/u/2822116/blog/726876

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《offer JavaPDF》是一本广受欢迎的面试算法题解答南。这本书主要针对求职者准备技术面试,特别是Java语言相关的职位。这本书不仅仅提供了问题解析,还包含了详细的答案和清晰的思路。这使得读者能够更好地理解问题,并掌握解决问题的技巧。 这本书采用了PDF格式,具有易于阅读和携带性强的优点。读者可以将其存储在电脑、手机或平板电脑中,随时随地学习和复习。这为读者提供了极大的便利。 《offer JavaPDF》不仅仅是一个算法题解答南,还包含了一些求职技巧和面试准备建议。这些内容帮助读者了解面试流程、优化简历、提高面试技巧等,并提供了一些建议来克服可能的挑战。这些经验和建议对求职者来说非常有价值,能够帮助他们在面试中更加出色地表现。 总的来说,《offer JavaPDF》是一本实用的书籍,对于准备技术面试的求职者来说,是一份宝贵的资料。无论是对于算法题的解析,还是对于求职技巧的培养,这本书都能提供很多帮助。如果你是一个Java语言的求职者,我强烈建议你阅读这本书,它将为你的面试准备带来很大的帮助。 ### 回答2: Offer是一本非常经典的面试刷题南,它包含了很多常见的编程面试题目,并提供了详细的解答和解题思路。Offer JavaPDF则是将这本书中的题目和解答都用Java语言实现,并以PDF文档的形式呈现出来。 这本JavaOffer PDF非常有用,特别适合正在准备面试的程序员。它将面试题目按照不同的难度级别进行了分类,并提供了相应的解答和解题思路。这样可以帮助程序员更好地了解面试官考察的重点和思考问题的方式,提高自己解题的能力。 此外,这本JavaOffer PDF还提供了一些常见的算法和数据结构的实现,帮助程序员更好地理解和掌握这些基础知识。通过阅读和实践这些题目,程序员可以提高自己的编程能力和解决问题的能力,为日后的面试做好充分准备。 总的来说,Offer JavaPDF是一本非常实用的面试刷题南,它以Java语言实现了原书中的题目和解答,并提供了详细的解题思路。它对于准备面试的程序员来说是一本非常有价值的参考书籍,能够帮助他们提高解题能力,更好地应对面试挑战。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值