回溯算法中的 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 的使用技巧,回溯问题的框架就会变得清晰很多。希望本文能帮助你彻底搞懂这个看似简单却容易出错的概念,在刷题路上更加得心应手!


欢迎留言讨论,如果觉得有帮助,请点个赞支持一下吧!

posted @ 2026-03-11 16:38  Leon_LL  阅读(2)  评论(0)    收藏  举报