LeetCode 1155. 掷骰子等于目标和的方法数
题目描述


题意分析
有
n个骰子,每个骰子的点数都在1..k,每个骰子恰好掷一次,统计总点数恰好等于target的方法数,答案对10^9 + 7取模。不同骰子可以区分,给各个骰子分配的点数不同就算不同方案,不能只把点数集合相同的结果合并。必须恰好使用
n个骰子,既不能少掷,也不能让某个骰子取零点。目标小于n或大于n × k时自然无解。
解法:滚动数组动态规划
核心思路
[!blue]
按已经掷过的骰子数量分层。设
ways[dice][sum]表示前dice个骰子掷出总和sum的方案数。最后一个骰子的点数若为face,前面的骰子就必须凑出sum - face,因此把旧层所有合法ways[dice - 1][sum - face]相加即可。按最后一个骰子的点数分类,不同
face的方案互不重叠,每种完整结果又都有唯一的最后点数,所以转移不重不漏。每种点数只能在1..k内,并且不能超过当前总和,避免读取负下标。初始时零个骰子凑出零有一种方法,即什么都不掷,因此
dp[0] = 1;零个骰子凑其他和则为零。这个空方案是第一颗骰子产生计数的起点,不代表以后仍可用掷过的骰子凑零。每一层只依赖上一层,可以用
dp保存旧层、next保存新层。每次新建的next从零开始,尤其next[0]必须保持零,因为已经掷过骰子后总和至少为一。整层计算完再用next替换dp,防止同一个骰子通过读取本轮结果被重复使用。计数增长很快,需要在转移中取模。Java 每个总和用
long汇总当前至多k个模内计数后取模;Go 在每次累加后取模,使累加值保持在安全范围内。处理完恰好n层后,目标位置就是答案。
解题步骤
- 创建长度为
target + 1的数组dp,只有dp[0] = 1。- 对每个骰子创建新的全零数组
next。- 枚举目标总和
sum = 1..target,再枚举face = 1..min(k, sum)。- 将旧层的
dp[sum - face]累加为新层该总和的计数,按代码方式及时取模。- 全部总和计算完后令
dp = next,继续下一颗骰子。- 完成
n层后返回dp[target],不可达状态会一直保持为零。
代码实现
class Solution {
public int numRollsToTarget(int n, int k, int target) {
int mod = 1_000_000_007;
int[] dp = new int[target + 1];
// 零个骰子凑零有一种空方案,为后续计数提供起点。
dp[0] = 1;
for (int dice = 1; dice <= n; dice++) {
// 新一层只读取旧骰子数量的状态,不能复用本轮结果。
int[] next = new int[target + 1];
for (int sum = 1; sum <= target; sum++) {
// 多个模内计数汇总时使用宽整数,再统一取模。
long ways = 0;
for (int face = 1; face <= k && face <= sum; face++) {
ways += dp[sum - face];
}
next[sum] = (int) (ways % mod);
}
dp = next;
}
return dp[target];
}
}
func numRollsToTarget(n int, k int, target int) int {
const mod = 1000000007
dp := make([]int, target+1)
// 零个骰子凑零有一种空方案,为后续计数提供起点。
dp[0] = 1
for dice := 1; dice <= n; dice++ {
// 新一层只读取旧骰子数量的状态,不能复用本轮结果。
next := make([]int, target+1)
for sum := 1; sum <= target; sum++ {
ways := 0
for face := 1; face <= k && face <= sum; face++ {
// 每项累加后取模,保持中间计数在安全范围内。
ways = (ways + dp[sum-face]) % mod
}
next[sum] = ways
}
dp = next
}
return dp[target]
}
复杂度分析
- 时间复杂度:
O(nkT),其中T = target。每一颗骰子更新至多T个总和,每个总和枚举至多k个点数。- 空间复杂度:
O(T + 1)。任意时刻只保留新旧两层数组,不需要保存全部骰子层。
关键点总结
[!green]
- 骰子数量必须作为层次,才能表达“恰好掷
n次”。- 按最后点数分类产生有序结果计数,交换不同骰子的点数通常对应另一方案。
- 空方案只存在于零骰子的初始层,后续零和应不可达。
- 分开新旧层避免本轮骰子被重复使用。
易错点总结
[!yellow]
- 初始
dp[0]为零:第一颗骰子没有计数来源,全部结果都会保持零。- 新层沿用旧层数值:相当于允许少掷骰子,必须从新的全零数组开始。
- 新层零和仍设为一:会允许已经使用的骰子取零点,不符合点数范围。
- 原地正序累加状态:可能读取本轮刚产生的计数,重复使用同一颗骰子。
- 点数枚举超过
sum:旧层下标会变成负数,需要同时受点数上限和当前总和限制。- 只在最终答案处取模:中间计数可能已经溢出,必须在转移期间控制数值。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 377. 组合总和 Ⅳ | 中等 | 同样计有序选择方案,本题额外固定恰好投掷n次,因此要记录已用骰子数。 |
| 1223. 掷骰子模拟 | 困难 | 同样计骰子结果序列,原题限制连续点数长度,本题限制点数总和。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!