从 LeetCode 78 子集出发,真正理解回溯:当前操作、子问题与下一个子问题
大家好我是程序员无隅第一次写回溯时我们很容易把注意力放在append()、递归调用和pop()上最后记住了一段模板却不知道为什么递归参数是i也不知道下一层为什么传i 1或j 1。其实写回溯最重要的并不是先背模板而是先把递归函数的含义说清楚。只要能回答“当前做什么、当前还要解决什么、做完以后还剩什么”这三个问题代码通常只是把这条逻辑翻译出来。本文从 LeetCode 78「子集」出发分别使用“选或不选”和“枚举选哪个”两种写法理解回溯最后再把同样的分析方法迁移到 LeetCode 131「分割回文串」。一、为什么写回溯前要先回答“三问”假设我们正在逐位构造一个字符串path[i]表示答案的第i个位置。这时可以先回答当前操作是什么枚举一个合法字母填入path[i]。dfs(i)解决什么子问题在前i个位置已经确定的基础上从第i位开始继续构造字符串。做完选择后下一个子问题是什么第i位已经填好调用dfs(i 1)继续构造第i 1位及后面的部分。这三问分别对应回溯代码里的三件事forchinchoices:# 枚举当前选择path.append(ch)# 执行当前选择dfs(i1)# 解决下一个子问题path.pop()# 撤销当前选择这里真正决定递归写法的不是append()和pop()而是我们对dfs(i)的定义。如果把dfs(i)定义成“处理第i个输入元素”下一层通常是dfs(i 1)如果把它定义成“从下标i开始枚举下一个要选的元素”那么选择nums[j]后下一层就应该是dfs(j 1)。递归参数不是凭感觉传递的它必须与递归函数的语义保持一致。二、回溯算法的本质在决策树上构造答案回溯可以理解为在一棵决策树上进行深度优先搜索。树上的一个节点表示当前已经完成了一部分选择。从一个节点走向子节点表示做出一次新选择。到达满足条件的节点时把当前路径收集为答案。返回父节点前撤销刚才的选择再尝试其他分支。因此写回溯时需要先确定两个核心对象。1.path已经完成了哪些选择path保存从根节点走到当前节点的选择结果。例如在子集问题中path [1, 3]表示当前已经选择了数字1和3。它不是“接下来还要做什么”而是已经做完的选择所形成的局部答案。2.dfs(状态)接下来还要解决什么问题递归参数描述剩余问题。例如dfs(i)可以定义为在当前path的基础上从下标i开始继续构造后面的答案。于是一次标准回溯过程就是path.append(choice)# 做选择dfs(next_state)# 解决选择之后的剩余问题path.pop()# 恢复到选择之前的状态pop()并不是为了“删除错误答案”。它的作用是恢复现场让同一个path可以继续表示父节点的状态随后尝试另一种选择。因此回溯的核心链路可以概括为当前状态 → 枚举一个合法选择 → 修改 path → 进入下一个状态 → 恢复 path。三、LeetCode 78用两种视角生成所有子集给定一个不含重复元素的整数数组nums返回它的所有子集。以nums [1, 2, 3]为例答案包括[] [1] [2] [3] [1,2] [1,3] [2,3] [1,2,3]这道题有两种经典回溯写法。它们没有改变问题本身只是观察决策树的角度不同。3.1 方法一站在输入角度选或不选站在输入数组的角度我们依次询问每个元素nums[i]要不要进入当前子集每个元素只有两种状态不选nums[i]选择nums[i]回溯三问当前操作是什么决定nums[i]选还是不选。当前子问题是什么dfs(i)表示在前i个元素已经决定完的基础上继续决定下标i及后面的元素。下一个子问题是什么无论是否选择nums[i]它都已经被处理完所以下一层都是dfs(i 1)。代码defsubsets(nums):ans[]path[]nlen(nums)defdfs(i):ifin:ans.append(path.copy())return# 不选 nums[i]dfs(i1)# 选择 nums[i]path.append(nums[i])dfs(i1)path.pop()dfs(0)returnans这棵搜索树一共有n层每一层处理一个输入元素。只有到达i n时才说明所有元素的“选或不选”都已经决定完因此在叶子节点收集答案。这里的path.copy()不能省略。path在整个搜索过程中会不断修改如果直接保存path答案数组中的多个位置将引用同一个列表后续回溯会一起改变它们。3.2 方法二站在答案角度枚举下一个选谁换一个角度不再逐个询问输入元素而是直接考虑当前要往path里放哪个数如果当前允许从下标i开始选择那么可以枚举j i, i 1, ..., n - 1把nums[j]作为答案中的下一个元素。回溯三问当前操作是什么从当前允许选择的范围[i, n)中枚举一个下标j把nums[j]加入path。当前子问题是什么dfs(i)表示在当前已经选好若干数字的基础上从下标i开始继续枚举下一个要选择的数字。下一个子问题是什么如果选择了nums[j]下一次只能从j 1开始继续选择因此调用dfs(j 1)。代码defsubsets(nums):ans[]path[]nlen(nums)defdfs(i):# 当前 path 本身就是一个合法子集ans.append(path.copy())forjinrange(i,n):path.append(nums[j])dfs(j1)path.pop()dfs(0)returnans注意这里的下一层是dfs(j 1)不是dfs(i 1)。因为i只表示本层允许选择的起始位置真正被选中的是nums[j]。选择完成后需要越过下标j下一层才能保证下标严格递增。3.3 为什么不会遗漏也不会产生重复子集假设某个子集选中的下标是i₁ i₂ i₃第二种写法会依次选择i₁ → i₂ → i₃任何一个子集都能把元素按照原数组下标从小到大排列因此它一定对应搜索树中的一条路径。这说明不会遗漏。同时递归只允许从当前下标之后继续选择所以下标不能回头。[1, 3]只能通过“先选择1再选择3”得到不可能再通过“先选择3再选择1”生成一次。这说明不会重复。递增下标同时建立了完整性和唯一性每个子集都对应唯一的一条递增下标序列。3.4 两种方法到底有什么区别第一种方法站在输入角度当前元素选不选它的递归深度固定为n每个叶子节点对应一种完整的选或不选方案。第二种方法站在答案角度当前答案的下一个元素选谁它的答案长度不固定每进入一个递归节点当前path就已经代表一个合法子集因此可以立即收集。两种写法都会生成2^n个子集。复制每个子集还需要与其长度成正比因此时间复杂度O(n × 2^n)递归栈与路径空间O(n)如果计算返回结果本身占用的空间O(n × 2^n)四、从子集迁移到 LeetCode 131 分割回文串LeetCode 131 要求把字符串分割成若干个回文子串并返回所有合法分割方案。例如s aab合法答案为[a, a, b] [aa, b]这道题与子集很像字符串中的切割位置同样可以看成一系列选择。它也有两种观察角度。4.1 方法一判断当前位置切不切站在输入位置的角度依次判断每个字符后面是否切一刀。回溯三问当前操作是什么判断位置i后面是否切割。不切当前子串继续向后延长。切取出s[start:i 1]只有它是回文串才能加入path。当前子问题是什么dfs(i, start)表示当前正在检查位置i未完成子串从start开始继续决定后面的切割方式。下一个子问题是什么不切时当前子串起点不变进入dfs(i 1, start)。切割时下一段从i 1开始进入dfs(i 1, i 1)。defpartition(s):ans[]path[]nlen(s)defis_palindrome(left,right):whileleftright:ifs[left]!s[right]:returnFalseleft1right-1returnTruedefdfs(i,start):ifin:ifstartn:ans.append(path.copy())return# 当前位置后面不切继续延长当前子串ifin-1:dfs(i1,start)# 当前位置后面切一刀ifis_palindrome(start,i):path.append(s[start:i1])dfs(i1,i1)path.pop()dfs(0,0)returnans一句话记忆依次判断每个字符后面切不切切出来的部分必须是回文串。4.2 方法二枚举下一段在哪里结束站在答案的角度我们不再判断每个位置“切不切”而是直接枚举下一段回文串的结束位置。回溯三问当前操作是什么从尚未分割的第一个字符i开始枚举结束位置j。如果s[i:j 1]是回文串就把它加入path。当前子问题是什么dfs(i)表示前i个字符已经分割完成从下标i开始继续分割剩余字符串。下一个子问题是什么选择s[i:j 1]后这一段已经完成下一次从j 1开始因此调用dfs(j 1)。defpartition(s):ans[]path[]nlen(s)defis_palindrome(left,right):whileleftright:ifs[left]!s[right]:returnFalseleft1right-1returnTruedefdfs(i):ifin:ans.append(path.copy())returnforjinrange(i,n):ifnotis_palindrome(i,j):continuepath.append(s[i:j1])dfs(j1)path.pop()dfs(0)returnans一句话记忆每次枚举下一段回文串选多长选完以后继续分割剩余字符串。4.3 为什么子集可以立即收集回文分割却不行在子集的“枚举选哪个”写法中即使后面还有数字没有选择当前path也已经是一个完整、合法的子集。例如nums [1, 2, 3] path [1][1]本身就是答案不需要等到所有数字都处理完因此进入dfs时就可以记录。而在回文分割中s aab path [aa]此时字符b还没有被分割[aa]只是一个半成品。只有递归位置到达n说明整个字符串都被若干回文子串覆盖当前path才是完整答案。所以答案何时加入ans不能靠背模板判断。应该先问当前 path 是否已经满足题目对一个完整答案的全部要求五、一套可复用的回溯分析方法遇到新的回溯题可以按照下面的顺序分析。第一步确定path表示什么先问自己当前已经做了哪些选择在子集问题中path是已经选中的数字在分割回文串中path是已经确定的回文子串。第二步定义dfs(状态)不要只写一个模糊的“dfs用来回溯”。需要把剩余问题说完整。例如dfs(i)在当前 path 的基础上从下标 i 开始继续枚举后面的选择。定义清楚以后递归参数如何变化通常也会随之确定。第三步回答回溯三问当前操作是什么当前子问题是什么做完选择后下一个子问题是什么如果第三问无法回答就说明递归函数的定义还不够清楚。第四步确定什么时候得到完整答案结束条件不是统一的i n收集答案的位置也不一定总在叶子节点。子集的“选或不选”写法所有元素都决定完时收集。子集的“枚举选哪个”写法每个节点的path都是合法子集进入递归就收集。分割回文串只有整个字符串都被分割完时收集。结束条件取决于题目如何定义一个完整答案而不是取决于模板长什么样。第五步枚举选择递归再恢复现场最后才把前面的分析翻译成代码defdfs(state):if当前已经构造出完整答案:ans.append(path.copy())returnforchoicein当前所有合法选择:path.append(choice)dfs(next_state)path.pop()这段代码只是一个结构提示并不是所有回溯题都要机械套用。真正需要记住的是path描述已经完成的选择dfs(状态)描述尚未解决的问题每次递归只做一个当前选择然后把剩余问题交给下一层。回到子集问题“选或不选”是在遍历输入元素每层决定一个元素的状态。“枚举选哪个”是在构造答案每层决定答案中的下一个元素。当你能准确说出自己站在哪个角度、当前做什么、下一层还剩什么时回溯就不再是一段需要死记硬背的模板而是一条可以一步步推导出来的决策链。

相关新闻

开源项目wvp-gb28181-pro怎么设置生产环境的IP

开源项目wvp-gb28181-pro怎么设置生产环境的IP

对照开源 WVP-PRO 官方配置语义,生产环境最合理的原则是:按“谁访问谁”拆 IP,不要一个公网 IP 打天下。你们现在生产更接近“能用,但不完全按官方最优拆分”。 如果是公网摄像头 公网网页观看 WVP/ZLM 同机或同内网&#xff0c…

2026/7/29 2:16:17阅读更多 →
从 Kimi K3 看 CoT、Skill 与模型垂直化

从 Kimi K3 看 CoT、Skill 与模型垂直化

从 Design Arena 的报告说起 Design Arena 发布了针对 Kimi K3 的分析报告,在其发布时的单次前端生成评测中,K3 以 1392 Elo 位列第一;其推理 token 约为 Claude Opus 4.8 的 12 倍、Kimi K2.6 的两倍以上。 更关键的是,K3 的推…

2026/7/29 2:16:17阅读更多 →
Linux内核6.x版本关键特性回顾:eBPF、io_uring与Rust支持的里程碑

Linux内核6.x版本关键特性回顾:eBPF、io_uring与Rust支持的里程碑

Linux内核6.x版本关键特性回顾:eBPF、io_uring与Rust支持的里程碑一、6.x时代的战略转折:从"稳定"到"进化" Linux 内核 6.x(6.0 至 6.12)延续了更务实的版本号策略。Linus Torvalds 在 6.0 发布时说&#xff…

2026/7/29 2:14:16阅读更多 →
Java并发工具类实战:从原理到电商秒杀优化

Java并发工具类实战:从原理到电商秒杀优化

1. Java并发工具类全景解析在Java生态中,并发编程能力是区分初级与中高级开发者的关键指标。java.util.concurrent包提供的工具类,远比简单的synchronized和volatile更适应现代多核处理器架构。我在电商秒杀系统开发中,曾通过合理选用并发工具…

2026/7/29 3:28:31阅读更多 →
【GitHub Copilot Harness工作流技术解析】用单一工具完成原型、规划、实现与代码审查

【GitHub Copilot Harness工作流技术解析】用单一工具完成原型、规划、实现与代码审查

文章目录GitHub Copilot Harness工作流技术解析:用单一工具完成原型、规划、实现与代码审查一、引言二、Harness 到底是什么2.1 模型只是大脑,Harness 才负责把工作做完2.2 一套 Harness,多种使用入口三、八步工作流:从想法到可提…

2026/7/29 3:28:31阅读更多 →
5步掌握Obsidian加密技巧:终极指南保护你的敏感笔记内容

5步掌握Obsidian加密技巧:终极指南保护你的敏感笔记内容

5步掌握Obsidian加密技巧:终极指南保护你的敏感笔记内容 【免费下载链接】obsidian-encrypt Hide secrets in your Obsidian.md vault 项目地址: https://gitcode.com/gh_mirrors/ob/obsidian-encrypt 在数字笔记时代,隐私保护已成为知识工作者的…

2026/7/29 3:28:31阅读更多 →
从生产文员到计划员:2026年转型必备技能与学习路径全解析

从生产文员到计划员:2026年转型必备技能与学习路径全解析

从生产文员转向计划员,是许多制造业从业者实现职业跃升的经典路径。这个转变意味着从“执行记录”走向“统筹规划”,工作视角从局部转向全局,职业发展空间也随之打开。如果你正站在这个转型的十字路口,不知道从何学起,…

2026/7/29 3:28:31阅读更多 →
FastAPI + Tortoise-ORM 企业端实战:企业认证、信息保存、审核与登录的设计与实现

FastAPI + Tortoise-ORM 企业端实战:企业认证、信息保存、审核与登录的设计与实现

FastAPI Tortoise-ORM 企业端实战:企业认证、信息保存、审核与登录的设计与实现一、前言介绍1.1 项目背景1.2 功能概览1.3 数据模型总览二、环境准备2.1 依赖库清单2.2 数据库与缓存配置要点2.3 路由与目录结构三、知识点讲解3.1 企业域四表拆分与 enterprise_id 关…

2026/7/29 3:28:31阅读更多 →
Maven Surefire与Failsafe插件实现单元测试与集成测试分离的完整指南

Maven Surefire与Failsafe插件实现单元测试与集成测试分离的完整指南

1. 项目概述:为什么我们需要分离单元测试与集成测试?在Java后端开发中,测试是保证代码质量的基石。但很多团队,尤其是项目初期,往往会把所有测试一股脑地扔给Maven Surefire插件去执行。这会导致一个典型问题&#xff…

2026/7/29 3:26:31阅读更多 →
覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

🔹 工具基础介绍 OpenClaw 是开源生态中一款实用性较强的本地智能工具,凭借本地离线运行、可视化图形操作和任务自动化三大核心特性,赢得了众多用户的青睐。与普通在线对话AI工具不同,它属于能够直接操控本机软硬件的智能数字员工…

2026/7/28 4:06:39阅读更多 →
伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

所谓液压伺服阀体的精密激光焊接,是用激光束对阀座壳体(通常为不锈钢或铝合金)进行密封焊接,使阀体在21-35MPa的高压液压油或压缩气体中长期运行而不发生介质泄漏。液压伺服阀是高端液压系统的"大脑"。从航空航天飞行控…

2026/7/28 2:08:06阅读更多 →
D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南 【免费下载链接】d2dx D2DX is a complete solution to make Diablo II run well on modern PCs, with high fps and better resolutions. 项目地址: https://gitcode.com/gh_mirrors/d2/d2dx 你是否还在…

2026/7/28 1:38:28阅读更多 →
28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“!

28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“!

28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“! 在构建复杂的 Agent 系统时,我们经常会遇到这样的场景:Agent 正在执行一个多步骤的任务,比如“下单购买商品”,但执行到一半时,我们…

2026/7/29 0:01:46阅读更多 →
自律同行,突破无界!NANK南卡正式官宣曾舜晞成为品牌代言人

自律同行,突破无界!NANK南卡正式官宣曾舜晞成为品牌代言人

近日,国际专注开放式技术研发的声学品牌Nank南卡,正式官宣实力艺人曾舜晞担任品牌代言人。消息一经发出便轰动全网。为什么耳机品牌不选择流量明星、老牌歌手?而且是选择曾舜晞?让我们一起来探索一下!比起短期的流量&a…

2026/7/29 0:01:46阅读更多 →
【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

一、本文介绍 🔥本文在RT-DETR多模态融合目标检测中引入RLAB残差线性注意力模块,可在不同模态特征交互阶段进行多次残差细化,使可见光、红外等特征在尺度、语义和空间位置上更好对齐;随后将细化特征与解码器输出拼接并生成Q、K、V,通过线性注意力自适应强化关键通道、目…

2026/7/29 0:01:46阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/28 20:22:24阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/28 3:17:03阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/28 2:35:58阅读更多 →