组合总和 Ⅳ 图解题解
这道题到底在问什么
- 输入
- nums = [1,2,3], target = 6
- 输出
- 24
先想最直接的笨办法
记住一句:dp[t] 把「最后一步选谁」全枚举一遍,每个选择把对应 dp[t−num] 加进来。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 377 组合总和 Ⅳ 名为组合、实为排列计数:顺序不同算不同。一维 DP,dp[t]=凑出 t 的排列数,外层枚举总和、内层枚举末尾数,累加 dp[t−num]。时间 O(target×n)、空间 O(target)。
题目叫组合,为什么算的是排列
给数组 nums(无重复正整数)和目标 target,求和为 target 的方案数。名字叫「组合」,示例却拆穿它:nums = [1,2,3]、target = 6 时答案是 24,而 1+2 和 2+1 被算成两种。顺序不同即不同,本质是排列。
真正要数的是「有序的凑数序列」有多少条,摆错循环顺序就把排列数得成组合数。
为什么不能把所有序列枚举一遍
最笨的办法是把每条凑数序列都摆出来数,可分支数随 target 指数级膨胀,稍大就数不完。慢在重复:凑出总和 5 的序列,算总和 6、7 时又被反复重走。既然「凑出总和 t 的排列数」能独立回答,存下来复用即可。
dp 的下标是总和,值是排列数
定义 dp[t] 为「凑出总和 t 的排列数」。下标 t 是总和值不是位置,长度 target+1。
起点 dp[0] = 1,凑出总和 0 只有一种方案——一个数都不选的空序列。它是所有递推的源头,若误设成 0,整张表全归零,答案变 0。
转移为什么按最后一步分类相加
凑出 t 的每条排列,最后一个数总是确定的,就按这个末尾数分堆。末尾是 num 的那堆,把末尾去掉,剩下的正好是一条凑出 t−num 的排列,两者一一对应,所以这堆恰好有 dp[t−num] 条。对每个 num ≤ t 各分一堆,堆间不重、合起来不漏,于是 dp[t] = 各个 dp[t−num] 相加。
命门在循环顺序。这里外层遍历总和 t、内层遍历 nums,对每个 t 都把「末尾可以是任意一个 num」枚举一遍,同一个数在不同位置被反复计入,数出的是排列。LeetCode 518 零钱兑换 II 求组合数,循环正好反过来——外层遍历硬币、内层遍历金额,每种硬币只按固定次序出现,顺序被抹掉,得到组合。所以循环顺序不是细节:外层放谁,数出来就是谁。
手工演算:拿 nums=[1,2,3] 凑到 6
先铺 dp[0] = 1,往后每个 t 把 num ≤ t 的 dp[t−num] 加起来。
dp[1] = dp[0] = 1。dp[2] = dp[1] + dp[0] = 2。dp[3] = dp[2] + dp[1] + dp[0] = 2 + 1 + 1 = 4。dp[4] = dp[3] + dp[2] + dp[1] = 4 + 2 + 1 = 7。dp[5] = dp[4] + dp[3] + dp[2] = 7 + 4 + 2 = 13。dp[6] = dp[5] + dp[4] + dp[3] = 13 + 7 + 4 = 24。
dp[6] = 24,即凑出 6 的全部排列数,和示例输出一致。
复杂度怎么算,循环写反和溢出两个坑
外层 target 个总和、内层扫 n 个数,时间 O(target×n);只用一条 target+1 长的数组,空间 O(target)。
两个坑。一是循环顺序写反:外层遍历 nums、内层遍历 t,数出来是组合数而非排列数,会偏小。二是整数溢出:target 大时排列数会超出 32 位整数范围,C++/Java 要用 long 存 dp。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记住一句:dp[t] 把「最后一步选谁」全枚举一遍,每个选择把对应 dp[t−num] 加进来。
- 4dp[0]=1先填边界:凑出和 0,只有"一个数都不选"这一种 → dp[0]=1。
- 5凑和 1:最后一个数选 1,剩下要凑 0 → 把 dp[0]=1 加进来。
- 61 条来路全加完:dp[1] = 1。
- 7凑和 2:最后一个数选 1,剩下要凑 1 → 把 dp[1]=1 加进来。
- 8凑和 2:最后一个数选 2,剩下要凑 0 → 把 dp[0]=1 加进来。
- 92 条来路全加完:dp[2] = 2。
- 10凑和 3:最后一个数选 1,剩下要凑 2 → 把 dp[2]=2 加进来。
- 11凑和 3:最后一个数选 2,剩下要凑 1 → 把 dp[1]=1 加进来。
- 12凑和 3:最后一个数选 3,剩下要凑 0 → 把 dp[0]=1 加进来。
- 133 条来路全加完:dp[3] = 4。
- 14凑和 4:最后一个数选 1,剩下要凑 3 → 把 dp[3]=4 加进来。
- 15凑和 4:最后一个数选 2,剩下要凑 2 → 把 dp[2]=2 加进来。
- 16凑和 4:最后一个数选 3,剩下要凑 1 → 把 dp[1]=1 加进来。
- 173 条来路全加完:dp[4] = 7。
- 18凑和 5:最后一个数选 1,剩下要凑 4 → 把 dp[4]=7 加进来。
- 19凑和 5:最后一个数选 2,剩下要凑 3 → 把 dp[3]=4 加进来。
- 20凑和 5:最后一个数选 3,剩下要凑 2 → 把 dp[2]=2 加进来。
- 213 条来路全加完:dp[5] = 13。
- 22凑和 6:最后一个数选 1,剩下要凑 5 → 把 dp[5]=13 加进来。
- 23凑和 6:最后一个数选 2,剩下要凑 4 → 把 dp[4]=7 加进来。
- 24凑和 6:最后一个数选 3,剩下要凑 3 → 把 dp[3]=4 加进来。
- 253 条来路全加完:dp[6] = 24。
- 26dp[6]=24dp[6]=24 就是凑出 6 的全部组合数。一维 DP 一遍扫完,O(target × nums) 搞定。
⚠️ 容易写错的地方
✗ 错:外层 num、内层 t(求组合的写法)
✓ 对:外层 t、内层 num
本题数顺序(排列),必须让同一数在不同位置重复计
✗ 错:忘了 dp[0] = 1
✓ 对:dp[0] 必须初始化为 1
它是空方案,所有递推的源头,漏了全表归零
✗ 错:结果用 int 存
✓ 对:C++/Java 用 long/无符号
target 大时排列数会溢出 int(本题官方明说会爆 int)
完整代码(Python / C++ / Java)
Python
class Solution:
def combinationSum4(self, nums, target):
dp = [0] * (target + 1)
dp[0] = 1 # 凑出 0 有空方案这一种
for t in range(1, target + 1): # 外层:总和 t
for num in nums: # 内层:最后取哪个 num
if num <= t:
dp[t] += dp[t - num] # 累加 dp[t-num]
return dp[target]C++
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<unsigned long long> dp(target + 1, 0);
dp[0] = 1; // 空方案
for (int t = 1; t <= target; t++) { // 外层:总和
for (int num : nums) { // 内层:末尾数
if (num <= t) dp[t] += dp[t - num];
}
}
return (int) dp[target];
}
};Java
class Solution {
public int combinationSum4(int[] nums, int target) {
long[] dp = new long[target + 1];
dp[0] = 1; // 空方案
for (int t = 1; t <= target; t++) { // 外层:总和
for (int num : nums) { // 内层:末尾数
if (num <= t) dp[t] += dp[t - num];
}
}
return (int) dp[target];
}
}复杂度
时间复杂度
O(target × n)
每个总和 t 都扫一遍 nums(n 个数),共 target 个总和
空间复杂度
O(target)
一维 dp 数组,长度 target+1
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 组合总和 Ⅳ 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
题目名字是「组合总和」,为什么解法数的是排列?+
看示例就懂:nums=[1,2,3]、target=6 的答案 24 里,1+2 和 2+1 被当成两种不同方案,顺序不同即不同,这正是排列的定义。所以转移时要把「末尾数可以是任意一个 num」全枚举,做法上外层遍历总和、内层遍历 nums。
它和 LeetCode 518 零钱兑换 II 差在哪?+
两题共用同一行 dp[t] += dp[t−num],只差循环顺序。518 求组合数(顺序无关),外层遍历硬币、内层遍历金额,让每种硬币只按固定次序出现一次,顺序被抹掉。本题求排列数(顺序有关),外层遍历金额、内层遍历数,同一个数在不同位置会被重复计入。
如果 nums 里有负数会怎样?+
会崩。负数能让总和一会儿加一会儿减,可以凑出任意长的序列,方案数变成无穷,dp 无界也就无从递推。这道题正是官方的一个追问方向——要处理负数,得先给序列长度或别的条件加上限,否则问题本身没有有限答案。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 组合总和 Ⅳ 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。