惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Hackread – Cybersecurity News, Data Breaches, AI and More
C
Check Point Blog
Hacker News: Ask HN
Hacker News: Ask HN
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
WordPress大学
WordPress大学
P
Proofpoint News Feed
V
Visual Studio Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
N
Netflix TechBlog - Medium
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 聂微东
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 叶小钗
Cisco Talos Blog
Cisco Talos Blog
S
Schneier on Security
T
Threat Research - Cisco Blogs
腾讯CDC
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Hacker News
The Hacker News
Google DeepMind News
Google DeepMind News
Microsoft Security Blog
Microsoft Security Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
GbyAI
GbyAI
N
News | PayPal Newsroom
L
LINUX DO - 最新话题
酷 壳 – CoolShell
酷 壳 – CoolShell
P
Palo Alto Networks Blog
T
Tenable Blog
S
Secure Thoughts
T
Threatpost
V2EX - 技术
V2EX - 技术
大猫的无限游戏
大猫的无限游戏
Martin Fowler
Martin Fowler
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Vercel News
Vercel News
罗磊的独立博客
P
Privacy & Cybersecurity Law Blog
Engineering at Meta
Engineering at Meta
小众软件
小众软件
Google DeepMind News
Google DeepMind News
N
News and Events Feed by Topic
Y
Y Combinator Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Cybersecurity and Infrastructure Security Agency CISA
P
Proofpoint News Feed
L
Lohrmann on Cybersecurity
P
Privacy International News Feed
H
Heimdal Security Blog
量子位
B
Blog

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12) Snack JSONPath 项目架构分析 Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度 AI新时代下的图床管理方案-Cloudflare图床+MCP+Skills方案指南 化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量 从零实现富文本编辑器#13-React非编辑节点的内容渲染 AI开发-python-langchain框架(3-23-OpenAI Functions风格Tool Calling智能助手) .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut PbootCMS 网站内容数量多导致访问慢?这些实用优化方案帮你提速! - 家兴网络技术工作室 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 网站漏洞怎么发现并修复?一篇实用指南(附完整流程) - 家兴网络技术工作室 开了 TUN 模式还是直连?90% 的人都踩过这个坑 Github日报|2026年04月12日 - AI一族 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术 Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模 MicroPython LVGL基础知识和概念:底层渲染与性能优化 - FreakStudio 数据库草图算法 Python 潮流周刊#146:CPython 引入 Rust 的进展 - 豌豆花下猫 最小生成树 - mofei1116 红日靶场七:从外网入口、容器逃逸到 AD 接管的完整利用链复盘 - YouDiscovered1t 分享四款开源且实用的 Kafka 管理工具 - 追逐时光者 vLLM 权重加载机制全解析:从挑战到理想架构 LCT 学习笔记 - ACehomoxue Avalonia UI 12.0.0 正式发布:架构演进和性能飞跃 - 张善友 当 AI Agent 把调用链拉长,延迟开始成为一门生意 conhost.exe 无法显示 U+2717 - 145a 太秀了,我把自己蒸馏成了 Skill!已开源 - 程序员鱼皮 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 基于 Ghostty 带有分割标签页和为 Claude 编程设计的通知终端 - BugShare AI 焊死入口:教育的“操作系统级”重塑 - 郝hai 初级Java开发工程师使用sql脚本编写代码的过程是简单而且不糊涂 - CoderOilStation Claude Code通关手册(六):MCP协议完全指南 - 暮色之狐 边框灯光环绕动画特效实现指南 - Newbe36524 开源:子木蒸馏版的 SEO 审计工具 seo-audit-skill v1.0 我所理解的Python元模型 【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite - 程序员Seven Claude 和 Codex 在审计 Skill 上性能差异探究 - ACai_sec AScript如何实现中文脚本引擎 - rockey627 【渗透测试】HTB Season10 Garfield 全过程wp - dynasty_chenzi Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 树状数组正确性证明 - AC-wyr 你的 AI 焦虑,可能比 AI 本身更危险——ATM 机没有消灭银行柜员,但恐慌消灭了你的判断力 - 我没有三颗心脏 一个拉胯的分库分表方案有多绝望?整个部门都在救火! - 冰河团队 动态规划入门必学之走方格问题 - Ofnoname PostgREST 与 PostgreSQL 角色权限配置全解析(生产级实践) - SheepDog1998 使用 UEFI 图形输出协议 GOP 在屏幕上显示图像的方法 - 阿源- Claude Code通关手册(五):组建你的AI专家团队,子代理系统 - 暮色之狐 一个程序员到架构师的催婚路之感悟(整整10年后的催婚相亲感悟) - MisterLip 用 Agent Skill 自动生成工作周报 - 赵康
从回溯到分支限界:重新理解搜索、剪枝与最优性证明
Ofnoname · 2026-05-10 · via 博客园_首页

很多程序员第一次接触“搜索”,是在 LeetCode 上的全排列、组合、子集、N 皇后、数独、单词搜索这些题里。这些题做多了以后,我们很容易形成一套肌肉记忆:递归、选择、撤销选择、剪枝。

但搜索算法真正重要的地方,并不在递归本身。

递归只是遍历方式。搜索真正关心的是:如何把一个巨大的候选空间组织成一棵隐式的树;如何在树还没有完全展开时,就证明某些分支不可能产生答案;如果问题要求最优解,又如何证明某些分支不可能比当前答案更好。

换句话说,搜索算法的核心不是“把所有可能性试一遍”,而是“尽早证明哪些可能性不必再试”。

这篇文章讨论的不是回溯模板,而是模板背后的结构:解向量、解空间树、部分解、死节点、限界函数,以及一个最重要的问题:剪枝为什么不会漏掉正确答案

从暴力枚举到隐式解空间树

很多组合问题看起来都绕不开穷举。

比如图着色问题中,给定一个无向图 \(G=(V,E)\),如果每个顶点都可以染成 \(1,2,3\) 三种颜色之一,那么对 \(n\) 个顶点来说,最朴素的候选空间大小是:

\[3^n \]

其中大多数染色方案并不合法,因为相邻顶点可能颜色相同。

如果完全无组织地枚举,搜索很快会失控。搜索算法所做的第一件事,就是把这些候选方案组织起来。很多搜索问题都可以写成一个解向量:

\[X=(x_1,x_2,\dots,x_n) \]

其中每个分量 \(x_i\) 来自一个有限集合 \(S_i\)。完整搜索空间就是:

\[S_1 \times S_2 \times \cdots \times S_n \]

这句话看起来有些数学化,但它其实就是我们平时写搜索代码时面对的对象。

例如:

  • 全排列问题中,\(x_i\) 表示第 \(i\) 个位置放哪个元素;
  • N 皇后问题中,\(x_i\) 可以表示第 \(i\) 行皇后所在的列;
  • 3-coloring 中,\(x_i\) 表示第 \(i\) 个顶点的颜色;
  • 0/1 背包中,\(x_i \in {0,1}\) 表示是否选择第 \(i\) 个物品。

一旦把问题写成连续决策,它就自然形成了一棵树:

  • 根节点表示空向量 \(()\)
  • \(k\) 层节点表示一个长度为 \(k\) 的前缀 \((x_1,\dots,x_k)\)
  • 从一个节点扩展子节点,就是给 \(x_{k+1}\) 选择一个候选值;
  • 叶子节点对应完整解向量。

这棵树通常不会被显式构造出来。它存在于递归调用栈、队列、优先队列、状态变量或搜索框架里。因此它也常被称为隐式解空间树。从这个角度看,搜索代码并不是“递归魔法”。它只是用某种顺序遍历这棵隐式树。

一个节点代表的不是一个答案,而是一整棵子树

真正决定搜索效率的,往往不是完整解,而是部分解。一个长度为 \(k\) 的前缀:

\[(x_1,x_2,\dots,x_k) \]

代表的不只是当前这 \(k\) 个选择,而是它下面所有可能的补全。也就是说,一个搜索节点代表的是一整棵子树。例如在 N 皇后中,前 \(k\) 行皇后的列号确定以后,这个节点下面包含了所有可能的后 \(n-k\) 行摆法。剪掉一个节点,等于同时放弃它下面的所有补全。

这件事很危险,也很强大。如果剪错了,就可能漏掉正确答案。如果剪得安全,一次判断就能砍掉指数级数量的候选方案。

所以每个安全剪枝背后,都应该有一个证明。

在回溯法中,我们通常证明:

\[\text{当前部分解违反约束} \Rightarrow \text{任意扩展都不可能合法} \]

在分支限界法中,我们通常证明:

\[\text{当前子树的最好可能性不优于已知答案} \Rightarrow \text{任意扩展都不可能成为最优解} \]

如果一个判断只能说“这样大概率不会错”,那它是启发式;如果它能说“这样一定不会漏解”,它才是精确搜索中的安全剪枝。

这也是搜索算法最重要的思想:剪枝不是技巧,而是一种证明。

02_partial_final_dead_nodes

部分解、死节点和 promising node

在解空间树中,一个节点通常可以被分成几类。

  • 第一类是完整解。它已经给所有变量赋值,并且满足问题约束。

  • 第二类是仍有希望的部分解。它还不是完整解,但目前没有足够理由否定它。

  • 第三类是死节点。它已经不可能扩展成合法解,或者不可能扩展成更优解。

当然,“当前没有违反约束”不一定等于“未来一定可以扩展成完整解”。例如图着色中,一个部分染色可能暂时没有任何已染色边冲突,但某个未染色顶点的所有颜色都已经被邻居占用。此时它虽然没有出现显式冲突,却已经不可能扩展成完整合法染色。没有被剪掉的节点,只表示“暂时不能证明它失败”;被剪掉的节点,则必须能证明“它一定失败”

回溯法中的 is_validis_promisingcheck 这些函数,本质上都是在回答同一个问题:这个节点下面,还有没有继续搜索的必要?

回溯法:用约束剪掉不可能的解

回溯法通常以深度优先方式遍历解空间树。它从空向量开始,依次给 \(x_1,x_2,\dots\) 赋值。如果当前前缀仍然有希望,就继续深入;如果当前前缀已经被证明不可能成功,就返回上一层,尝试下一个候选值。

程序员熟悉的结构大概是:

search(k):
    if current state is a final solution:
        record it
        return

    for x in candidates of next decision:
        choose x
        if current state is still promising:
            search(k + 1)
        undo x

这里最重要的不是递归,而是 promising 判断。如果没有这个判断,搜索就退化成完整枚举。promising 越能尽早发现失败,搜索树就越小。

搜索顺序不是实现细节

从解空间树的角度看,候选值顺序决定了遍历顺序,变量顺序决定了树的形态。

这会带来几个直接后果:

  • 变量顺序会改变剪枝发生的早晚;
  • 候选值顺序会影响第一个解出现的位置;
  • 剪枝判断越早发生,被砍掉的子树越大;
  • 同一个问题换一种状态表示,可能得到完全不同规模的搜索树。

所以,成熟的搜索算法设计并不是“套模板”,而是先设计一个好的状态空间,再决定如何遍历它。

N 皇后:状态设计本身就是剪枝

N 皇后是最经典的回溯问题之一。最直接但很糟糕的建模方式,是逐格决定棋盘上每个位置是否放皇后。对于一个 \(n \times n\) 的棋盘,这相当于面对接近 \(2^{n^2}\) 的候选空间。

但我们知道,每一行最终必须恰好放一个皇后。于是可以改用一维向量:

\[x_i = \text{第 } i \text{ 行皇后所在的列} \]

这样搜索空间立刻从棋盘格子的子集,变成每行列号的组合,规模变成 \( n^n \)

如果进一步意识到每一列也最多只能放一个皇后,那么还可以把状态空间限制为列的排列,规模进一步变成 \( n! \)

这三个状态空间描述的是同一个问题,但搜索规模完全不同。这说明一个很重要的事实:剪枝不只发生在 if 语句里,也发生在状态定义里。

03_n_queens_state_model

在一维表示中,对于任意两行 \(i\)\(j\),两个皇后冲突当且仅当 \( x_i = x_j \) 或者 \( |x_i-x_j|=|i-j| \)。前者表示同列冲突,后者表示对角线冲突。

当我们只放了前 \(k\) 行皇后时,只要前 \(k\) 行内部已经冲突,就没有必要再考虑第 \(k+1\) 行及之后的任何放法。因为后续选择无法改变已经放下的皇后之间的冲突。

这就是一个典型的回溯剪枝证明:

\[\text{已有皇后互相攻击} \Rightarrow \text{任意扩展都不可能合法} \]

N 皇后的重点是理解:好的状态设计本身就在提前编码约束。

图着色:从冲突检查到更强的提前失败检测

再看 3-coloring。给定无向图 \(G=(V,E)\),要给每个顶点染三种颜色之一,使得任意相邻顶点颜色不同。完整解可以表示为:

\[X=(x_1,x_2,\dots,x_n), \quad x_i \in {1,2,3} \]

没有剪枝时,候选解数量是 \( 3^n \)

最基本的回溯做法是按顺序给顶点染色。对于一个部分染色,只要某两个已经染色的相邻顶点颜色相同,就可以立刻剪枝:

\[\exists (u,v)\in E,\ x_u=x_v \Rightarrow \text{任意扩展都不可能合法} \]

因为后续给其他顶点染色,无法修复已经发生的边冲突。

更强的做法是维护每个未染色顶点仍然可用的颜色集合。给某个顶点染色后,它的邻居就不能再使用这个颜色。如果某个未染色顶点的颜色集合变为空,那么即使当前没有任何已染色边冲突,这个节点也已经不可能扩展成完整解。这叫 forward checking,可以理解为一种更早的失败检测。

例如,某个未染顶点 \(v\) 的三个邻居已经分别使用了颜色 \(1,2,3\)。此时 \(v\) 已经没有任何合法颜色可选。即使当前染色方案还没有直接冲突,也应该立即剪掉。越强的必要条件,越可能提前发现死节点,节省无用的搜索。

变量选择顺序也很重要。如果总是按编号给顶点染色,可能很晚才遇到真正困难的顶点。更好的策略是优先选择可选颜色最少的顶点,也就是常说的 MRV,minimum remaining values。

直觉很简单:越难安排的变量,越应该早点处理。因为如果它注定失败,越早失败,剪掉的子树越大。这就是为什么搜索顺序不只是实现细节。它直接影响剪枝发生的时机。

从可行性剪枝到最优性剪枝

回溯法擅长处理约束满足问题,例如 N 皇后、数独、图着色。它关心的主要问题是:这个部分解还能不能扩展成合法解?

但很多问题不只是要找一个合法解,而是要找最优解。例如 0/1 背包中,合法解有很多个。我们真正关心的是:在容量限制内,总价值最大的是哪一个?这时,仅仅判断一个节点是否合法还不够。一个部分解即使合法,也可能没有继续搜索的价值,因为它下面无论怎么扩展,都不可能超过当前已知最优解。

这就是分支限界法的入口。分支限界法也是一种有组织的穷举。它和回溯法一样在解空间树上搜索,但它多了一个关键问题:这个节点所在的子树,理论上最多能产生多好的解?

对于最大化问题,我们通常给每个节点计算一个上界 upper bound,记作 \(ub\)。它表示:从这个部分解继续扩展,理论上最多能达到多好的目标值。

如果当前已经有一个可行解,目标值为 \(best\),并且某个节点满足:

\[ub \le best \]

那么这个节点下面不可能产生更好的解,可以安全剪掉。

对于最小化问题,逻辑类似,只不过使用的是下界 lower bound。如果某个节点的下界已经不小于当前已知最优值,也可以剪掉。

因此,回溯法和分支限界法的区别可以概括为:

\[\text{回溯法:证明这里不可能有合法解} \]

\[\text{分支限界法:证明这里不可能有更优解} \]

bound 不是猜测,而是正确性的一部分

bound 很像启发式评分,但二者不能混为一谈。

如果某个函数只是用来决定“先搜索哪个节点”,那它可以是启发式的。它估得准,算法就快一些;估得不准,算法就慢一些。

但如果某个函数被用来剪枝,为了保证不漏掉最优解,它就必须满足严格的上界或下界性质。

对于最大化问题,如果我们用 \(ub(s)\) 剪枝,就必须保证:

\[ub(s) \ge \max_{y \in completion(s)} value(y) \]

也就是说,\(ub(s)\) 必须不小于这个节点所有可行补全中的最大价值。

对于最小化问题,如果我们用 \(lb(s)\) 剪枝,就必须保证:

\[lb(s) \le \min_{y \in completion(s)} cost(y) \]

也就是说,\(lb(s)\) 必须不大于这个节点所有可行补全中的最小成本。

这听起来有些绕,但本质很简单:

  • 最大化问题的上界必须足够乐观,不能低估;
  • 最小化问题的下界也必须足够乐观,不能高估。

如果这个性质不成立,剪枝就不安全。算法可能直接丢掉真正的最优解。因此,bound 不是装饰性的性能优化,而是分支限界法正确性的一部分。

0/1 背包:用分数背包构造上界

0/1 背包问题很适合说明分支限界法。给定 \(n\) 个物品,每个物品有重量 \(w_i\) 和价值 \(v_i\),背包容量为 \(W\)。每个物品只能选或不选,因此解向量是:

\[x_i \in {0,1} \]

其中 \(x_i=1\) 表示选择第 \(i\) 个物品。目标是最大化总价值:

\[\max \sum_{i=1}^{n} v_i x_i \]

约束是总重量不超过容量:

\[\sum_{i=1}^{n} w_i x_i \le W \]

06_knapsack_branch_bound_tree

搜索树的第 \(i\) 层对应是否选择第 \(i\) 个物品。如果某个节点已经超重,那么它是不可行节点,可以直接剪掉。这是回溯式的可行性剪枝。

但分支限界法还会进一步问:如果当前还没超重,它有没有可能超过当前最优解?假设当前已经决定了前 \(k\) 个物品,当前重量为 \(w\),当前价值为 \(v\)。最粗糙的上界是:

\[ub = v + \sum_{i=k+1}^{n} v_i \]

这个上界一定安全,因为它假设后面所有物品都能被装进去。但它太乐观了,很多明显不可能超过 \(best\) 的节点也无法被剪掉。更精细的做法,是借用分数背包的思想。

把剩余物品按照单位价值 \(v_i/w_i\) 从高到低排序,然后尽可能装入背包。如果最后一个物品装不下,就允许只装一部分。这会得到一个上界。因为分数背包是 0/1 背包的松弛:它去掉了“物品不可切分”的约束,允许物品被部分选取。放松约束之后,可选方案只会变多,不会变少。因此,分数背包在同一子树中得到的最优值,一定不会小于 0/1 背包的真实最优值。

也就是说:

\[ub \ge \text{当前子树中的真实最优价值} \]

虽然这个 \(ub\) 本身不一定能被 0/1 背包达到,但如果它都不超过当前已知最优值:

\[ub \le best \]

那么这个子树一定不可能产生更优解,可以安全剪掉。

这里有一个重要权衡:

  • 很松的上界也能保证正确性,但剪枝效果差;
  • 很紧的上界能剪掉更多节点,但计算成本更高。

分支限界法的工程设计,往往就在这两者之间寻找平衡。

最大团:回溯和限界合在一起

前面的例子里,N 皇后偏向可行性剪枝,背包偏向最优性剪枝。最大团问题则能很好地展示两者如何结合。

最大团问题是:给定一个无向图,找到最大的顶点集合,使得集合中任意两个顶点之间都有边。

一个搜索状态可以写成:

C: 当前已经选入团的顶点集合
P: 仍然可能加入 C 的候选顶点集合
best: 当前找到的最大团大小

其中 \(C\) 必须始终是一个团。

如果选择一个顶点 \(v \in P\) 加入当前团,那么后续还能加入的顶点,必须同时满足两个条件:

  1. 它原本就在候选集合 \(P\) 中;
  2. 它必须与 \(v\) 相邻。

因此新状态是:

\[C' = C \cup {v} \]

\[P' = P \cap N(v) \]

这里的 \(P \cap N(v)\) 本身就是一种状态级剪枝。它直接排除了所有不可能与 \(v\) 共处同一个团的顶点。

一个简化版搜索可以写成:

expand(C, P):
    if |C| + |P| <= best:
        return

    choose v from P
    expand(C ∪ {v}, P ∩ N(v))
    expand(C, P \ {v})

其中:

\[|C| + |P| \]

是一个朴素上界。因为即使候选集合 \(P\) 中所有顶点都能加入,最终团大小也不会超过 \(|C|+|P|\)

所以如果:

\[|C| + |P| \le best \]

这个节点就可以剪掉。

但这个上界通常很松,因为 \(P\) 中的顶点未必两两相邻。候选点很多,不代表它们都能一起加入团。

更强的做法是对候选子图 \(P\) 做一次贪心染色。

如果 \(P\) 可以被染成 \(k\) 种颜色,那么从 \(P\) 中最多只能选出 \(k\) 个顶点加入团。

原因是:同一种颜色中的顶点两两不相邻,而一个团要求任意两个顶点都相邻。所以同一颜色中最多只能贡献一个顶点。

于是我们得到更强的上界:

\[|C| + coloring_bound(P) \]

如果:

\[|C| + coloring_bound(P) \le best \]

也可以安全剪枝。

这个例子很有代表性。它说明 bound 不一定来自原问题本身,也可以来自另一个更容易计算的结构。这里我们不是在真正求图着色的最优解,而是用一次贪心染色快速得到一个安全的上界。

更重要的是,这个 bound 依然有证明:候选集合中的团大小不可能超过候选子图的染色数。因此,用染色结果给最大团搜索做上界是安全的。

TSP:下界来自必要条件和问题松弛

旅行商问题也是分支限界法中的经典例子。给定 \(n\) 个城市和两两之间的距离,要求每个城市访问一次并回到起点,使总路程最短。

这是一个最小化问题,所以我们需要下界。一个最简单的必要条件是:在最终的 Hamilton 回路中,每个城市的度数都是 2,也就是有一条边进入,一条边离开。因此,对于对称 TSP,可以对每个城市取与它相连的两条最便宜的边,把这些成本加起来再除以 2,得到一个粗下界:

\[lb = \frac{\sum_i (min1_i + min2_i)}{2} \]

08_tsp_lower_bound

更强的下界通常来自问题松弛。例如,对于某个已经固定部分路径的节点,剩余城市最终必须被连接起来。我们可以用剩余城市上的最小生成树成本,再加上从路径端点连接到剩余集合的必要边,构造一个下界。

更经典的做法是 1-tree lower bound:固定一个根城市,对其他城市求最小生成树,再加上根城市关联的两条最便宜边。这个结构比普通 MST 更接近 TSP 回路,因此通常能给出更紧的下界。

TSP 的例子再次说明:bound 往往来自原问题的必要条件或松弛问题。它可以不是一个合法解,但必须是一个安全的乐观估计

待处理节点集合:PT 不一定是优先队列

很多教材在讲分支限界法时,会提到一个待处理节点表,常记为 PT。

需要注意的是,PT 的本质不是优先队列,而是活节点集合:所有还没有被剪掉、也还没有被扩展完的节点,都可以放在里面。

至于下一步扩展哪个节点,是搜索策略的问题。

如果用 FIFO 策略,PT 表现为队列,搜索更接近广度优先。

如果用 LIFO 策略,PT 表现为栈,搜索更接近深度优先。

如果每次选择 bound 最有希望的节点,PT 通常用优先队列实现,搜索就是 best-first 分支限界。

所以,分支限界法的本质不是“必须使用优先队列”,而是:每个节点都有一个界,搜索过程中可以用这个界来剪枝,也可以用它来决定扩展顺序。

一个节点通常需要保存:

partial_solution
level
current_value_or_cost
bound
parent_or_path

这和普通 DFS 很不同。

回溯法沿着递归栈向下走,路径天然保存在调用栈里。best-first 分支限界则可能跳跃式地处理节点:这一步扩展左边子树的某个节点,下一步可能跳到右边子树的另一个节点。

因此,如果最后不仅要得到最优值,还要得到对应的完整解,就需要额外保存路径、父指针,或者在节点中保存足够的状态信息。

最坏复杂度不会消失

回溯法和分支限界法都属于精确搜索方法。它们没有改变很多组合问题的最坏情况本质。如果问题本身具有指数级搜索空间,在最坏情况下,算法仍然可能访问指数级数量的节点。

剪枝改善的是实际搜索规模,而不是魔法般消除复杂性。一个好的剪枝条件,可能在真实数据上把搜索量从天文数字降到可以接受;但在理论最坏情况下,它仍然可能无能为力。

这也是为什么我们需要同时关心两件事:

  • 正确性:剪枝一定不能漏掉答案或最优解;
  • 有效性:剪枝要足够早、足够强、计算成本足够低。

搜索算法的设计,往往就是在这两者之间反复权衡。

和 A*、动态规划、整数规划的关系

理解回溯和分支限界后,很多算法思想会变得更容易联系起来。

A* 搜索也使用估价函数来决定优先扩展哪个节点。它的:

\[f(n)=g(n)+h(n) \]

和分支限界中的 bound 有相似之处。区别在于,A* 通常讨论图搜索和最短路语境,而分支限界更常用于组合优化的解空间树。如果 A* 要保证最优性,启发函数 \(h(n)\) 通常需要满足 admissible,也就是不能高估从当前节点到目标的剩余代价。这和分支限界中“bound 必须安全”的思想是一致的。

动态规划也可以看成避免重复搜索的技术。如果搜索树中大量不同路径会到达相同状态,那么继续把它当作树搜索就会产生大量重复计算。此时,记忆化搜索或动态规划可能比单纯回溯更合适。

整数规划中的分支限界也高度依赖松弛思想。很多整数规划求解器会先解线性规划松弛问题,用松弛解提供界,再通过分支逐步逼近整数最优解。这里的逻辑和 0/1 背包中用分数背包提供上界,本质上是一样的。

这些连接说明,搜索算法并不是孤立技巧。它们共同关心:怎么表示状态?怎么避免无效遍历?怎么如何证明没有错过正确答案或最优答案?

结语

回溯除了表面模板:选择、递归、撤销选择。真正值得掌握的是这几个问题:

  • 我的解空间树是什么?
  • 一个节点代表哪些可能的补全?
  • 我什么时候能证明这个节点下面没有合法解?
  • 如果是最优化问题,我能不能证明这个节点下面没有更优解?
  • 我的 bound 是否安全?是否足够紧?计算是否划算?

而所有有效的剪枝,本质上都在做同一件事:在答案出现之前,证明某些答案不可能存在。