把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
需要用一个浮点数数组返回答案,其中第i个元素代表这n个骰子所能掷出的点数集合中第i小的那个的概率。
输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
思路:动态规划
设输入n个骰子的解(即概率列表)为f(n),其中 “点数和”x的概率为f(n, x)
假设已知n - 1个骰子的解f(n - 1),此时添加一枚骰子,求n个骰子的点数和为x的概率f(n,x)。
当添加骰子的点数为1时,前n-1个骰子的点数和应为x - 1,方可组成点数和x;同理,当此骰子为2时,前n - 1个骰子应为x - 2;以此类推,直至此骰子点数为6。将这6种情况的概率相加,即可得到概率f(n, x)。
f(n,x) =
i = 1
∑ [ ( f(n−1,x−i) ) × (1/6) ]
6
以上递推公式虽然可行,但f(n - 1, x - i)中的x - i会有越界问题。
如下图所示,以上递推公式是“逆向”的,即为了计算f(n, x),将所有与之有关的情况求和;而倘若改换为“正向”的递推公式,便可解决越界问题。
将f(i)记为动态规划列表形式dp[i],则i = 1, 2, …, n的状态转移过程如下图所示:
通常做法是声明一个二维数组dp,dp[i][j]代表前i个骰子的点数和j的概率,并执行状态转移。而由于dp[i]仅由dp[i - 1]递推得出,为降低空间复杂度,只建立两个一维数组dp,tmp交替即可。
class Solution {
public:
vector<double> dicesProbability(int n){
vector<double> dp(6, 1.0 / 6.0);
for (int i = 2; i < = n; i++)
{
vector<double> tmp(5 * i + 1, 0);
for (int j = 0; j < dp.size(); j++)
{
for (int k = 0; k < 6; k++)
{
tmp[j + k] += dp[j] / 6.0;
}
}
dp = tmp;
}
return dp;
}
};
如上图所示,在计算j = 1时,由于tmp第一个计算的就是tmp[1],后面新增加了一个tmp[6],于是可以形成滑动数组的形式。
复杂度分析
-
时间复杂度:O(n
2
) : 状态转移循环 n - 1n−1 轮;每轮中,当 i = 2, 3, …, ni=2,3,…,n 时,对应循环数量分别为 6 \times 6, 11 \times 6, …, [5(n - 1) + 1] \times 66×6,11×6,…,[5(n−1)+1]×6 ;因此总体复杂度为 O((n - 1) \times \frac{6 + [5(n - 1) + 1]}{2} \times 6)O((n−1)×
2
6+[5(n−1)+1]
×6) ,即等价于 O(n^2)O(n
2
) -
空间复杂度:状态转移过程中,辅助数组 tmp 最大长度为 6(n-1) - [(n-1) - 1] = 5n - 4,因此使用 O(5n - 4) = O(n)大小的额外空间。