分支限界的拆解
阅读前了解 binary heap 与 A* 搜索会有帮助:前者是「取 bound 最大的活节点」所依赖的容器,后者与分支限界同源。
1 · 指数级决策树
全文用一个具体问题贯穿:0/1 背包。给定一组物品,各有价值与重量,以及一个容量上限,挑一个子集塞进背包,在不超重的前提下让总价值最大。每件物品只有选与不选两种结局,把这些决定一层层摊开就是一棵二叉决策树。
件物品给出 个叶子。图 1-1 只有 4 件物品,叶子 16 个、节点 31 个;但叶子数按指数增长,暴力枚举很快不可行。
分支限界的做法是不走到每个叶子,而在树的中间层就给整棵子树估一个乐观上界,达不到已知最好解的子树提前剪掉。
2 · 乐观界与 best-first 剪枝
分支限界由分支(branch)与限界(bound)两部分组成。分支是在决策树上展开选与不选;限界是给每个节点算一个 bound,即这棵子树再走运也只能到多好的乐观估计。一旦某节点的 bound 不超过当前已知最好解,它和它整棵子树就再无翻盘可能,可以一刀剪掉。
背包的 bound 用 LP 松弛给出。站在某个节点上,已决定了前几件、装了若干价值、占了若干重量,把剩下的物品按性价比从高到低往剩余容量里塞,最后一件装不下时允许切一块塞满。这个可切分背包的解一定不小于真实的 0/1 解,因而是合格的乐观上界,永不低估,剪枝才安全。
挑谁先探则交给 priority queue:每次弹出 bound 最大的活节点,即 best-first。最有希望的方向先走,往往很快逼出一个高的已知最好解,后面的子树也就更容易被剪。
图 2-1 的完整决策树有 31 个节点、16 个叶子,分支限界往往只生成其中少数几个。解空间仍是指数级,但大多数指数子树被挡在搜索之外,这正是它能处理 NP 难问题实际实例的原因。
警示 · bound 越紧剪得越多。本页用的可切分背包是一个较紧的界;界若较松,例如直接取「剩余物品总价值」,剪枝就少,退化回接近暴力枚举。设计一个既紧又易计算的 bound 是分支限界的关键,不同问题各有各的界。
3 · 与 A* 的关系
分支限界与 A* 在「用 priority queue、按某个乐观估计挑下一个最该扩展的节点」这个骨架上几乎一致,因而容易混淆。区别在于那个估计是什么,以及在解什么问题。分支限界解组合优化,队列里排的是 bound,即子树再走运能到多好的乐观界;A* 解图上最短路径,队列里排的是 ,即已走代价加剩余代价的启发估计。
3.1 · 逐项对照
| 维度 | best-first 分支限界 | A* |
|---|---|---|
| 解的对象 | 解空间决策树,一串「选或不选、第 步放哪」的决定 | 状态图,节点是状态、边是转移代价 |
| 队列排序键 | bound:当前部分解经松弛能达到的最优目标值 | |
| 键怎么构成 | 通常只有未来的乐观估计,来自 LP 或贪心松弛 | 已花费的 加未来估计 ,二者相加 |
| 优化方向 | 最大化取上界、最小化取下界;本页取 bound 最大 | 最小化,取 最小 |
| 界与启发的要求 | bound 须是真最优的乐观估计,不低估能达到的最优,剪枝才安全 | admissible(不高估)保证最优,consistent 保证每点只扩展一次 |
| 核心动作 | 剪枝:bound 不优于已知最好解即剪去整棵子树 | 松弛:更新邻居的 ,按 重排 |
| 记已访问状态 | 一般不需要,树结构下路径天然不重复 | 需要 closed 集或距离表,图里同一状态有多条路径汇聚 |
| 终止 | 队列空,或队首 bound 已不优于现有最好解 | 目标节点出队时 |
把 A* 的 看作「从起点经该节点到终点的总代价」的乐观估计,它正是 best-first 分支限界的 bound。因此 A* 可以看作在「状态图最短路径」这个具体问题上、bound 取 形式的分支限界。反过来分支限界更一般:它的 bound 不一定能拆成「已付出的 加未来的 」这种可加结构,背包的可切分松弛上界就不是沿路径累加出来的;它要找的也不是一条路径,而是一组最优决策。
警示 · 两处最容易混。其一, 算不算:A* 一定把已付出的真实代价 计入排序键,只看 的是贪心 best-first,不保证最优;分支限界的 bound 里通常也隐含「已确定部分的真实值加未确定部分的松弛值」,与 同源。其二,剪枝与不重复扩展是两件事:分支限界靠 bound 不优于已知最好解主动砍子树,A* 靠 admissible 的 保证目标出队即最优,再靠 consistent 的 配 closed 集做到每点只扩展一次,后者本质上也是一种剪枝。
4 · 预算分配实例
每个广告渠道有投放成本与预估收益,总预算有限,每个渠道要么投要么不投,求收益最大的组合。这是一个不折不扣的 0/1 背包,方案空间是 个子集。分支限界给每个分支算一个乐观上界,凡「最乐观也超不过当前最优」的子树整片剪掉,往往只评估少数几个节点即可确定最优。
省下来的开销来自界的紧度:渠道按性价比排序后,把剩余预算允许切分地按性价比塞满,得到一个绝不可能被超越的天花板;一旦某分支的天花板不优于当前已知最优,它整棵子树的指数级方案一次性被剪。
5 · 工程落地
整数规划求解器是最主要的落地:Gurobi、CPLEX 与 Google OR-Tools 解混合整数线性规划(MILP)的内核就是 branch-and-bound,叠加割平面即 branch-and-cut,这是运筹优化领域的主力引擎。调度与分配类问题同理,航空机组排班、车辆路径规划、生产排程、云资源装箱求精确最优,最终都会落到 B&B 或 MILP。此外还有 0/1 背包、集合覆盖、TSP 的精确解,以及 EDA 芯片布局布线与编译器里某些精确寄存器分配。
注 · 本页每次「取 bound 最大的活节点」靠的仍是 binary heap;把 bound 换成 、把最大化价值换成最小化路径,就得到地图导航与游戏寻路使用的 A* 搜索。同一个取极值的容器,两种填法。
相关链接
-
本仓库 · 最短路径 A*
/shortest-path
A* 网格寻路:取
f = g + h最小、朝目标收窄。它正是 bound 取g + h形式的 best-first 分支限界。 - 本仓库 · binary heap (priority queue) /priority-queue best-first 分支限界「每次取 bound 最大的活节点」依赖的容器,与 A* 的 open 集是同一种数据结构。
- Branch and bound Wikipedia 分支限界的形式定义、bounding function 的设计、与 best-first / DFS / BFS 搜索策略的组合。
- Knapsack problem Wikipedia 本系列贯穿的 0/1 背包问题:LP 松弛(可切分背包)上界的由来,以及它作为 bound 为何永不低估。
- Google OR-Tools developers.google.com 以 branch-and-bound / branch-and-cut 为内核的开源 MILP / CP 求解器,工业界排班、VRP、装箱的主力工具。