劍指Offer面試題43(Java版):n個骰子的點數

 

劍指Offer面試題43(Java版):n個骰子的點數

題目:把n個骰子仍在地上,所有骰子朝上一面的點數之和爲s,輸入n,打印出s的所有可能的值出現的概率。



n個骰子朝上的數之和爲s,求s的所有可能以及概率

   

分析問題

   

如果是用笨方法,一般人最開始都會想到笨方法,那就是枚舉法

   

舉個例子,比如兩個骰子,第一個骰子的結果爲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,共三十六種,用n平方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所得

   

但遞歸的方法並不是很好,很多重複計算,重複計算的問題可以考慮斐波拉契計算過程,我們最後提出一種以空間換時間的方法,也是傳統的記錄中間結果的方法,根斐波拉契的優化很像,將某些中間結果存起來以減少遞歸過程的重複計算問題

   

解決問題

   

主體在如何計算次數,將次數存到數組中,由於要用到遞歸,我們最好單獨寫一個base函數,這是我的經驗,base函數中的參數要包括遞歸的時候要用到的那些變量,比如總的n,現在的n,以及現在的sum,以及貫穿始終的次數數組

   

static void baseProbabilities(int numTotal,int numCur,int sum,int[] probabilities){

if (numCur==1) {

probabilities[sum-numTotal]++;

}else {

for (int i = 1; i <=g_maxValue ; i++) {

baseProbabilities(numTotal, numCur-1, sum+i, probabilities);

}

}

}

而計算次數的時候就是去調用這個base的函數

   

for (int i = 1; i <=g_maxValue; i++) {

baseProbabilities(number, number, i, probabilities);

}

   

考慮n=2時的遞歸過程,首先nT=2,nC=2,sum=1,表明第一個骰子甩出一個1,由於nC=2表明現在有兩個骰子,所以進入else部分,i又從1到6循環,表明這是進入到第二個骰子在甩了,首先i爲1,表明又甩出一個1,這時候nC=1,就將2-n的位置上加1,表明結果爲2的次數加1,然後退到上一層,i++,此時還是第二個骰子在甩,甩出一個2,此時sum=3,nC=1,所以在和爲3的位置上加1,一直這樣,到了和爲7的位置上加1的時候,會退到在上一次循環,這時候表明第一個骰子甩出了一個2,此時進入第二個骰子,依次會出現和爲3,4,5,6,7,8的結果,然後再在相應位置上加1即可

   

優化方法

   

我們需要將中間值存起來以減少遞歸過程中的重複計算問題,可以考慮我們用兩個數組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]

   

然後就是第二個骰子,第二個骰子的結果存到probabilities[1],是以probabilities[0]爲基礎的,此時和爲s的次數就是把probabilities[0]中和爲s-1,s-2,s-3,s-4,s-5,s-6的次數加起來即可

   

int temp=0;

for(int j=1;j<=i && j<=g_maxValue;++j)

temp+=probabilities[flag][i-j];

probabilities[1-flag][i]=temp;

   

而第k次用k個骰子那麼要更新的結果範圍就是k到maxValue*k

   

所以連起來就是

   

for(int i=k;i<=g_maxValue*k;++i)

{

int temp=0;

for(int j=1;j<=i && j<=g_maxValue;++j)

temp+=probabilities[flag][i-j];

probabilities[1-flag][i]=temp;

}

   

然後就需要把probabilities[1]作爲中間值數組,這裏我們把flag賦值爲1-flag即可,是不是很神奇!

   

flag=1-flag;


解法一:基於遞歸求骰子的點數,時間效率不夠高

現在我們考慮如何統計每一個點數出現的次數。要向求出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 

基於這個思路實現代碼如下:

[java] view plain copy
  1. /** 
  2.  * n個骰子的點數 
  3.  */  
  4. package swordForOffer;  
  5.   
  6. /** 
  7.  * @author JInShuangQi 
  8.  * 
  9.  *         2015年8月11日 
  10.  */  
  11. public class E43DicsProbability {  
  12.     /* 
  13.      * 把n個骰子仍在地上,所有骰子朝上一面的點數之和爲s,輸入n,打印出s的所有可能出現的概率 
  14.      */  
  15.     public void printProbability(int number) {  
  16.         if (number < 1)  
  17.             return;  
  18.         int g_maxValue = 6;  
  19.         int[][] probabilities = new int[2][];  
  20.         probabilities[0] = new int[g_maxValue * number + 1];  
  21.         probabilities[1] = new int[g_maxValue * number + 1];  
  22.         int flag = 0;  
  23.         for (int i = 1; i <= g_maxValue; i++)  
  24.             probabilities[0][i] = 1;  
  25.         for (int k = 2; k <= number; ++k) {  
  26.             for (int i = 0; i < k; ++i)  
  27.                 probabilities[1 - flag][i] = 0;  
  28.             for (int i = k; i <= g_maxValue * k; ++i) {  
  29.                 probabilities[1 - flag][i] = 0;  
  30.                 for (int j = 1; j <= i && j <= g_maxValue; ++j)  
  31.                     probabilities[1 - flag][i] += probabilities[flag][i - j];  
  32.             }  
  33.             flag = 1 - flag;  
  34.         }  
  35.         double total = Math.pow(g_maxValue, number);  
  36.         for (int i = number; i <= g_maxValue * number; i++) {  
  37.             double ratio = (double) probabilities[flag][i] / total;  
  38.             System.out.println(i);  
  39.             System.out.println(ratio);  
  40.         }  
  41.     }  
  42. }  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章