把n個骰子扔在地上,所有骰子朝上一面的點數之和為s。輸入n,列印出s的所有可能的值出現的機率。
你需要用一個浮點數陣列返回答案,其中第 i 個元素代表這 n 個骰子所能擲出的點數集合中第 i 小的那個的機率。
示例 1:
輸入: 1
輸出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
示例 2:
輸入: 2
輸出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
限制:
1 <= n <= 11
方法一:動態規劃
文章可能沒有很清晰的講述程式碼中的一些細節,但這些細節大多數都可以在評論中找到,我都進行了詳細的解釋,希望對大家有幫助。
解題思路
題目需要我們求出所有點數出現的機率,根據機率出現的計算公式,點數 k 出現機率計算公式為:
P(k)=k 出現次數 / 總次數
投擲 n 個骰子,所有點數出現的總次數是 6^n
,因為一共有 n 枚骰子,每枚骰子的點數都有 6 種可能出現的情況。我們的目的就是 計算出投擲完 n 枚骰子後每個點數出現的次數。
使用遞迴造成重複計算的問題
單純使用遞迴搜尋解空間的時間複雜度為 6^n
,會造成超時錯誤,因為存在重複子結構。解釋如下:
我們使用遞迴函式 getCount(n,k) 來表示投擲 n 枚骰子,點數 k 出現的次數。
為了簡化分析,我們以投擲 2 枚骰子為例。
我們來模擬計算點數 4 和點數 6,這兩種點數各自出現的次數。也就是計算 getCOunt(2,4) 和 getCount(2,6)。
他們的計算公式為:
getCount(2,4) = getCount(1,1) + getCount(1,2) + getCount(1,3)
getCount(2,6) = getCount(1,1) + getCount(1,2) + getCount(1,3) + getCount(1,4) + getCount(1,5)
我們發現遞迴統計這兩種點數的出現次數時,重複計算了 getCount(1,1) + getCount(1,2) + getCount(1,3)
這些子結構,計算其它點數的次數時同樣存在大量的重複計算。
動態規劃
使用動態規劃解決問題一般分為三步:
- 表示狀態
- 找出狀態轉移方程
- 邊界處理
下面我們一步一步分析,相信你一定會有收穫!
表示狀態
分析問題的狀態時,不要分析整體,只分析最後一個階段即可!因為動態規劃問題都是劃分為多個階段的,各個階段的狀態表示都是一樣,而我們的最終答案就是在最後一個階段。
對於這道題,最後一個階段是什麼呢?
透過題目我們知道一共投擲 n 枚骰子,那最後一個階段很顯然就是:當投擲完 n 枚骰子後,各個點數出現的次數。
注意:這裡的點數指的是前 n 枚骰子的點數和,而不是第 n 枚骰子的點數,下文同理。
找出了最後一個階段,那狀態表示就簡單了。
- 首先用陣列的第一維來表示階段,也就是投擲完了幾枚骰子。
- 然後用第二維來表示投擲完這些骰子後,可能出現的點數。
- 陣列的值就表示,該階段各個點數出現的次數。
所以狀態表示就是這樣的 dp[i][j],表示投擲完 i 枚骰子後,點數 j 的出現次數。
找出狀態轉移方程
找狀態轉移方程也就是找各個階段之間的轉化關係,同樣我們還是隻需分析最後一個階段,分析它的狀態是如何做到的。
最後一個階段也就是投擲完 n 枚骰子後的這個階段,我們用 dp[n][j] 來表示最後一個階段點數 j 出現的次數。
單單看第 n 枚骰子,它的點數可能為 1,2,3,4,5,6,因為投擲完 n 枚骰子後點數 j 出現的次數,可以由投擲完 n-1 枚骰子後,對應點數 j-1,j-2,j-3,j-4,j-5,j-6 出現的次數轉化過來。
for (第n枚骰子的點數 i = 1; i <= 6; i ++) {
dp[n][j] += dp[n-1][j - i]
}
寫成數學公式是這樣的:
6
dp[n][j]= ∑ dp[n−1][j−i]
i=1
n 表示階段,j 表示投擲完 n 枚骰子後的點數和,i 表示第 n 枚骰子會出現的六個點數。
邊界處理
這裡的邊界處理很簡單,只要我們把可以直接知道的狀態初始化就好了。
我們可以直接指導的狀態是啥,就是第一階段的狀態:投擲完 1 枚骰子後,它的可能點數分別為 1,2,3,4,5,6,並且每個點數出現的次數都是 1.
for (int i = 1; i <= 6; i ++) {
dp[1][i] = 1;
}
程式碼
class Solution {
public:
vector<double> twoSum(int n) {
int dp[15][70];
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= 6; i ++) {
dp[1][i] = 1;
}
for (int i = 2; i <= n; i ++) {
for (int j = i; j <= 6*i; j ++) {
for (int cur = 1; cur <= 6; cur ++) {
if (j - cur <= 0) {
break;
}
dp[i][j] += dp[i-1][j-cur];
}
}
}
int all = pow(6, n);
vector<double> ret;
for (int i = n; i <= 6 * n; i ++) {
ret.push_back(dp[n][i] * 1.0 / all);
}
return ret;
}
};
空間最佳化
我們知道,每個階段的狀態都只和它前一階段的狀態最佳化,因此我們不需要額外的一維來儲存所有階段。
用一維陣列來儲存一個階段的狀態,然後對下一階段可能出現的點數 j 從大到小遍歷,實現一個階段一個階段的轉換。
最佳化程式碼如下:
class Solution {
public:
vector<double> twoSum(int n) {
int dp[70];
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= 6; i ++) {
dp[i] = 1;
}
for (int i = 2; i <= n; i ++) {
for (int j = 6*i; j >= i; j --) {
dp[j] = 0;
for (int cur = 1; cur <= 6; cur ++) {
if (j - cur < i-1) {
break;
}
dp[j] += dp[j-cur];
}
}
}
int all = pow(6, n);
vector<double> ret;
for (int i = n; i <= 6 * n; i ++) {
ret.push_back(dp[i] * 1.0 / all);
}
return ret;
}
};
個人理解
- 感覺用遞迴的題都可以用動態規劃來實現。
- 使用動態規劃的話,就得實現動態規劃的三步:狀態,狀態轉移方程,邊界值。
- 當我們確定了上述三步之後,其實我們已經解決了這個題。
- 各個陣列迴圈起始、陣列大小一定要搞清楚。
附上一個我寫的 java 版本的程式碼
class Solution {
public double[] twoSum(int n) {
double[] result = new double[5*n+1];
int total = (int) Math.pow(6,n);
int[][] dp = new int[n+1][6*n+1];
for (int i = 1 ; i <= 6; i++) {
dp[1][i] = 1;
}
for (int i = 2; i <= n; i++) {
for (int j = i; j <= 6*i; j++) {
for (int current = 1; current <= 6; current ++) {
if (j <= current) {
break;
}
dp[i][j] += dp[i-1][j-current];
}
}
}
for (int i = n; i <= 6*n; i++) {
result[i-n] = (double) dp[n][i] / total;
}
return result;
}
}
題解來源
作者:huwt
連結:leetcode-cn.com/problems/nge-tou-z...
來源:力扣(LeetCode)
來源:力扣(LeetCode)
連結:leetcode-cn.com/problems/nge-tou-z...
本作品採用《CC 協議》,轉載必須註明作者和本文連結