《代码随想录》回溯问题学习总结

How to work out the problem of backtracing?

1. 组合问题

回溯的本质仍然是递归
考虑递归三部曲:

  1. 递归传递参数和返回值 backtracing() -> None:
  2. 递归的结束条件
  3. 在每次递归中需要执行的逻辑

1.1 分析回溯问题

image
以最简单的组合问题为例,在抽象树的每一层中是遍历每种可能取值,而在树枝(深度)中遍历则是寻找每种组合的子组合

1.2 回溯问题的模版代码

仍然组合问题为例

# 组合问题
def combine(self, n: int, k: int) -> list[list[int]]:
    result = []
    self.backtracing(n, k, 1, [], result)
    return result

# paragram
def backtracing(self, n, k, startindex, path: list, result: list):
    # stop condition
    if len(path) == k:
        result.append(path[:])
        return
    
    for i in range(startindex, n - (k - len(path)) + 2): # 剪枝
        path.append(i)
        self.backtracing(n, k, i+1, path, result)
        path.pop()

1.3 什么时候需要startindex

每次在集合中选取元素,可选择的范围随着选取的进行而收缩(可选取元素减少),调整可选取的范围,就是要靠startindex

1.4 在for - 层遍历过程中添加额外判断逻辑

Leetcode第40题组合总和为例,需要对元素进行去重

    # LC.40.组合总和2
def combinationSum2(self, candidates: list[int], target: int) -> list[list[int]]:
    candidates = sorted(candidates)
    temp, res = [], []

    def backtracing(startindex: int, used: list) -> None:
        
        if target == sum(temp):
            res.append(temp.copy())
            return
        
        elif target < sum(temp):
            return
            

        for i in range(startindex, len(candidates)):
            if i > 0 and candidates[i-1] == candidates[i] and used[i-1] == False:
                continue

            temp.append(candidates[i])
            used[i] = True
            backtracing(i+1, used)
            used[i] = False
            temp.pop()
    
    backtracing(0, [False for i in range(len(candidates))])

在本题中,同一个解可以出现同一元素(按树枝搜索),但在解集合中不能出现相同元素,如[[1,1,2],[1,2,1]]是不合法的,使用candidates[]used[]数组来控制按层搜索和按树枝搜索时出现重复元素是否可以被添加到解集合中,体现在代码中:

for i in range(startindex, len(candidates)):
        if i > 0 and candidates[i-1] == candidates[i] and used[i-1] == False:
            continue

2. 子集问题

2.1 子集问题概述

子集问题和排列问题的不同之处是需要收集抽象树上的每一个节点,而非叶子节点。即每次回溯都需要判断当前节点是否是解,而非在每次回溯判断是否到达叶子节点。

2.2 子集问题模版

以力扣第70题为例,在每次调用backtracing()函数后都需要收集当前节点

def subsets(self, nums: list[int]) -> list[list[int]]:
    res, path = [], []

    # 回溯
    def backtracing(startindex: int) -> None:
        # 每次调用backtracing都是在遍历该树的一个节点
        res.append(path.copy())

        if startindex == len(nums):
            return
        
        for i in range(startindex, len(nums)):
            path.append(nums[i])
            backtracing(i+1)
            path.pop()
    
    backtracing(0)
    return res

2.3 子集问题去重

子集问题的去重同组合问题相同,但需要对集合进行排序,考虑在抽象树的每一数层上不可以使用相同元素,以力扣第90题为例,分别使用used标志数组和set()集合去重

2.3.1 used标志数组去重
# 去重问题,同一树层不能重复(不同的解集合),但在同一树枝可以重复(寻找唯一子集的过程,树的遍历深度加深)
def subsetsWithDup(self, nums: list[int]) -> list[list[int]]:
    nums.sort()
    res, path = [], []


    # 回溯
    def backtracing(startindex: int, used: list[bool]) -> None:
        # 每次调用backtracing都是在遍历该树的一个节点
        res.append(path.copy())

        if startindex == len(nums):
            return
        
        for i in range(startindex, len(nums)):
            # 去重
            if i > 0 and nums[i-1] == nums[i] and used[i-1] is False:
                continue

            path.append(nums[i])
            used[i] = True
            backtracing(i+1, used)
            # 回溯
            used[i] = False
            path.pop()
    
    backtracing(0, [False for i in range(len(nums))])
    return res
2.3.2 set()集合去重
def subsetsWithDup(self, nums):
    nums.sort()  # 去重需要排序
    result = []
    self.backtracking(nums, 0, [], result)
    return result

def backtracking(self, nums, startIndex, path, result):
    result.append(path[:])
    used = set()
    for i in range(startIndex, len(nums)):
        if nums[i] in used:
            continue
        used.add(nums[i])
        path.append(nums[i])
        self.backtracking(nums, i + 1, path, result)
        path.pop()

3. 排列问题

3.1 概述

排列问题与组合问题的区别在于每次for循环遍历时不使用startindex来寻找下一个遍历起始位置,而是从0开始

3.2 模版

def permute(self, nums: list[int]) -> list[list[int]]:
    res, path = [], []

    def backtracing() -> None:
        if len(path) is len(nums):
            res.append(path.copy())
            return
        
        for i in range(len(nums)):
            # 数字不重复
            if nums[i] in path:
                continue

            path.append(nums[i])
            backtracing()
            path.pop()

    backtracing()
    return res

3.3 去重

去重逻辑和之前相同,但需要注意的是,在同一树枝上进行遍历时也需要去重,即使用if used[i] == Fasle来避免在同一树枝进行遍历时使用到相同元素如nums=[1,1,2]中,选取顺序:nums[0], nums[1], nums[2]nums[1], nums[0], nums[2]会导致排列集重复

def permuteUnique(self, nums: list[int]) -> list[list[int]]:
    nums.sort()
    res, path = [], []

    def backtracing(used: list) -> None:
        if len(path) is len(nums):
            res.append(path.copy())
            return
        for i in range(len(nums)):
            if (i > 0 and nums[i-1] == nums[i] and not used[i-1]):
                continue

            if used[i] == False: # 同一树枝i没使用过
                used[i] = True
                path.append(nums[i])
                backtracing(used)
                path.pop()
                used[i] = False

    backtracing([False for i in range(len(nums))])
    return res
原创作者: yunshengbuzhichu 转载于: https://www.cnblogs.com/yunshengbuzhichu/p/18950205
### 如何使用代码随想录进行学习 #### 明确目标与资源 代码随想录是一个专注于算法和数据结构学习的平台,其内容设计旨在帮助初学者逐步掌握编程技能并提升解题能力。对于希望深入理解算法的人来说,该平台提供了丰富的题目分类、详细的解析以及系统的训练计划[^1]。 #### 制定合理的学习策略 为了最大化利用代码随想录的内容,建议采用分阶段的方式进行学习。例如,在开始刷题前先熟悉基本的数据结构概念(如数组、链表、栈等),这样可以更好地理解和应用所学到的知识点。同时,按照主题顺序推进练习也很重要,比如从简单的动态规划问题到复杂的回溯法案例[^2]。 #### 跟踪进度与反思总结 记录自己的学习过程是非常有益的做法之一。通过撰写个人笔记或者博客文章分享心得体验可以帮助巩固记忆,并形成属于自己的知识体系。正如某位参与者提到,“经过40天的系统化训练...解决了之前‘死记硬背却记不住’的痛点”,说明持续性的实践加上适时回顾能够显著提高效率[^4]。 #### 实践中的注意事项 当遇到具体类型的难题时,不妨参照官方给出的标准解答思路去模仿操作。以“重建行程”为例虽然尚未完成编写工作但仍可作为参考模板来探索解决路径[^3]。此外还需注意保持耐心坚持到底因为技术成长往往需要时间积累才能见到成效。 ```python def example_backtracking(): result = [] def backtrack(path, choices): if not choices and is_valid_solution(path): result.append(list(path)) # 记录可行方案 return for choice in list(choices): path.append(choice) choices.remove(choice) backtrack(path, choices) # 探索下一步 path.pop() # 撤销当前决策 choices.add(choice) # 恢复原始状态 initial_choices = set(...) # 初始化选项集合 empty_path = [] # 开始为空路径 backtrack(empty_path, initial_choices) return result # 返回所有可能的结果列表 ``` 上述伪代码展示了典型的回溯算法框架,适用于多种组合优化类问题求解场景下运用尝试错误机制寻找最优答案的过程演示。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值