从零讲透DFS-深度优先搜索-2(排序与组合)

一、核心知识点清单

1. 排列组合问题分类
  • 排列问题(顺序相关):如全排列(LeetCode 46)。

  • 组合问题(顺序无关):如子集(LeetCode 78)。

  • 关键区别:是否考虑元素的顺序。

2. 回溯模板强化
python 复制代码
def backtrack(path, choices):
    if 满足终止条件:
        记录结果
        return
    for 选择 in choices:
        if 选择不合法:  # 剪枝
            continue
        path.append(选择)          # 做选择
        backtrack(path, 新choices) # 递归
        path.pop()                # 撤销选择(回溯)
3. 去重技巧
  • 排序+跳过重复元素:适用于输入含重复数字的情况(如LeetCode 47)。

  • 示例代码:

    python 复制代码
    nums.sort()  # 先排序
    for i in range(len(nums)):
        if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
            continue  # 跳过重复分支
4. 剪枝优化场景
  • 提前终止:当路径不可能满足条件时(如组合总和超过目标值)。

  • 跳过无效分支:如全排列中已使用的数字不再选择。

5. 视频教程:

6. 图文教程:


二、学习安排(2小时)

第1个30分钟:理解排列与组合的区别

对比练习:

  • 全排列(1,2,3 → 6种顺序不同的解)。

    \[1,2,3\],\[1,3,2\],\[2,1,3\],\[2,3,1\],\[3,1,2\],\[3,2,1\]

  • 子集(1,2,3 → 8种包含不同元素的子集)。

\[空集\],\[1\],\[2\],\[3\],\[1,2\],\[1,3\],\[2,3\],\[1,2,3\]

显然**1,2 2,1是两个排列,但是为同一个组合。**

画决策树:

  • 用纸笔画出输入为1,2时的全排列和子集决策树,体会回溯过程。
第2个30分钟:全排列代码实现
  1. 题目:LeetCode 46. 全排列(无重复数字)。

    • 代码框架:

      python 复制代码
      def permute(nums):
          def backtrack(path):
              if len(path) == len(nums):
                  res.append(path.copy())
                  return
              for num in nums:
                  if num in path:  # 剪枝:已使用的数字跳过
                      continue
                  path.append(num)
                  backtrack(path)
                  path.pop()
          res = []
          backtrack([])
          return res
    • 关键点 :用path记录已选数字,通过if num in path剪枝。

值得注意的是上面的代码在力扣的运行中频繁报错AttributeError: 'list' object has no attribute 'copy', 这不禁联想到相同情况之前引用split报错,这里的原因是访问类型的不匹配,就好比split中我们试图将字符串分隔,但可能我们要操作的对象并不是字符串类型,而是列表类型。后面我将这句copy改成append("".join(path))也是报错。这里代码尝试把整数列表path连接成字符串。不过join方法要求传入的可迭代对象元素是字符串,而path中的元素是整数,所以会引发 TypeError。

所以可以得出这一块我们正在操作的是列表类型下的整数元素,最终将代码改成append(path:),即为正确答案。

这里不断修改类型是因为参考的是力扣的题目,有输入输出类型的限制。

:type nums: Listint

:rtype: ListList\[int]

第3个30分钟:子集问题与去重
  1. 题目:LeetCode 78. 子集。

    • 代码框架:

      python 复制代码
      def subsets(nums):
          def backtrack(start, path):
              res.append(path[:])  # 所有节点均记录结果
              for i in range(start, len(nums)):  # 避免重复选择
                  path.append(nums[i])
                  backtrack(i + 1, path)  # 从i+1开始选
                  path.pop()
          res = []
          backtrack(0, [])
          return res
    • 与全排列的区别 :通过start参数控制选择范围,避免顺序不同但元素相同的重复解。

第4个30分钟:全代码去重问题
  1. 题目:LeetCode 47. 全排列2。

    • 剪枝技巧:标记已使用过的数组、相同数字按顺序使用避免重复。

    • 代码片段:

      python 复制代码
      class Solution(object):
          def permuteUnique(self, nums):
              """
              :type nums: List[int]
              :rtype: List[List[int]]
              """
              def backtrack(path, used):
                  if len(path) == len(nums):
                      res.append(path[:])
                      return
                  for i in range(len(nums)):
                  # 剪枝条件:
                  # 1. 当前数字已使用过
                  # 2. 当前数字与前一个数字相同,且前一个数字未被使用(保证相同数字的顺序)
                      if used[i] or (i > 0 and nums[i] == nums[i-1] and not used[i-1]):
                          continue
                      used[i] = True
                      path.append(nums[i])
                      backtrack(path, used)
                      path.pop()
                      used[i] = False
      
              nums.sort()  # 必须先排序,使相同数字相邻
              res = []
              used = [False] * len(nums)
              backtrack([], used)
              return res

i > 0 and nums[i] == nums[i-1] and not used[i-1]:当前数字与前一个数字相同且前一个数字未被使用(保证相同数字按顺序使用,避免重复)

全排序1中代码直接检查num in path来判断是否使用过,这在有重复数字时会失效。


三、常见错误与调试技巧

  1. 忘记回溯:

    • 错误示例 :在path.append(num)后忘记path.pop(),导致结果重复。

    • 调试方法 :在递归前后打印path,观察状态变化。

  2. 去重逻辑错误:

    • 案例:LeetCode 47(含重复数字的全排列)中未排序直接跳过重复数。

    • 修正 :必须先排序,再判断nums[i] == nums[i-1]。

文章感谢各个CSDN博主、b站up主等多位大佬的细心讲解,以上是我的一些笔记。

相关推荐
杨丰玮4189 分钟前
C语言核心语法
java·c语言·开发语言·学习方法
ttwuai11 分钟前
Go开源后台管理系统推荐:README、源码和License应该怎么验
开发语言·golang·开源
Java后端的Ai之路14 分钟前
Python 进阶探索30 - Python中的装饰器
开发语言·人工智能·python·文件处理·装饰器模式
沫璃染墨14 分钟前
《从零入门Linux系统篇(五十八):线程篇·十一——线程安全与死锁详解:从可重入到多锁管理》
linux·服务器·开发语言·c++·驱动开发·安全·架构
打码的老程是远篁17 分钟前
手把手教你玩转大模型——3. Transformer 的位置编码——模型怎么知道谁在前,谁在后?
人工智能·算法
嘎嘎风18 分钟前
MiniMind 学习笔记之 06 一张图、一百行:MiniMind 的骨架
算法·源码
用户73416810354821 分钟前
只盯被动响应做状态监测?论“控制信号”在变工况故障诊断中的重要作用
算法
黑妹天下第一乖23 分钟前
第 05 讲:阿加犀 AidCV 图像处理加速与 OpenCV 一致开发实战
开发语言·图像处理·人工智能·嵌入式硬件·数码相机·opencv·计算机视觉
Ivanqhz23 分钟前
MQA、GQA、稀疏/滑动窗口注意力及混合范式
人工智能·算法·机器学习
言乐624 分钟前
Python基于关键词分拣快递(适用于电商与货运代理等)
开发语言·python·django·virtualenv·pygame