题目描述
思路解析动画文字版
记住这三件事:传 i+1(只用一次)→ 同层跳重(去重)→ 超额就剪。下面每一帧就是其中一个动作。
选下标 0 的 1:这一层从下标 0 的 1 往下钻:把它加进 path,剩余从 8 减到 7。
path 接上 1:path → [1]。进入下一层从下标 1 继续——传 i+1,所以每个数只用一次。
选下标 1 的 1:这一层从下标 1 的 1 往下钻:把它加进 path,剩余从 7 减到 6。
path 接上 1:path → [1,1]。进入下一层从下标 2 继续——传 i+1,所以每个数只用一次。
选下标 2 的 2:这一层从下标 2 的 2 往下钻:把它加进 path,剩余从 6 减到 4。
path 接上 2:path → [1,1,2]。进入下一层从下标 3 继续——传 i+1,所以每个数只用一次。
超 target 剪枝:5 > 剩余 4:剩余只要凑 4,但下标 3 的 5 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 2 的 2:这条分支走完,撤销 2(path → [1,1]),回到上一层换下一个候选。
选下标 3 的 5:这一层从下标 3 的 5 往下钻:把它加进 path,剩余从 6 减到 1。
path 接上 5:path → [1,1,5]。进入下一层从下标 4 继续——传 i+1,所以每个数只用一次。
超 target 剪枝:6 > 剩余 1:剩余只要凑 1,但下标 4 的 6 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 3 的 5:这条分支走完,撤销 5(path → [1,1]),回到上一层换下一个候选。
选下标 4 的 6:这一层从下标 4 的 6 往下钻:把它加进 path,剩余从 6 减到 0。
path 接上 6:path → [1,1,6]。进入下一层从下标 5 继续——传 i+1,所以每个数只用一次。
收集第 1 条组合:剩余 = 0,path [1,1,6] 之和正好 = 8!收进结果。这是一条合法组合。
撤销下标 4 的 6:这条分支走完,撤销 6(path → [1,1]),回到上一层换下一个候选。
超 target 剪枝:7 > 剩余 6:剩余只要凑 6,但下标 5 的 7 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 1 的 1:这条分支走完,撤销 1(path → [1]),回到上一层换下一个候选。
选下标 2 的 2:这一层从下标 2 的 2 往下钻:把它加进 path,剩余从 7 减到 5。
path 接上 2:path → [1,2]。进入下一层从下标 3 继续——传 i+1,所以每个数只用一次。
选下标 3 的 5:这一层从下标 3 的 5 往下钻:把它加进 path,剩余从 5 减到 0。
path 接上 5:path → [1,2,5]。进入下一层从下标 4 继续——传 i+1,所以每个数只用一次。
收集第 2 条组合:剩余 = 0,path [1,2,5] 之和正好 = 8!收进结果。这是一条合法组合。
撤销下标 3 的 5:这条分支走完,撤销 5(path → [1,2]),回到上一层换下一个候选。
超 target 剪枝:6 > 剩余 5:剩余只要凑 5,但下标 4 的 6 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 2 的 2:这条分支走完,撤销 2(path → [1]),回到上一层换下一个候选。
选下标 3 的 5:这一层从下标 3 的 5 往下钻:把它加进 path,剩余从 7 减到 2。
path 接上 5:path → [1,5]。进入下一层从下标 4 继续——传 i+1,所以每个数只用一次。
超 target 剪枝:6 > 剩余 2:剩余只要凑 2,但下标 4 的 6 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 3 的 5:这条分支走完,撤销 5(path → [1]),回到上一层换下一个候选。
选下标 4 的 6:这一层从下标 4 的 6 往下钻:把它加进 path,剩余从 7 减到 1。
path 接上 6:path → [1,6]。进入下一层从下标 5 继续——传 i+1,所以每个数只用一次。
超 target 剪枝:7 > 剩余 1:剩余只要凑 1,但下标 5 的 7 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 4 的 6:这条分支走完,撤销 6(path → [1]),回到上一层换下一个候选。
选下标 5 的 7:这一层从下标 5 的 7 往下钻:把它加进 path,剩余从 7 减到 0。
path 接上 7:path → [1,7]。进入下一层从下标 6 继续——传 i+1,所以每个数只用一次。
收集第 3 条组合:剩余 = 0,path [1,7] 之和正好 = 8!收进结果。这是一条合法组合。
撤销下标 5 的 7:这条分支走完,撤销 7(path → [1]),回到上一层换下一个候选。
超 target 剪枝:10 > 剩余 7:剩余只要凑 7,但下标 6 的 10 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 0 的 1:这条分支走完,撤销 1(path → []),回到上一层换下一个候选。
同层去重:跳过下标 1 的 1:下标 1 的 1 和同层前一个下标 0 的 1 相同,这一层已经从前一个 1 开过分支了,再选会产出重复组合 → 画 ✗ 剪掉。
选下标 2 的 2:这一层从下标 2 的 2 往下钻:把它加进 path,剩余从 8 减到 6。
path 接上 2:path → [2]。进入下一层从下标 3 继续——传 i+1,所以每个数只用一次。
选下标 3 的 5:这一层从下标 3 的 5 往下钻:把它加进 path,剩余从 6 减到 1。
path 接上 5:path → [2,5]。进入下一层从下标 4 继续——传 i+1,所以每个数只用一次。
超 target 剪枝:6 > 剩余 1:剩余只要凑 1,但下标 4 的 6 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 3 的 5:这条分支走完,撤销 5(path → [2]),回到上一层换下一个候选。
选下标 4 的 6:这一层从下标 4 的 6 往下钻:把它加进 path,剩余从 6 减到 0。
path 接上 6:path → [2,6]。进入下一层从下标 5 继续——传 i+1,所以每个数只用一次。
收集第 4 条组合:剩余 = 0,path [2,6] 之和正好 = 8!收进结果。这是一条合法组合。
撤销下标 4 的 6:这条分支走完,撤销 6(path → [2]),回到上一层换下一个候选。
超 target 剪枝:7 > 剩余 6:剩余只要凑 6,但下标 5 的 7 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 2 的 2:这条分支走完,撤销 2(path → []),回到上一层换下一个候选。
选下标 3 的 5:这一层从下标 3 的 5 往下钻:把它加进 path,剩余从 8 减到 3。
path 接上 5:path → [5]。进入下一层从下标 4 继续——传 i+1,所以每个数只用一次。
超 target 剪枝:6 > 剩余 3:剩余只要凑 3,但下标 4 的 6 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 3 的 5:这条分支走完,撤销 5(path → []),回到上一层换下一个候选。
选下标 4 的 6:这一层从下标 4 的 6 往下钻:把它加进 path,剩余从 8 减到 2。
path 接上 6:path → [6]。进入下一层从下标 5 继续——传 i+1,所以每个数只用一次。
超 target 剪枝:7 > 剩余 2:剩余只要凑 2,但下标 5 的 7 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 4 的 6:这条分支走完,撤销 6(path → []),回到上一层换下一个候选。
选下标 5 的 7:这一层从下标 5 的 7 往下钻:把它加进 path,剩余从 8 减到 1。
path 接上 7:path → [7]。进入下一层从下标 6 继续——传 i+1,所以每个数只用一次。
超 target 剪枝:10 > 剩余 1:剩余只要凑 1,但下标 6 的 10 已经更大——再选必然超 8,升序下它后面的更大,整层一起放弃,画 ✗。
撤销下标 5 的 7:这条分支走完,撤销 7(path → []),回到上一层换下一个候选。
超 target 剪枝:10 > 剩余 8:搜索结束。所有「每个数只用一次、同层不重、不超 8」的路径都试过了,收集到 4 条组合:[[1,1,6],[1,2,5],[1,7],[2,6]]。
边界三连:同路径可用两个相同值、同层只用第一个、凑不出返回空。
两个高频追问,区别全在 i+1 与同层去重。
参考代码
def combinationSum2(candidates, target): candidates.sort() # 排序: 相同数字相邻 + 便于剪枝 res, path = [], [] def bt(start, remain): if remain == 0: res.append(path[:]); return for i in range(start, len(candidates)): if i > start and candidates[i] == candidates[i-1]: continue # 同层去重 if candidates[i] > remain: break # 超 target 剪枝 path.append(candidates[i]) bt(i + 1, remain - candidates[i]) # 传 i+1: 每个数只用一次 path.pop() # 回溯撤销 bt(0, target) return res复杂度
- 时间:O(2ⁿ · n),n 个候选每个选/不选构成搜索树,剪枝后远小于此;每条组合拷贝 O(n)
- 空间:O(n),递归栈 + path 深度最多 n(不计结果)
易错点
面试追问把动画讲成自己的话
追问和 LC39 组合总和有什么区别?
追问为什么 i>start 能正确去重又不误伤 [1,1,6]?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
单词搜索
LeetCode 79 · 中等 · 沿着 回溯 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题