377. 组合总和 Ⅳ
考点
- 线性DP
- DP计算排列问题
思路
一、问题概述
给定一个由 正整数 组成的数组 nums
和一个目标值 target,要求计算:
使用
nums中的元素(每个元素可重复使用),凑出和为target的方案数, 不同顺序视为不同方案。
例如:
nums = [1, 2]target = 3
合法方案包括:
1 + 1 + 11 + 22 + 1
因此答案为 3。
二、状态定义(State Definition)
定义一维动态规划数组: \[ f[s] = \text{凑成总和为 } s \text{ 的方案数} \] 该定义具有以下特点:
- 状态只与“当前目标和”有关
- 不区分使用了哪些元素,而是从顺序角度计数
- 非常适合“顺序不同算不同方案”的 排列(permutation)问题
三、初始值(Initialization)
\[ f[0] = 1 \]
含义说明
- 凑出和为
0的方式只有一种:什么都不选 - 这是一个计数 DP 中的关键基准状态
- 它保证了后续状态转移可以正确累加
如果没有这个初始条件,所有状态都将无法被推导出来。
四、状态转移方程(Transition)
对于任意 \(s \in [1, \text{target}]\),有: \[ f[s] = \sum_{x \in nums,\; s \ge x} f[s - x] \]
转移逻辑解释
- 将“最后一步选了哪个数”作为划分方式
- 若最后一步选择了
x,那么前面的部分必须凑出s - x f[s - x]的每一种方案,在末尾接上x,都会形成一个新的合法方案
为什么这是“排列”而不是“组合”?
- 外层枚举的是 目标和
s - 内层枚举的是 最后一步选择的元素
x - 不同的“最后一步”顺序会被完整区分
五、循环结构与顺序
对应的循环结构为:
1 | for s = 1 .. target: |
该顺序是本题的关键:
- 外层是和,内层是元素
- 保证了
[1,2]与[2,1]会被分别统计 - 若交换循环顺序,则会退化为“组合问题”,结果错误
六、整数溢出问题(非常重要)
1. 题目中的隐含条件
题目说明:
最终答案保证在 32 位有符号整数范围内
但需要注意:
并未保证中间 DP 状态不会溢出
在状态转移过程中,f[s]
可能会暂时变得非常大,随后在更大的 s
上被“抵消”或不再参与最终答案。
2. 有符号整数的风险
在 C++ 中:
int、long long的溢出行为是 Undefined Behavior- 一旦溢出,程序的行为将不再受语言标准保证
- 编译器可能基于“永不溢出”的假设进行激进优化,导致结果错误
3. 为什么使用
unsigned 是安全的?
unsigned int的溢出行为是定义良好的- 所有运算都在模 \(2^{32}\) 意义下进行
- 对于本题:
- 中间状态即使溢出,也只是在取模意义下循环
- 最终结果仍然能正确落在
int范围内
因此,使用 unsigned
是一种工程上安全、符合题目语义的选择。
七、时间与空间复杂度
时间复杂度: \[ O(\text{target} \times |nums|) \]
空间复杂度: \[ O(\text{target}) \]
八、完整参考代码(已 AC)
1 | class Solution { |