【概率DP】
n个骰子所能掷出的点数之和的所有可能性的概率。
解:
- 方法:概率DP
- i个骰子,那么它所能掷出的点数之和为i~6i
- 掷i个骰子,掷出的点数之和为j,这是一种状态,记为(i,j)
- 它是从掷了(i-1)个骰子的状态转移来的
- 因为一个骰子的点数是1~6,所以掷了i个骰子,和为j(状态(i,j)),只可能是掷了(i-1)个骰子,和为(j-k)(k=1,…6)(即状态(i-1,j-k))这6个状态转移过来
- 所以选择:6个
- 因为一个骰子的点数是1~6,所以掷了i个骰子,和为j(状态(i,j)),只可能是掷了(i-1)个骰子,和为(j-k)(k=1,…6)(即状态(i-1,j-k))这6个状态转移过来
- 它是从掷了(i-1)个骰子的状态转移来的
- 有了状态,有了选择,写出状态转移方程:
- 设dp(i,j)为掷i个骰子,掷出的点数之和为j的概率
- 那么 d p ( i , j ) = ∑ k = 1 6 1 6 ⋅ d p ( i − 1 , j − k ) dp(i,j)= \sum_{k=1}^6\frac{1}{6}\cdot dp(i-1,j-k) dp(i,j)=∑k=1661⋅dp(i−1,j−k)
- base case: d p ( 1 , j ) = 1 6 dp(1, j) = \frac{1}{6} dp(1,j)=61
- 要求的是 d p ( n , j ) dp(n, j) dp(n,j) (j=n,…6n)
- 关于如何遍历:
- 很显然,对于第i个骰子,有效的和是
i~6i
,<i和>6i的概率必为0,所以只关注i~6i
即可
- 很显然,对于第i个骰子,有效的和是
- 其实也可以设dp(i,j)为掷i个骰子,掷出的点数之和为j的方法数
- 则对于状态(n,j),概率为dp(i,j) / pow(6, n)
- pow(6, n)为掷n个骰子的状态空间大小,即全部的方法数
- 则对于状态(n,j),概率为dp(i,j) / pow(6, n)
- 设dp(i,j)为掷i个骰子,掷出的点数之和为j的概率
- 掷i个骰子,掷出的点数之和为j,这是一种状态,记为(i,j)
- i个骰子,那么它所能掷出的点数之和为i~6i
// dp[i][j]:掷i个骰子,掷出的点数之和为j的概率
vector<double> dicesProbability(int n) {
vector<vector<double>> dp(n+1, vector<double>(6*n+1, 0.0)); //下标偏移1位
for(int j = 1; j<= 6;j++) dp[1][j] = 1.0/6;
for(int i = 2; i<= n; i++){
for(int j = i; j <= 6*i; j++){
for(int k = 1; k <= 6; k++){
if(j-k <= 0) break; //if(j-k <= 0 || j-k<i-1) break; 更严谨
dp[i][j] += 1.0/6*dp[i-1][j-k];
}
}
}
vector<double> res;
for(int j = n; j<= 6*n; j++) res.push_back(dp[n][j]);
return res;
}
//dp[i,j]:掷i个骰子,掷出的点数之和为j的方法数
vector<double> dicesProbability(int n) {
vector<vector<double>> dp(n+1, vector<double>(6*n+1, 0.0)); //下标偏移1位
for(int j = 1; j<= 6;j++) dp[1][j] = 1.0;
for(int i = 2; i<= n; i++){
for(int j = i; j <= 6*i; j++){
for(int k = 1; k <= 6; k++){
if(j-k <= 0) break; //if(j-k <= 0 || j-k<i-1) break; 更严谨
dp[i][j] += dp[i-1][j-k];
}
}
}
vector<double> res;
double cnt = pow(6.0, n); //n=6时状态空间总数
for(int j = n; j<= 6*n; j++) res.push_back(dp[n][j]/cnt);
return res;
}
优化:空间压缩,降维
vector<double> dicesProbability(int n) {
vector<double> dp(6*n+1, 0.0); //下标偏移1位
for(int j = 1; j<= 6;j++) dp[j] = 1.0/6;
for(int i = 2; i<= n; i++){
for(int j = 6*i; j >=i; j--){ //通过递推式(状态转移方程)得出要逆序遍历
dp[j] = 0.0;
for(int k = 1; k <= 6; k++){
if(j-k <= 0 || j-k<i-1) break; //必须大于(i-1)状态的对角线(即大于i-1)才行。因为状态压缩后,小于i-1的格子dp[k](k<i-1)里存的是状态(i-2)及之前的情况(遍历不到了所以没有更新),而状态未压缩时,dp[i-1][k](k<i-1)存的就是0,计算结果也符合真实情况
dp[j] += 1.0/6*dp[j-k];
}
}
}
vector<double> res;
for(int j = n; j<= 6*n; j++) res.push_back(dp[j]);
return res;
}