題目:把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即可
優化方法
我們需要將中間值存起來以減少遞歸過程中的重複計算問題,可以考慮我們用兩個數組AB,A在B之上得到,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
基於這個思路實現代碼如下: