1155. 掷骰子的N种方法

我们的目标是掷骰子n次,获得target的总和的数量
这个问题是一个典型的 k 阶树,高度为n,即所有的解法为k^n种,但是明显的是,很多种解法是不必要的,因此第一个思路就是回溯剪枝,但是这里考虑使用dp的办法:

定义dp[i][j]为第i次投掷时,获得和为j的数量
遍历骰子可以投掷的范围[1..k],第i次投掷得到骰子结果为k

  • j - k > 0时,此时,意味着前 i-1次投掷得到的和j小于k,为j - k,因此应该找的是前面i-1次投掷中和为j-k的所有情况:dp[i][j] = dp[i-1][j - k]+...+dp[0][j-k]
  • j - k = 0时,显然:dp[i][j] = 1
  • j - k < 0时,显然此次不用计算,联合第一步考虑,就是只需要考虑到大小小于k的即可

初始化排除第二种情况
这道题其实也是分组背包
看三叶姐的题解

  1. /**
  2. * 分组背包问题:掷骰子的N种方法
  3. */
  4. private static int MOD = (int)1e9+7;
  5. public int numRollsToTarget1(int n, int m, int target) {
  6. int[][] dp = new int[n + 1][target + 1];
  7. dp[0][0] = 1;// 考虑dp[1][1]的上一种情况就知道为啥了
  8. // 枚举物品组:骰子
  9. for (int i = 1; i <= n; i++) {
  10. // 枚举背包容量:总点数
  11. for (int j = 0; j <= target; j++) {
  12. // 枚举决策:当前骰子的点数
  13. for (int k = 1; k <= m; k++) {
  14. if (j - k >= 0)
  15. dp[i][j] = (dp[i-1][j-k] + dp[i][j]) % MOD;
  16. }
  17. }
  18. }
  19. return dp[n][target];
  20. }
  21. // 优化:
  22. public int numRollsToTarget(int n, int m, int target) {
  23. int[] dp = new int[target + 1];
  24. dp[0] = 1;
  25. // 枚举物品组:骰子
  26. for (int i = 1; i <= n; i++) {
  27. // 枚举背包容量:总点数.注意要反过来,因为是要用到j-k的值
  28. for (int j = target; j >= 0; j--) {
  29. dp[j] = 0; // 手动初始化为0,注意!!!
  30. // 枚举决策:当前骰子的点数
  31. for (int k = 1; k <= m; k++) {
  32. if (j - k >= 0)
  33. dp[j] = (dp[j-k] + dp[j]) % MOD;
  34. }
  35. }
  36. }
  37. return dp[target];
  38. }

上面是看为一个相同的骰子置m次的情况,因此看为一个树形的问题。但是上面的推导不是很清晰,看一下三叶姐的:
image.png
其实我可以学这种推导的方式,直接考虑f[i][j]然后查看上一次的情况,根据价值来判断。写出来就清晰很多:

  1. impl Solution {
  2. pub fn num_rolls_to_target(n: i32, k: i32, target: i32) -> i32 {
  3. let (n, m, t) = (n as usize, k as usize, target as usize);
  4. let mut dp = vec![0; t + 1];
  5. dp[0] = 1;
  6. (1..=n).for_each(|i| {
  7. (0..=t).rev().for_each(|j| {
  8. dp[j] = 0;
  9. // 骰子的范围
  10. (1..=m).for_each(|k| {
  11. if j >= k {
  12. dp[j] = (dp[j - k] + dp[j]) % (1e9 as i32 + 7);
  13. }
  14. });
  15. });
  16. });
  17. dp[t]
  18. }
  19. }