前言
通过n个骰子不同点数和的不同概率来深刻理解动态规划问题,做到举一反三。
一、例题
1.1、题目
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。 你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
1.2、示例
示例 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
二、题解
对于一个问题的分析,抓住核心逻辑是关键,而对于动态规划问题,找到其中的求解规律即可。
1)挖掘题目数据 A) n个骰子各种和的概率 = 一种和出现的次数 / 所有和出现的总次数 B) 每个骰子有6种可能,那么组合在一起就是n个6相乘,即所有和出现的总次数 = 6n; C) 易知,和的范围为n – 6n,最小全为1,最大全为6。 D) 代码体现
double[] res = new double[5 * n + 1];
double total = Math.pow(6, n);
2)找题目规律 求n个骰子落下,各和概率,那么让n取最小,一步一步来,即n = 1,可以容易得出各和出现的次数如下表,
sum = 1 | sum = 2 | sum = 3 | sum = 4 | sum = 5 | sum = 6 |
---|
1 | 1 | 1 | 1 | 1 | 1 |
n = 2时,它与n = 1之间有什么联系呐?n = 2比n = 1多了一个骰子,多出的这一个骰子就可能出现1 – 6。所以当和固定为s,则s出现的情况为,
后一个骰子点数 | 前面所有骰子点数和 |
---|
1 | s-1 | 2 | s-2 | 3 | s-3 | 4 | s-4 | 5 | s-5 | 6 | s-6 |
综述,设骰子个数为n,和为s出现的次数为f(n,s), 代码体现为,
for (int i = 1; i <= n; i++) {
for (int j = i * 6; j >= i; j--) {
for (int k = 1; k <= 6; k++) {
dp[i][j] += j - k >= i - 1 ? dp[i - 1][j - k] : 0;
A) for (int k = 1; k <= 6; k++) {dp[i][j] += j - k >= i - 1 ? dp[i - 1][j - k] : 0;} 体现上式中的递推公式; B) i 代表骰子个数,j 代表骰子点数和,j >= i 体现上式中的条件一:s >= n; C) k 代表最后一个骰子所摇出的点数,j - k >= i - 1体现上式中的条件二:s - i >= n-1; 注:接下来看完整解答
2.1、二维数组
public double[] dicesProbability(int n) {
double[] res = new double[5 * n + 1];
double total = Math.pow(6, n);
int[][] dp = new int[n + 1][6 * n + 1];
dp[1][1] = dp[1][2] = dp[1][3] = dp[1][4] = dp[1][5] = dp[1][6] = 1;
for (int i = 1; i <= n; i++) {
for (int j = i * 6; j >= i; j--) {
for (int k = 1; k <= 6; k++) {
dp[i][j] += j - k >= i - 1 ? dp[i - 1][j - k] : 0;
}
if (i == n)
res[j - n] = dp[i][j] / total;
}
}
return res;
}
2.2、一维数组
可以发现更新骰子数为n时,只用到了n-1的点数和的个数,所以可以用一个一维数组存n-1的点数和的个数。
一处创新和两处注意事项 A)改变初始状态,把初始状态设为n = 0,即摇出的点数和s = 0的次数为1,其它都为0。那么n = 1时,点数和为1 – 6的情况为1+0 = 1,2+0 = 2,…,6+0 = 6,即6种和情况出现次数都为1,回到上面二维dp的初始化情况。 B)for (int j = i * 6; j >= i; j--) 必须从后往前更新,因为后面的值更新需要用到前面的值,防止值覆盖。 C)对于单个一维数组从后往前更新时,在刚开始更新之前需要初始化当前值为0,避免加上“脏值”。
public double[] dicesProbability(int n) {
double[] res = new double[5 * n + 1];
double total = Math.pow(6, n);
int[] dp = new int[6 * n + 1];
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 6 * i; j >= i; j--) {
dp[j] = 0;
for (int k = 1; k <= 6; k++) {
dp[j] += j - k >= i - 1 ? dp[j - k] : 0;
}
if (i == n)
res[j - n] = dp[j] / total;
}
}
return res;
}
2.3、直接上概率
有了上面的知识沉淀,那么用概率来解题也就是换个马甲。
public double[] dicesProbability3(int n) {
double total = Math.pow(6, n);
double[] dp = new double[6 * n + 1];
dp[0] = 1.0;
for (int i = 1; i <= n; i++) {
for (int j = 6 * i; j >= i; j--) {
dp[j] = 0;
for (int k = 1; k <= 6; k++) {
dp[j] += j - k >= i - 1 ? dp[j - k] * 1 / 6d : 0;
}
}
}
return Arrays.copyOfRange(dp, n, 6 * n + 1);
}
总结
1)动态规划,分析出问题中的循序渐进的规律。 2)动态规划三关键:初始状态、状态更新方程(递推公式)、最终结果。
参考文献
[1] Leetcode 原题 [2] Leetcode用户,算法岗从零到无穷,对该题的评论
|