题目来源
题目描述
class Solution {
public:
vector<double> dicesProbability(int n) {
}
};
题目解析
题目要求我们求出所有点数(即点数和)出现的概率,根据概率的计算公式,点数k出现的概率就是
P ( K ) = k 出 现 的 次 数 / 总 次 数 P_{(K)} = k出现的次数/总次数 P(K)=k出现的次数/总次数
又,给定n个骰子:
- 每枚骰子的点数都有 6 种可能出现的情况,每个骰子摇到1至6的概率相等,都为 1 / 6 1/6 1/6
- 将每个骰子的点数看作独立情况,共有
6
n
6^n
6n种[点数组合]。例如 n = 2n=2 时的点数组合为:
( 1 , 1 ) , ( 1 , 2 ) , ⋯ , ( 2 , 1 ) , ( 2 , 2 ) , ⋯ , ( 6 , 1 ) , ⋯ , ( 6 , 6 ) (1,1),(1,2),⋯,(2,1),(2,2),⋯,(6,1),⋯,(6,6) (1,1),(1,2),⋯,(2,1),(2,2),⋯,(6,1),⋯,(6,6) - n 个骰子「点数和」的范围为 [ n , 6 n ] [n, 6n] [n,6n],数量为 6 n − n + 1 = 5 n + 1 6n - n + 1 = 5n + 1 6n−n+1=5n+1种。
即,回答上面问题:
- 所有点数出现的总次数是 6 n 6^n 6n
所以下面的目标是计算出投掷完 n枚骰子后每个点数出现的次数
暴力
暴力统计: 每个「点数组合」都对应一个「点数和」,考虑遍历所有点数组合,统计每个点数和的出现次数,最后除以点数组合的总数(即除以 6 n 6^n 6n),即可得到每个点数和的出现概率。
如下图所示,为输入 n = 2时,点数组合、点数和、各点数概率的计算过程。
暴力的本质是递归。我们需要计算出n枚骰子每个点数出现的次数。可以使用递归函数 g e t C o u n t ( n , k ) getCount(n, k) getCount(n,k)来表示投掷 n枚骰子,点数 k 出现的次数。
为简化分析,我们以2枚骰子为例。
我们来模拟计算点数4和点数6,这两种点数各自出现的次数。也就是计算getCount(2, 4)和getCount(2, 6)
它们的计算公式为:
g
e
t
C
o
u
n
t
(
2
,
4
)
=
g
e
t
C
o
u
n
t
(
1
,
1
)
+
g
e
t
C
o
u
n
t
(
1
,
2
)
+
g
e
t
C
o
u
n
t
(
1
,
3
)
getCount(2,4)=getCount(1,1)+getCount(1,2)+getCount(1,3)
getCount(2,4)=getCount(1,1)+getCount(1,2)+getCount(1,3)
g
e
t
C
o
u
n
t
(
2
,
6
)
=
g
e
t
C
o
u
n
t
(
1
,
1
)
+
g
e
t
C
o
u
n
t
(
1
,
2
)
+
g
e
t
C
o
u
n
t
(
1
,
3
)
+
g
e
t
C
o
u
n
t
(
1
,
4
)
+
g
e
t
C
o
u
n
t
(
1
,
5
)
getCount(2,6)=getCount(1,1)+getCount(1,2)+getCount(1,3)+getCount(1,4)+getCount(1,5)
getCount(2,6)=getCount(1,1)+getCount(1,2)+getCount(1,3)+getCount(1,4)+getCount(1,5)
我们发现递归统计这两种点数的出现次数时,重复计算了
g e t C o u n t ( 1 , 1 ) , g e t C o u n t ( 1 , 2 ) , g e t C o u n t ( 1 , 3 ) getCount(1, 1) , getCount(1, 2) , getCount(1, 3) getCount(1,1),getCount(1,2),getCount(1,3)
这些子结构,计算其它点数的次数时同样存在大量的重复计算。
动态规划
(1)定义状态
- 分析问题的状态时,不要分析整体,只分析最后一个阶段即可!因为动态规划问题都是划分为多个阶段的,各个阶段的状态表示都是一样,而我们的最终答案在就是在最后一个阶段。
- 最后一步:
- 最后一个阶段是:当投掷完 n 枚骰子后,各个点数出现的次数。
- 注意,这里的点数指的是前 n 枚骰子的点数和,而不是第 n 枚骰子的点数,下文同理。
- 分析状态:
- 首先用数组的第一维表示阶段,也就是投掷完了几枚骰子。
- 然后用第二维来表示投掷完这些骰子后,可能出现的点数。
- 数组的值就表示,该阶段各个点数出现的次数。
- 定义状态: d p [ i ] [ j ] dp[i][j] dp[i][j]表示投掷完 i 枚骰子后,点数j的出现次数
(2)状态转移方程
- 找状态转移方程也就是找各个阶段之间的转化关系,同样我们还是只需分析最后一个阶段,分析它的状态是如何得到的。
- 最后一个阶段也就是投掷完 n 枚骰子后的这个阶段,我们用 dp[n][j]来表示最后一个阶段点数 j 出现的次数。
- 单单看第n枚骰子,它的点数可能为 1 、 2 、 . . . . . 6 1、2、.....6 1、2、.....6,因此投掷完n没骰子后点数j出现的次数,可以由投掷完 n-1枚骰子后,对应点数 j − 1 , j − 2 , j − 3 , . . . , j − 6 j-1, j-2, j-3, ... , j-6 j−1,j−2,j−3,...,j−6 出现的次数之和转化过来。
for (第n枚骰子的点数 i = 1; i <= 6; i ++) {
dp[n][j] += dp[n-1][j - i]
}
(3)边界处理
- 这里的边界处理很简单,只要我们把可以直接知道的状态初始化就好了。
- 我们可以直接知道的状态是啥,就是第一阶段的状态:投掷完 1 枚骰子后,它的可能点数分别为 1 , 2 , 3 , . . . , 6 1, 2, 3, ... , 6 1,2,3,...,6并且每个点数出现的次数都是 1
for (int i = 1; i <= 6; i ++) {
dp[1][i] = 1;
}
(4)计算顺序
class Solution {
public:
vector<double> dicesProbability(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) { // 前n枚骰子
for (int j = i; j < 6 * i; ++j) { //点数和
for (int curr = 1; curr <= 6; ++curr) { //j可能由j - 1, j - 2, ... j - 6投转换而来
if(j - curr <= 0){
break;
}
dp[i][j] += dp[i - 1][j - curr];
}
}
}
int all = pow(6, n);
std::vector<double > ret;
for (int i = n; i <= 6 * n; ++i) {
ret.emplace_back(dp[n][i] * 1.0 / all);
}
return ret;
}
};
(4)空间优化