目录
首先来看一道经典的问题:n个骰子的点数
我们再来看另一个问题:掷骰子的N种方法
牢记投骰子类问题的解决方法:动态规划
首先来看一道经典的问题:n个骰子的点数
题目是这样的:把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
示例 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]
预备知识
给定 n 个骰子,可得:
每个骰子摇到 1 至 6 的概率相等,都为 。
将每个骰子的点数看作独立情况,共有 种「点数组合」。例如 n = 2 时的点数组合为:
(1,1), (1,2), ... , (2, 1), (2, 2), ... , (6,1), ... , (6, 6)
- n 个骰子「点数和」的范围为 [n, 6n],数量为 5n+1 种。
设输入 n 个骰子的解(即概率列表)为 f(n) ,其中「点数和」 x 的概率为 f(n, x)。
当添加骰子的点数为 1 时,前 n - 1 个骰子的点数和应为 x - 1,方可组成点数和 x ;同理,当此骰子为 2 时,前 n - 1 个骰子应为 x - 2 ;以此类推,直至此骰子点数为 6 。将这 6 种情况的概率相加,即可得到概率 f(n, x)。递推公式如下所示:
根据以上分析,得知通过子问题的解 f(n - 1)可递推计算出 f(n),而输入一个骰子的解 f(1) 已知,因此可通过解 f(1) 依次递推出任意解 f(n) 。
观察发现,以上递推公式虽然可行,但 f(n - 1, x - i) 中的 x - i 会有越界问题。例如,若希望递推计算 f(2, 2) ,由于一个骰子的点数和范围为 [1, 6] ,因此只应求和 f(1, 1) ,即 f(1, 0) , f(1, -1) , ... , f(1, -4) 皆无意义。此越界问题导致代码编写的难度提升。
所以为了计算 f(n, x) ,将所有与之有关的情况求和;而倘若改换为 “正向” 的递推公式,便可解决越界问题。
逆向递推(有越界问题):为求f(n,x),将f(n-1)中所有与其有关的概率项求和。
正向递推(无越界问题):遍历f(n-1),统计每项f(n-1,i)对概率f(n,i+1),f(n,i+2),...,f(n,i+6)产生的贡献。
具体来看,由于新增骰子的点数只可能为 1 至 6 ,因此概率 f(n - 1, x) 仅与 f(n, x + 1) , f(n, x + 2), ... , f(n, 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 交替前进即可。
我们再来看另一个问题:掷骰子的N种方法
题目:这里有 d
个一样的骰子,每个骰子上都有 f
个面,分别标号为 1, 2, ..., f
。
我们约定:掷骰子的得到总点数为各骰子面朝上的数字的总和。
如果需要掷出的总点数为 target
,请你计算出有多少种不同的组合情况(所有的组合情况总共有 f^d
种),模 10^9 + 7
后返回。
示例 1:
输入:d = 1, f = 6, target = 3 输出:1
示例 2:
输入:d = 2, f = 6, target = 7 输出:6
示例 3:
输入:d = 2, f = 5, target = 10 输出:1
这道题的处理方法其实和上一道题一样,只是做了稍微的变通。
首先我们定义 dp[i][j] 代表投掷 i 个骰子和为 j。而dp[i][j] 和 dp[i-1] 的关系是,当第 i 次我投的点数为k(1<= k <= f),那么前 i-1次和为 j-k,对应的是 dp[i-1][j-k]。
那么最终就有 dp[i][j] = dp[i-1][j-1] + dp[i-1][j-2] + ... + dp[i-1][j-f];