39. 组合总和
本题是 集合里元素可以用无数次,那么和组合问题的差别 其实仅在于 startIndex上的控制
题目链接/文章讲解:https://programmercarl.com/0039.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C.html
视频讲解:https://www.bilibili.com/video/BV1KT4y1M7HJ
[] (remain=8)
├─ 2 (→ 6)
│ ├─ 2 (→ 4)
│ │ ├─ 2 (→ 2)
│ │ │ └─ 2 (→ 0) ✓ 收集 [2,2,2,2]
│ │ └─ (后续候选均 > 2)× ← 遇到 3>2,直接 break
│ └─ 3 (→ 3)
│ ├─ 3 (→ 0) ✓ 收集 [2,3,3]
│ └─ (后续候选均 > 0)×
└─ 3 (→ 5)
├─ 3 (→ 2) (后续候选均 > 2)×
└─ 5 (→ 0) ✓ 收集 [3,5]
└─ 5 (→ 3) (后续候选均 > 3)×
candidates:候选数字列表(正整数、互不相同)。
target:目标和。
res:结果大盒子,装所有满足和为target的组合。
path:临时背包,装“当前已经选择的数字”(始终非降序)。
dfs(start, remain):回溯函数
start:从candidates的哪个下标开始枚举(只往后看,避免重复排列);
remain:还差多少才能凑到target(target - sum(path))。结束条件:
remain == 0表示刚好凑满,收集path的拷贝;剪枝:如果某个候选
x > remain,因为已排序,后面更大,直接break。
class Solution:
def combinationSum(self, candidates, target):
"""
题意:从 candidates 中选若干数(每个数可重复使用),使其和为 target,
返回所有“组合”(不关心顺序,通常按非降序输出)。
参数:
- candidates: List[int],正整数、互不相同。
- target: int,目标和。
返回:
- res: List[List[int]],所有满足和为 target 的组合,每个组合内部非降序。
"""
# 基本健壮性处理
if not candidates or target <= 0:
return []
# 1) 排序:为了使用“> remain 则后续全剪掉”的剪枝
candidates = sorted(candidates)
res = [] # 结果集合
path = [] # 当前构造的组合(始终非降序)
def dfs(start, remain):
"""
回溯函数(深度优先搜索)
参数:
- start: int,从 candidates 的哪个下标开始枚举(只向后看,避免重复排列)
- remain: int,还差多少才能凑到 target(target - sum(path))
流程:
A. 命中:remain == 0 -> 收集 path 的副本,返回
B. 循环枚举 i 从 start 到末尾:
B-1. 若 candidates[i] > remain -> 后面都更大,直接 break(整段剪枝)
B-2. 做选择:把当前数放进 path
B-3. 递归:由于可重复使用当前数,下一层仍从 i 开始(不是 i+1)
B-4. 回溯:把刚才放进去的数弹出,试下一个候选
"""
# A. 命中
if remain == 0:
res.append(path[:]) # 记得拷贝!(否则后续回溯会修改同一个列表)
return
# B. 从 start 开始尝试每个候选
for i in range(start, len(candidates)):
x = candidates[i]
# B-1. 剪枝:已排序,若 x 超过 remain,后面更大,整段可跳出
if x > remain:
break
# B-2. 做选择
path.append(x)
# B-3. 递归:因为允许重复使用 x,所以下一层仍从 i 开始
dfs(i, remain - x)
# B-4. 回溯:撤销选择,试下一个候选
path.pop()
# 入口:从下标 0 开始,剩余和为 target
dfs(start=0, remain=target)
return res
# =============== 自测(可直接运行) ===============
def _norm(lst):
"""组合的顺序不重要:每个组合内部先排,再整体排序,便于对比"""
return sorted([sorted(x) for x in lst])
if __name__ == "__main__":
s = Solution()
# 示例:candidates = [2,3,5], target = 8
got = s.combinationSum([2,3,5], 8)
expect = [[2,2,2,2],[2,3,3],[3,5]]
assert _norm(got) == _norm(expect)
# 其他用例
assert _norm(s.combinationSum([2,3,6,7], 7)) == _norm([[2,2,3],[7]])
assert s.combinationSum([2], 1) == []
assert _norm(s.combinationSum([1], 2)) == _norm([[1,1]])
print("All tests passed!")
40.组合总和II
本题开始涉及到一个问题了:去重。
注意题目中给我们 集合是有重复元素的,那么求出来的 组合有可能重复,但题目要求不能有重复组合。
题目链接/文章讲解:https://programmercarl.com/0040.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CII.html
视频讲解:https://www.bilibili.com/video/BV12V4y1V73A
candidates:候选数字列表(正整数,可能包含重复值)。
target:目标和。
res:结果大盒子,收集所有和为target的组合(组合内部非降序、结果无重复)。
path:临时背包,保存“当前已选择的数字”(非降序)。
dfs(start, remain):回溯函数
start:这一层从candidates的哪个下标开始枚举(只往后看,避免排列)。
remain:距离target还差多少(target - sum(path))。结束:
remain == 0→ 收集path的拷贝。剪枝1(和):若
nums[i] > remain,后面更大,整段 break。剪枝2(去重):同层枚举中,若
i>start 且 nums[i]==nums[i-1]就跳过,避免重复组合。单次使用:这题一个元素最多用一次 → 递归到下一层用
dfs(i+1, …)(而不是i)。
class Solution:
def combinationSum2(self, candidates, target):
"""
题意:在 candidates(可包含重复)中选若干数,每个数最多用一次,使其和为 target,
返回所有不重复的组合(组合内部通常非降序)。
关键点:
1) 排序(非常关键):便于“>remain 整段剪枝”和“同层去重”。
2) 同层去重:i>start 且 nums[i]==nums[i-1] -> continue。
3) 每个数最多用一次:递归下一层从 i+1 开始。
"""
if not candidates or target <= 0:
return []
nums = sorted(candidates) # 排序是去重与剪枝的基础
res = []
path = []
def dfs(start, remain):
"""
参数:
- start: 当前层从 nums 的哪个下标开始枚举(只往后看)
- remain: 还差多少到 target
流程:
A) 命中 remain==0:收集 path 的拷贝并返回
B) i 从 start..len(nums)-1:
- 去重:若 i>start 且 nums[i]==nums[i-1] -> continue(同层跳过相同值)
- 和剪枝:若 nums[i] > remain -> 后续更大,break
- 选择 nums[i] -> path.append(...)
- 递归到下一层 dfs(i+1, remain - nums[i]) # 单次使用
- 回溯 path.pop()
"""
if remain == 0:
res.append(path[:]) # 拷贝很重要
return
for i in range(start, len(nums)):
# —— 去重(同一层枚举时,跳过与前一个相同的值)——
if i > start and nums[i] == nums[i - 1]:
continue
x = nums[i]
# —— 和剪枝:排序后若 x 已经超过 remain,后面只会更大,直接整段剪掉 ——
if x > remain:
break
# 做选择
path.append(x)
# 单次使用:下一层从 i+1 开始
dfs(i + 1, remain - x)
# 回溯
path.pop()
dfs(start=0, remain=target)
return res
# =============== 自测(可直接运行) ===============
def _norm(lst):
"""组合顺序不重要:组合内部与整体都做排序以便比较"""
return sorted([sorted(x) for x in lst])
if __name__ == "__main__":
s = Solution()
# 官方示例 1
got = s.combinationSum2([10,1,2,7,6,1,5], 8)
expect = [[1,1,6],[1,2,5],[1,7],[2,6]]
assert _norm(got) == _norm(expect)
# 官方示例 2
got = s.combinationSum2([2,5,2,1,2], 5)
expect = [[1,2,2],[5]]
assert _norm(got) == _norm(expect)
# 无解
assert s.combinationSum2([4,4,4], 3) == []
# 含重复但每个只用一次
got = s.combinationSum2([1,1,1,2,2,3], 4)
# 可行组合(顺序无关):[1,1,2], [1,3], [2,2]
expect = [[1,1,2],[1,3],[2,2]]
assert _norm(got) == _norm(expect)
print("All tests passed!")
39 题:元素可重复使用 → 递归用
dfs(i, …)。40 题:每个元素最多用一次 + 去重 → 递归用
dfs(i+1, …),并在同层用
if i>start and nums[i]==nums[i-1]: continue跳过重复值。
131.分割回文串
本题较难,大家先看视频来理解 分割问题,明天还会有一道分割问题,先打打基础。
https://programmercarl.com/0131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.html
变量/参数角色
s:原字符串。
result:大盒子,收集所有“切出来每段都是回文”的方案(例如["aa","b"])。
path:临时背包,记录当前这一条搜索路径已经切出来的若干段(从左到右)。
start_index:下一段要从s[start_index:]的开头开始切。递归框架(一层意思是一段的右端点)
在一次递归里,我们固定左端点
start_index,然后把右端点i从start_index枚举到末尾:
取出当前候选段
seg = s[start_index : i+1]判断
seg是否回文(seg == seg[::-1])如果是回文:把
seg加入path,下一段从i+1再开始递归递归返回后,
path.pop()回溯,尝试更长/别的段终止条件:
start_index == len(s),说明整串都切完了,path就是一种完整切法,result.append(path[:])收集。心法:每一层就是“确定下一段的右端点在哪”;层层向右推进,直到切到字符串末尾。
为什么不会重复?
一个切分方案由一组切割位置决定(比如在索引 1、2 切)。不同的切割位置就得到不同的
path;相同的切割位置只会被枚举到一次,所以天然不会生成重复方案,不需要像“组合总和 II”那样同层去重。小例子:
s = "aab"索引:
0:a 1:a 2:b
start=0, path=[] i=0 → seg="a" 是回文 → path=["a"] → 递归 start=1 start=1, path=["a"] i=1 → seg="a" 是回文 → path=["a","a"] → 递归 start=2 start=2, path=["a","a"] i=2 → seg="b" 是回文 → path=["a","a","b"] → 递归 start=3 start==len(s):收集 ["a","a","b"] 回溯 path=["a","a"] i=2 → seg="ab" 不是回文 → 跳过 回溯 path=["a"] i=1 → seg="aa" 是回文 → path=["a","aa"] → 递归 start=2 start=2, path=["a","aa"] i=2 → seg="b" 是回文 → path=["a","aa","b"] → 递归 start=3 收集 ["a","aa","b"] 回溯 path=["a","aa"] → 再回溯 path=["a"] i=2 → seg="aab" 不是回文 → 跳过 回溯 path=[]
关键细节与常见坑
seg == seg[::-1]:这是 Python 判回文的常用招,[::-1]会返回一份倒序拷贝;比较是 O(段长)。必须拷贝
path:result.append(path[:]),否则后续回溯会把已经加入结果的列表改掉。边界:当
s == "",第一次就命中终止条件,返回[[]](一条“切零段”的合法方案),这和题意一致。复杂度:
枚举切法本身最坏有 ~
2^(n-1)种(每个间隙切/不切)。每次判回文/切片是 O(段长),所以总体接近
O(2^n · n)。想加速:改用回文 DP 表或双指针
is_pal(l,r)(不切片)即可。
s="aab"
(start=0) path=[]
├─ "a" = s[0:1](回文) → path=["a"] (start=1)
│ ├─ "a" = s[1:2](回文) → path=["a","a"] (start=2)
│ │ └─ "b" = s[2:3](回文) → path=["a","a","b"] (start=3) ✓ 收集
│ └─ "ab" = s[1:3](非回文) ×
├─ "aa" = s[0:2](回文) → path=["aa"] (start=2)
│ └─ "b" = s[2:3](回文) → path=["aa","b"] (start=3) ✓ 收集
└─ "aab" = s[0:3](非回文) ×
from typing import List
class Solution:
def partition(self, s: str) -> List[List[str]]:
"""
把 s 切分为若干回文子串,返回所有切法。
"""
result: List[List[str]] = []
self.backtracking(s, start_index=0, path=[], result=result)
return result
def backtracking(self, s: str, start_index: int, path: List[str], result: List[List[str]]) -> None:
# 终止:切到了字符串末尾,收集一份方案
if start_index == len(s):
result.append(path[:]) # 记得拷贝
return
# 枚举右端点 i,尝试把下一段切成 s[start_index : i+1]
for i in range(start_index, len(s)):
seg = s[start_index : i + 1]
# 判回文:正序 == 反序
if seg == seg[::-1]:
path.append(seg) # 做选择
self.backtracking(s, i + 1, path, result) # 下一段从 i+1 开始
path.pop() # 回溯
|第七章 回溯算法part02|·39. 组合总和·40.组合总和II·131.分割回文串&spm=1001.2101.3001.5002&articleId=150932413&d=1&t=3&u=cde735a01d12466684dd9079f41f14f0)
1031

被折叠的 条评论
为什么被折叠?



