回溯算法中的 startindex:从组合到子集,彻底搞懂搜索起点
回溯算法中的 start index:从组合到子集,彻底搞懂搜索起点
在回溯算法的学习中,start index 是一个经常出现却又容易混淆的参数。为什么有的递归传入 i,有的传入 i+1?为什么排列问题不用 start 而用 used 数组?本文将带你彻底搞懂 start index 的作用、使用场景和技巧,并通过经典题型示例加深理解。
一、什么是 start index?
start index 是递归函数中的一个参数,它表示当前层搜索的起始位置。在每一层递归中,我们只从 start 开始遍历候选元素,从而限制下一层的选择范围。
它的核心作用是保证结果集中的组合/子集是无序的,避免产生重复的排列。例如,在组合问题中,[1,2] 和 [2,1] 被视为同一个组合,通过 start 就能确保只按递增顺序选取元素。
二、start index 的两种常见形式
根据问题对元素使用次数的限制,start 的传递方式有两种:
1. 下一层从 i 开始(允许重复使用当前元素)
场景:组合总和 I(允许重复选取同一个元素)
含义:当前选择了 candidates[i],下一层仍然可以继续选择它,所以递归调用时传入 i。
示例代码(组合总和 I):
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
res = []
path = []
def backtrack(start, cur_sum):
if cur_sum == target:
res.append(path[:])
return
if cur_sum > target:
return
for i in range(start, len(candidates)):
path.append(candidates[i])
backtrack(i, cur_sum + candidates[i]) # 传入 i,允许重复使用
path.pop()
backtrack(0, 0)
return res
2. 下一层从 i+1 开始(不允许重复使用当前元素)
场景:组合总和 II(每个元素只能使用一次)、子集 I
含义:当前选择了 candidates[i],下一层只能从它后面的元素中选取,所以传入 i+1。
示例代码(组合总和 II):
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort() # 排序方便剪枝和去重
res = []
path = []
def backtrack(start, cur_sum):
if cur_sum == target:
res.append(path[:])
return
for i in range(start, len(candidates)):
if cur_sum + candidates[i] > target:
break # 剪枝
if i > start and candidates[i] == candidates[i-1]:
continue # 去重:同一层跳过重复元素
path.append(candidates[i])
backtrack(i+1, cur_sum + candidates[i]) # 传入 i+1,不可重复
path.pop()
backtrack(0, 0)
return res
三、结合剪枝与去重
在使用 start index 的同时,常常配合排序和剪枝来提高效率:
- 排序:将数组排序后,可以利用递增性质进行剪枝(如当前和已经超过目标,后续更大元素直接跳过)。
- 同一层去重:当数组包含重复元素时,需要在同一层跳过相同值,避免产生重复组合。典型如组合总和 II、子集 II。
子集 II 示例(去重 + 从 i+1 开始):
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
path = []
def backtrack(start):
res.append(path[:])
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i-1]:
continue
path.append(nums[i])
backtrack(i+1)
path.pop()
backtrack(0)
return res
四、对比排列问题:为什么不用 start index?
排列问题与组合问题不同,它关心元素的顺序,即 [1,2] 和 [2,1] 是两个不同的结果。因此,我们不能用 start 来限制选择范围,而是需要用 used 数组标记哪些元素已经被使用过,确保每个元素在每个排列中只出现一次。
全排列示例(用 used 数组):
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
path = []
used = [False] * len(nums)
def backtrack():
if len(path) == len(nums):
res.append(path[:])
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True
path.append(nums[i])
backtrack()
path.pop()
used[i] = False
backtrack()
return res
小结:
- 组合/子集:无序 → 用
start控制起点 - 排列:有序 → 用
used标记已选
五、经典题型总结
| 题型 | 核心特点 | start 使用方式 | 关键技巧 |
|---|---|---|---|
| 组合总和 I | 元素可重复使用 | 下一层传入 i |
剪枝(和超过目标时停止) |
| 组合总和 II | 元素不可重复,可能有重复元素 | 下一层传入 i+1 |
排序 + 同一层去重 |
| 子集 I | 元素互异 | 下一层传入 i+1 |
每个节点都加入结果 |
| 子集 II | 可能有重复元素 | 下一层传入 i+1 |
排序 + 同一层去重 |
| 分割回文串 | 按分割线切割 | 用 start 表示当前切割的起始位置 |
每次截取 s[start:i+1] |
| 电话号码的字母组合 | 多个集合组合 | 用 index 遍历每个数字对应的字符串 |
无需 start,只需递归下一个集合 |
六、总结
start index 是回溯算法中控制搜索方向的核心参数,它确保了组合/子集问题不会产生重复结果。记住以下口诀:
- 组合无序用 start,排列有序用 used
- 可重复用 i,不可重用 i+1
- 去重需排序,同一层跳过相同值
掌握了 start index 的使用技巧,回溯问题的框架就会变得清晰很多。希望本文能帮助你彻底搞懂这个看似简单却容易出错的概念,在刷题路上更加得心应手!
欢迎留言讨论,如果觉得有帮助,请点个赞支持一下吧!

浙公网安备 33010602011771号