从零讲透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: List[int]

:rtype: List[List[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主等多位大佬的细心讲解,以上是我的一些笔记。

相关推荐
咩咩觉主4 分钟前
c#数据结构 线性表篇 非常用线性集合总结
开发语言·数据结构·unity·c#·游戏引擎·程序框架
李匠202425 分钟前
C++GO语言微服务和服务发现②
开发语言·c++·golang·服务发现
每次的天空39 分钟前
Kotlin 内联函数深度解析:从源码到实践优化
android·开发语言·kotlin
268572591 小时前
JVM 监控
java·开发语言·jvm
曼岛_1 小时前
[Java实战]Spring Boot 静态资源配置(十三)
java·开发语言·spring boot
爱补鱼的猫猫1 小时前
22、近端策略优化算法(PPO)论文笔记
论文阅读·算法
m0_616188491 小时前
使用vue3-seamless-scroll实现列表自动滚动播放
开发语言·javascript·ecmascript
开心星人1 小时前
【论文阅读】Reconstructive Neuron Pruning for Backdoor Defense
论文阅读·算法·剪枝
qq_433554541 小时前
C++ STL编程 vector空间预留、vector高效删除、vector数据排序、vector代码练习
开发语言·c++
老朋友此林1 小时前
MiniMind:3块钱成本 + 2小时!训练自己的0.02B的大模型。minimind源码解读、MOE架构
人工智能·python·nlp