劍指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 優化

 

分析問題 spa

若是是用笨方法,通常人最開始都會想到笨方法,那就是枚舉法 遞歸

舉個例子,好比兩個骰子,第一個骰子的結果爲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個結果 it

仔細分析能夠發現其實這其中有不少是重複的,因此去除重複,考慮最小的應該是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的數組 io

因此咱們再聲明一個結果數組,5n+1大小,經過遍歷前面的n平方大小的數組,出現和爲s就在5n+1大小的s-n位上加1便可 class

這樣的方式,時間複雜度爲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個骰子時,再增長一個骰子,這個骰子的點數只可能爲一、二、三、四、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

相關文章
相關標籤/搜索