当前位置: 首页 > news >正文

回溯专题其一(组合篇)

一、回溯理论基础

回溯的概念

回溯(Backtracking)是一种搜索算法,常被称为“回溯搜索”。它本质上是 递归的副产品:在递归过程中不断尝试不同的选择,当某条路径不满足条件时,就 回退到上一步重新选择。因此,有回溯必有递归

由于回溯法会枚举搜索空间中的所有可能路径(可通过剪枝优化减少无效搜索),它非常适合解决 枚举子集、组合或排列 等类问题。这些问题往往难以用动态规划或贪心等常规解法解决。

抽象来看:

  • 集合的大小 → 决定了树的宽度
  • 递归深度 → 决定了树的深度

常见应用场景:

  • 组合问题:N 个数中找出 K 个数的集合
  • 切割问题:字符串的切割方式
  • 子集问题:求所有满足条件的子集
  • 排列问题:生成所有排列
  • 棋盘问题:N 皇后、数独等

回溯法模板

和递归类似,回溯也可以总结为“三部曲”:

  1. 回溯函数的参数与返回值

    • 返回值一般为空,结果保存到全局变量中
    • 参数根据题目不同,可能包括目标值、集合、起始索引等
  2. 递归终止条件

    • 每次递归对应树的一层,总会有终止条件
    • 一般是“找到一个符合条件的解” → 保存结果并返回
if condition:result.append(path.copy())return
  1. 单层递归逻辑

    • 横向:for 循环遍历当前层的可选元素
    • 纵向:进入递归,继续深入
    • 回退:撤销选择,恢复现场
for 选择本层集合中的元素:做出选择backtracking()撤销选择

完整模板:

def backtracking(参数):if condition:result.append(path.copy())returnfor 选择本层集合中的元素:path.append(元素)      # 做出选择backtracking(...)      # 递归path.pop()             # 撤销选择

二、组合类问题的解题思路

LeetCode 常见题目:

组合 77
组合总和 III 216
电话号码的字母组合 17
组合总和 39
组合总和 II 40

我们以 LeetCode 77. 组合 为例,来说明组合类问题的分析思路。

题目:给定两个整数 nk,返回 1 ... n 中所有可能的 k 个数组合。
示例:输入 n=4, k=2,输出:
[[2,4], [3,4], [2,3], [1,2], [1,3], [1,4]]


树形结构分析

我们可以将组合问题抽象为树形结构,每层选择一个数字:

组合77回溯过程演示

  • n 控制树的宽度
  • k 控制树的深度
  • 当路径长度等于 k 时,得到一个完整解,保存后结束递归

回溯三部曲应用

  • 参数与返回值
    参数包括 nk,以及 startIdx,用于限制下层循环的起始位置,避免重复组合。
    返回值为空,结果存入全局变量 result,当前路径用 path 保存。

  • 递归终止条件

if len(path) == k:result.append(path.copy())return
  • 单层搜索逻辑
    在当前层,从 startIdx 开始遍历所有候选数,依次加入路径,递归下探,再回溯撤销。
for i in range(startIdx, n+1):  # 横向遍历path.append(i)              # 做出选择backtracking(n, k, i+1)     # 递归path.pop()                  # 撤销选择

三、具体代码实现

1. 组合 77

有了以上的思路铺垫,给出组合 77的实现代码
剪枝前:

class Solution:def backtracking(self, n: int, k: int, startIdx: int) -> None:if len(self.path) == k:self.result.append(self.path.copy())returnfor i in range(startIdx, n):self.path.append(i+1)self.backtracking(n, k, i+1)self.path.pop()def combine(self, n: int, k: int) -> List[List[int]]:self.result = []self.path = []self.backtracking(n, k, 0)return self.result

剪枝后:

如果剩余可选数字数量不足以补齐 k,则提前终止搜索:

class Solution:def backtracking(self, n: int, k: int, startIdx: int) -> None:if len(self.path) == k:self.result.append(self.path.copy())returnfor i in range(startIdx, n - (k - len(self.path)) + 1):  # 剪枝self.path.append(i+1)self.backtracking(n, k, i+1)self.path.pop()def combine(self, n: int, k: int) -> List[List[int]]:self.result = []self.path = []self.backtracking(n, k, 0)return self.result

2. 组合总和 III 216

题目:找出所有相加之和为 n 的 k 个数组合。只能用 1-9 的数字,每个数字最多用一次。

示例:

  • 输入:k=3, n=7 → 输出 [[1,2,4]]
  • 输入:k=3, n=9 → 输出 [[1,2,6], [1,3,5], [2,3,4]]

思路

  • 参数:目标和 n,组合长度 k,树层起始索引 startIdx
  • 终止条件:路径长度 = k 时检查和是否等于 n
  • 单层搜索:从 startIdx 开始遍历 1-9,选一个数加入路径,再递归

代码实现:

class Solution:def combinationSum3(self, k: int, n: int) -> List[List[int]]:self.path = []self.result = []self.backtracking(k, n, 1)return self.resultdef backtracking(self, k: int, n: int, startIdx: int) -> None:if len(self.path) == k:if n == 0:self.result.append(self.path.copy())returnendIdx = min(n + 1, 10)  # 剪枝:数字不会超过 n 或 9for i in range(startIdx, endIdx):if n - i < 0:  # 剪枝:和已超出目标breakself.path.append(i)self.backtracking(k, n - i, i + 1)self.path.pop()

3. 电话号码的字母组合 17

题目:给定数字 2-9,返回所有可能的字母组合。

映射关系:

2 -> abc
3 -> def
4 -> ghi
5 -> jkl
6 -> mno
7 -> pqrs
8 -> tuv
9 -> wxyz

思路

  • 参数:数字字符串 digits,当前递归深度 index
  • 终止条件:当 index == len(digits),收集路径
  • 单层搜索:取出当前数字对应的字母串,依次尝试

代码实现:

class Solution:letterMap = {'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl','6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'}def backtracking(self, digits: str, index: int) -> None:if index == len(digits):self.result.append(''.join(self.path))returnletters = self.letterMap[digits[index]]for ch in letters:self.path.append(ch)self.backtracking(digits, index + 1)self.path.pop()def letterCombinations(self, digits: str) -> List[str]:self.path = []self.result = []if digits:self.backtracking(digits, 0)return self.result

4. 组合总和 39

题目:在 candidates 中找到和为 target 的所有组合,数字可重复使用。

示例:
输入:candidates = [2,3,6,7], target = 7
输出:[[7], [2,2,3]]

思路

  • 元素可以重复使用,所以递归时仍然传入当前索引 i
  • 剪枝:如果当前和超过 target,直接返回
  • 搜索时要先排序,以便提前终止循环

代码实现:

class Solution:def backtracking(self, candidates: List[int], target: int, startIdx: int, sum: int) -> None:if sum == target:self.result.append(self.path.copy())returnfor i in range(startIdx, len(candidates)):if sum + candidates[i] > target:  # 剪枝breakself.path.append(candidates[i])self.backtracking(candidates, target, i, sum + candidates[i])self.path.pop()def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:self.path = []self.result = []candidates.sort()self.backtracking(candidates, target, 0, 0)return self.result

5. 组合总和 II 40

题目:在 candidates 中找到和为 target 的所有组合,每个数字只能使用一次,数组可能有重复。

示例:
输入:candidates = [10,1,2,7,6,1,5], target = 8
输出:[[1,7], [1,2,5], [2,6], [1,1,6]]

组合总和2去重说明

思路

  • 每个数字只能用一次 → 递归时传 i+1
  • 数组可能重复 → 在同一层中对相同元素去重
  • 去重方式:排序 + used 数组,若前一个相同元素未被使用,则跳过当前元素

代码实现:

class Solution:def backtracking(self, candidates: List[int], target: int, startIdx: int, sum: int, used: List[bool]) -> None:if sum == target:self.result.append(self.path.copy())returnfor i in range(startIdx, len(candidates)):if sum + candidates[i] > target:  # 剪枝breakif i > 0 and candidates[i] == candidates[i-1] and not used[i-1]:continue  # 同层去重self.path.append(candidates[i])used[i] = Trueself.backtracking(candidates, target, i+1, sum+candidates[i], used)self.path.pop()used[i] = Falsedef combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:used = [False]*len(candidates)self.path = []self.result = []candidates.sort()self.backtracking(candidates, target, 0, 0, used)return self.result

四,小结

以上五道题目本质上都属于 回溯的组合类问题,核心逻辑都是:

  • 用一个 path 保存当前组合
  • 用 result 收集所有解
  • 通过递归 + 回溯遍历解空间

它们的主要区别在于 约束条件 的不同:77. 组合:从 1..n 中选 k 个数,无和限制 216. 组合总和 III:固定选 k 个数,且和为 n,数字范围 1..9 17. 电话号码字母组合:典型的多叉树遍历,层数由输入长度决定 39. 组合总和:可重复使用数组中的元素,求和为 target 40. 组合总和 II:与 39 类似,但每个数只能用一次,并需要处理数组中的重复元素

可以看到,这些题目的解法都是 在统一回溯框架下,稍作改动即可适配:

  • 通过 剪枝 提高效率
  • 通过 索引控制 或 used 数组 来避免重复
  • 通过 限制路径和 或 路径长度 来满足题目条件
http://www.sczhlp.com/news/55224/

相关文章:

  • 架构师的悟道2:抽象高度 - 码客
  • 做钢材的做什么网站效果好泊头网站建设的有哪些
  • 网站建设网站排行百度快速收录在线提交工具
  • wordpress手机主题下载南平网站建设wzjseo
  • 手机网站对企业用户的好处电子商务公共服务网
  • 网站开发岗位群英语卷子哪个网站可以做
  • 网站注册费用俄文网站设计
  • 四川成都现在可以去吗系统优化的知识
  • 南通建网站的公司高端网站建设 房产
  • 网站上做推广山西制作网站
  • 南宁在百度上建网站网站链接提交收录
  • 网站开发合同注意wordpress网页排版
  • 教你做面食的网站中医协会网站建设方案
  • 拓扑复习 | 25 道路连通 知识点+习题
  • Visual Studio 工具栏不显示 DevExpress 组件解决办法
  • 云网站 深圳部门网站建设的工作领导小组
  • 冒险岛钓鱼网站做啥用虚拟主机管理系统源码
  • 腾讯云服务器centos做静态网站wordpress网站导出
  • 东莞企业网站推广技巧wordpress 文章 打赏
  • 上海网站建设 乐云seo长沙网络推广外包
  • 网站html代码Wordpress标签与分类
  • 民宿网站建设问卷调查wordpress分页页面
  • 电影网站cpa怎么做找客户信息的软件
  • 杭州网站建设有限公司做微商有什么好的货源网站
  • 专业的网站建设多少钱广宁网站建设
  • 网页建立网站平台如何设置淘宝友情链接
  • 深圳网站制作建设公司网站优化首页付款
  • 个人音乐网站策划书范文海外推广怎么做
  • 高效的shell
  • 视野修炼-技术周刊第125期 | nano-banana