题意
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出 s的所有可能的值 出现的概率。你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
解法1—暴力法
给定 n 个骰子,可得:
-
每个骰子摇到 1 至 6 的概率相等,都为 。
-
将每个骰子的点数看作独立情况,共有 种「点数组合」。例如 n = 2时的点数组合为:
-
(1,1),(1,2),⋯,(1,6),
-
(2,1),(2,2),⋯,(2,6),
-
...........
-
(6,1),(6,2),⋯,(6,6)。
-
-
n 个骰子「点数和」的范围为[n,6n] ,数量为 6n−n+1=5n+1 种,因此返回的结果中就要有 5*n+1项。
暴力统计: 每个「点数组合」都对应一个「点数和」,考虑遍历所有点数组合,统计每个点数和的出现次数,最后除以点数组合的总数(即除以 ),即可得到每个点数和的出现概率。
如下图所示,为输入 n = 2 时,点数组合、点数和、各点数概率的计算过程。
暴力法需要遍历所有点数组合,因此时间复杂度为 O() ,复杂度太高。
法2—动态规划
设输入 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), 递推公式如下所示:
为什么要乘一个? 因为多了一个骰子后,每个骰子的点数组合就会变多,n个骰子有种组合情况。
根据以上分析,得知通过子问题的解 f(n - 1)可递推计算出 f(n) ,而输入一个骰子的解 f(1) 已知,因此可通过解 f(1) 依次递推出任意解 f(n) 。
如下图所示,为 n = 2,x=7 的递推计算示例。
观察发现,以上递推公式虽然可行,但 f(n - 1, x - i) 中的 x - i 会有越界问题。例如,若希望递推计算 f(2, 2) ,由于一个骰子的点数和范围为 [1, 6] ,因此只应求和 f(1, 1) ,因为只有上一个骰子的点数为1时,且当前骰子也为1时,两个骰子才能组成点数和为2。如果上一个骰子的点数超过1了,两个骰子点数和将会超过2。
即 f(1, 0) , f(1, -1) , ... , f(1, -4) 皆无意义。此越界问题导致代码编写的难度提升。
如下图所示,以上递推公式是 “逆向” 的,即为了计算 f(n, x),将所有与之有关的情况求和;而倘若改换为 “正向” 的递推公式,便可解决越界问题。
具体来看,由于新增骰子的点数只可能为 1 至 6,
因此概率 f(n - 1, x)仅与 f(n, x + 1) , f(n, x + 2), ... , f(n, x + 6)相关。即 (n-1)个骰子组成的点数和为x,那么新增的骰子点数只能为:1~6。然后 (n-1)个骰子与这新加的一个骰子组成的点数和范围为:x+1~x+6。
因而,遍历 f(n - 1) 中各点数和的概率,并将其相加至 f(n) 中所有相关项,即可完成 f(n - 1) 至 f(n) 的递推。
正向递推就不会产生越界问题。
具体实现:
通常做法是声明一个二维数组 dp ,dp[i][j] 代表前 i 个骰子的点数和 j 的概率,并执行状态转移。而由于 dp[i] 仅由 dp[i-1] 递推得出,为降低空间复杂度,只建立两个一维数组 dp 和 tmp 交替前进即可。
C++实现(正推)
class Solution
{
public:
vector<double> dicesProbability(int n)
{
vector<double> res(6,1.0/6.0); //当筛子只有一个的时候,每个点数的概率都是一样的,都是 1/6
for(int i=2;i<=n;i++)
{
// i表示当前是共有 i 个筛子
// 第i个筛子的点数和的范围为:[i,6*i],
//共有(6i-i+1)=5i+1 个数量,每个数都有对应的概率,因此 i个筛子共要开5*i+1个空间
vector<double> tmp(5*i+1,0);
for(int j=0;j<res.size();j++)
{
for(int k=0;k<6;k++)
{
// 上(i-1)个筛子的每个点数和x 对应 i个筛子 的 x+1..x+6点数和
tmp[j+k]+=(res[j]/6.0);
}
}
res=tmp; //交替前进,迭代
}
return res;
}
};
C++实现(逆推)
class Solution
{
public:
vector<double> dicesProbability(int n)
{
vector<double> res(n*5+1); //n个骰子组成的点数和范围:[n,6n],共5n+1种点数和
// dp数组
//一维长度为 n+1,下标范围为;[0,n],可以表示 1个骰子、2个骰子、...、n个骰子组成的点数和概率情况
//二维长度为 6n+1,下标范围:[0,6n],因为n个骰子的点数和最大值为 6n
vector<vector<double> > dp(n+1,vector<double>(6*n+1)) ;
//初始化第一个骰子的所有点数和的概率
for(int i=1;i<=6;i++)
{
dp[1][i]=1.0/6.0;
}
//然后分别计算第2个骰子、第三个骰子可以组成的点数和的概率值
for(int i=2;i<=n;++i)
{
for(int j=i;j<=6*i;++j) // i个骰子组成的点数和的范围:[i,6i]
{
for(int k=1;k<=6;++k) //第i个骰子的点数为k时候的情况
{
// 当(i-1)个骰子组成的点数和为 (j-k)时
// 加上第i个骰子的点数k,即可组成 i个骰子的点数和为 j 的情况。
if((j-k)>0) // 边界条件,如果j-k都≤0了,则没有意义
{
dp[i][j]+=dp[i-1][j-k]/6.0;
}
else
{
break;
}
}
}
}
for(int i=0;i<5*n+1;++i)
{
res[i]=dp[n][n+i];
}
return res;
}
};