分支限界法:从思想到实战

分支限界法,说白了就是「有脑子的暴力搜索」。

我刚开始学算法时,总觉得它跟回溯法差不多。后来在项目中做路径规划,才真正体会到两者的区别——回溯是「一条路走到黑,不行就回头」,分支限界是「每走一步都看看,哪条路最有希望」。

嗯,今天我们就来聊聊这个思想,以及它在三个经典问题里的应用。

分支限界思想的核心

分支限界法有两个关键动作:分支限界

  • 分支:把问题拆成若干子问题,每个子问题就是一个分支节点。
  • 限界:对每个节点估算一个「上界」或「下界」,如果这个界比当前最优解还差,就直接剪掉。

你想想看,这就像面试时筛简历——先看学历,再看经验,如果第一轮就不达标,后面就不用看了。

关键区别:回溯法用深度优先,分支限界法通常用广度优先或优先队列(最佳优先)。

我个人习惯用优先队列来实现分支限界。为什么呢?因为每次都能挑出「最有潜力」的节点先扩展,往往能更快找到最优解。

旅行商问题(TSP)

TSP 问题:一个销售员要跑 n 个城市,每个城市只去一次,最后回到起点,求最短路径。

n 稍微大一点,比如 20 个城市,暴力枚举就是 20! 种可能——你算算,这数字比银河系的星星还多。

分支限界法怎么解?

  1. 从起点出发,每到一个城市就分支——选择下一个未访问的城市。
  2. 对每个部分路径,估算一个「下界」:当前已走距离 + 剩余城市的最小可能距离。
  3. 如果这个下界已经大于当前最优解,直接剪枝。

我在项目中遇到过类似问题——给机器人规划巡检路线,30 个点。用分支限界法,配合一个简单的下界函数,几秒钟就出结果了。要是用回溯法,跑半小时都停不下来。

小技巧:下界函数可以这样算——对每个未访问城市,取它到其他未访问城市的最短边,加起来除以 2。这个下界虽然不紧,但计算快,实际效果不错。

0-1背包问题(分支限界解法)

0-1 背包,大家应该不陌生。n 个物品,每个有重量和价值,背包容量有限,求最大价值。

动态规划能解,但物品重量很大时,DP 表会爆炸。这时候分支限界法就派上用场了。

思路是这样的:

  • 按价值密度(价值/重量)从大到小排序。
  • 每个节点表示「当前已选物品」和「当前总价值」。
  • 分支:下一个物品选或不选。
  • 限界:用「贪心+分数背包」算上界——假设剩下的物品可以按比例拿,算出一个理论最大值。

如果这个上界 ≤ 当前最优价值,剪掉。

// 分支限界解0-1背包的核心结构
typedef struct {
    int level;      // 当前处理到第几个物品
    int weight;     // 当前总重量
    int value;      // 当前总价值
    double bound;   // 上界
} Node;

// 计算上界函数
double bound(Node node, int n, int capacity, 
             int weights[], int values[]) {
    if (node.weight >= capacity) return 0;
    
    double profit_bound = node.value;
    int j = node.level + 1;
    int total_weight = node.weight;
    
    // 贪心拿完整物品
    while (j < n && total_weight + weights[j] <= capacity) {
        total_weight += weights[j];
        profit_bound += values[j];
        j++;
    }
    
    // 最后一个物品拿一部分
    if (j < n) {
        profit_bound += (capacity - total_weight) * 
                        (double)values[j] / weights[j];
    }
    
    return profit_bound;
}

我曾经用这个解法处理过一个物流装箱问题——100 个包裹,卡车容量有限。分支限界法配合优先队列,比纯贪心多装了 12% 的货值。

注意:分支限界法虽然快,但最坏情况仍然是指数级。如果物品数量超过 50,建议先用启发式算法找近似解,再用分支限界做精确优化。

装载问题

装载问题:有两艘船,载重量分别为 c1 和 c2,有 n 个集装箱,每个重量为 w[i]。问能否把所有集装箱装上船?

这个问题可以转化为:第一艘船尽量装,剩下的给第二艘。说白了,就是找一个子集,总重量 ≤ c1,且尽可能大。

分支限界解法:

  • 每个节点表示「当前已选集装箱的总重量」。
  • 分支:下一个集装箱装或不装。
  • 限界:当前重量 + 剩余所有集装箱重量。如果这个值 ≤ 当前最优解,剪掉。

你可能会问:「这跟回溯法有什么区别?」

区别在于搜索顺序。分支限界用优先队列,每次选「当前重量最大」的节点扩展——这样能更快逼近最优解,剪枝效率更高。

我记得有一次做港口调度,200 个集装箱,两艘船。用回溯法跑了 3 分钟没出结果,换成分支限界法,15 秒就找到了可行方案。

知识体系总览

下面这张图,帮你把分支限界法的核心逻辑串起来:

分支限界法知识体系 分支限界思想 分支:拆解子问题 限界:估算上下界 旅行商问题 最短路径搜索 0-1背包问题 价值密度上界 装载问题 子集和最大化 优先队列(最佳优先) 广度优先搜索 核心:用界剪枝,用优先队列加速

避坑指南

讲几个我踩过的坑:

  • 下界太松:我曾经给 TSP 问题写了一个很粗糙的下界函数,结果剪枝效率极低,跑得比暴力还慢。后来改用「最小生成树」做下界,速度快了 10 倍。
  • 优先队列的排序:0-1 背包里,按上界从大到小排序。但要注意——上界相同的节点,优先选 level 大的(更接近叶子节点),这样能更快更新最优解。
  • 内存爆炸:分支限界法会生成大量节点。如果问题规模大,建议限制队列大小,或者用迭代加深的方式控制。

我的建议:初学者先拿 0-1 背包练手,因为它的上界函数好理解,调试也方便。等熟练了,再挑战 TSP 和装载问题。

分支限界法,说白了就是「聪明地穷举」。它不像动态规划那样需要完美的状态转移方程,也不像贪心那样可能错过最优解。它给你一个平衡点——在时间和最优性之间做取舍。

嗯,今天就聊到这里。代码多写几遍,你自然就找到感觉了。


公众号:蓝海资料掘金营,微信deep3321