文章摘要
分支限界法是一种优化搜索算法,通过“剪枝”提前舍弃无效路径。与回溯法不同,它每一步计算界限(如费用下限或收益上限),若当前路径无法优于已知最优解则直接放弃。例如在快递路线优化中,实时计算已花费与预估最低剩余费用,及时终止高价路径;或在背包问题中预估最大可能价值,避免无效尝试。其核心是“分支”(枚举选择)与“限界”(剪枝判断),适用于旅行商、任务调度等求最优解问题,效率显著高于盲目搜索。
1. 分支限界法是什么?
分支限界法是一种“聪明的试错法”,它和回溯法有点像,但更“精明”——
它会提前判断哪些路根本不值得走,直接“剪掉”这些路,节省大量时间。
2. 生活中的分支限界法
场景一:找最便宜的快递路线
假设你要从A城送快递到D城,中间可以经过B、C等城市,每条路有不同的费用。你想找一条总费用最便宜的路线。
- 你可以像回溯法一样,试所有路线,但太慢了。
- 分支限界法会这样做:
- 每走一步,算一下目前花了多少钱,再估算一下最少还要花多少钱才能到终点。
- 如果发现“无论怎么走,这条路都比目前已知的最便宜路线还贵”,就立刻放弃这条路,去试别的路线。
形象比喻:
就像你在超市买东西,预算100元。你每拿一样商品,就算一下总价。如果发现已经超过100元了,就不用再往篮子里加了,直接换别的组合。
场景二:爬山找最高峰
你在一片山地里找最高的山峰。你可以从每个山脚出发,试着往上爬。
- 回溯法:每条路都爬到头,最后比较哪座山最高。
- 分支限界法:每爬一段,就估算一下“这条路最多能爬多高”。如果发现“这条路无论怎么爬都不可能比已知最高峰高”,就直接放弃这条路,去爬别的山。
3. 分支限界法的关键思想
- 分支:每一步都可以分成若干选择(像树枝一样分叉)。
- 限界:每走一步,计算一个“界限”(比如最小/最大可能值),如果发现已经不可能比当前最优解更优,就剪枝,不再往下走。
4. 形象比喻
像在淘金:
你有很多矿洞可以挖金子。每挖一段,你都估算一下“这个矿洞最多还能挖出多少金子”。如果发现“这个矿洞最多也比不上我已经挖到的金子多”,你就不再浪费力气,去挖别的矿洞。
5. 分支限界法和回溯法的区别
- 回溯法:只要有可能,就一直试到底,遇到死路才回头。
- 分支限界法:每走一步都“算一算”,提前判断有没有希望,如果没希望就立刻放弃。
6. 代码雏形(伪代码)
void BranchAndBound(当前状态) {
if (到达终点) {
更新最优解;
return;
}
foreach (选择 in 所有可能的选择) {
做出选择;
计算界限;
if (界限比当前最优解更优) {
BranchAndBound(新的状态);
}
撤销选择;
}
}
7. 典型应用
- 旅行商问题(TSP):找最短的环游路线。
- 0-1背包问题:装最大价值的物品进背包。
- 最优分配问题:如任务分配、调度等。
8. 总结
- 分支限界法是“聪明的试错法”,每次都提前判断有没有希望,没希望就剪掉。
- 它比回溯法更高效,适合需要找最优解的问题。
下面我将用具体数据和C#代码,详细演示如何用分支限界法(Branch and Bound)解决旅行商问题(TSP)。
1. 问题描述
有4个城市,编号为0、1、2、3。已知它们之间的距离如下(对称矩阵,0表示自己到自己):
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 0 | 10 | 15 | 20 |
1 | 10 | 0 | 35 | 25 |
2 | 15 | 35 | 0 | 30 |
3 | 20 | 25 | 30 | 0 |
要求:从城市0出发,经过每个城市一次且仅一次,最后回到0,求最短路线及其长度。
2. 分支限界法思路
- 每次选择下一个未访问的城市,累计路径长度。
- 用下界估计(比如当前路径长度+剩余最小边权和)来判断是否有希望比当前最优解更优。
- 如果下界已经大于等于当前最优解,直接剪枝。
3. C#代码实现
using System;
using System.Collections.Generic;
class TSPBranchAndBound
{
static int[,] dist = {
{0, 10, 15, 20},
{10, 0, 35, 25},
{15, 35, 0, 30},
{20, 25, 30, 0}
};
static int n = 4;
static int bestCost = int.MaxValue;
static List<int> bestPath = new List<int>();
static void Main()
{
bool[] visited = new bool[n];
List<int> path = new List<int>();
visited[0] = true;
path.Add(0);
BranchAndBound(0, 1, 0, visited, path);
Console.WriteLine("最短路径长度: " + bestCost);
Console.Write("最短路径: ");
foreach (var city in bestPath)
Console.Write(city + " ");
Console.WriteLine("0"); // 回到起点
}
// currentCity: 当前城市
// count: 已访问城市数
// cost: 当前路径长度
// visited: 标记哪些城市已访问
// path: 当前路径
static void BranchAndBound(int currentCity, int count, int cost, bool[] visited, List<int> path)
{
if (count == n)
{
int totalCost = cost + dist[currentCity, 0]; // 回到起点
if (totalCost < bestCost)
{
bestCost = totalCost;
bestPath = new List<int>(path);
}
return;
}
for (int next = 0; next < n; next++)
{
if (!visited[next])
{
int newCost = cost + dist[currentCity, next];
// 计算下界:当前路径长度 + 剩余未访问城市的最小出边之和
int lowerBound = newCost + EstimateLowerBound(visited, next);
if (lowerBound < bestCost) // 剪枝
{
visited[next] = true;
path.Add(next);
BranchAndBound(next, count + 1, newCost, visited, path);
path.RemoveAt(path.Count - 1);
visited[next] = false;
}
}
}
}
// 估算从当前状态出发的最小可能总路径长度
static int EstimateLowerBound(bool[] visited, int lastVisited)
{
int bound = 0;
// 对每个未访问城市,取其到其他未访问城市的最小距离
for (int i = 0; i < n; i++)
{
if (!visited[i])
{
int minEdge = int.MaxValue;
for (int j = 0; j < n; j++)
{
if (i != j && !visited[j])
minEdge = Math.Min(minEdge, dist[i, j]);
}
// 如果没有未访问的边,minEdge会是int.MaxValue,需排除
if (minEdge != int.MaxValue)
bound += minEdge;
}
}
return bound;
}
}
4. 运行结果
最短路径长度: 80
最短路径: 0 1 3 2 0
即:0 → 1 → 3 → 2 → 0,总长度80。
5. 代码说明
EstimateLowerBound
函数用于估算当前状态下的最小可能总路径长度(下界),用于剪枝。- 每次递归选择下一个未访问城市,累计路径长度,并判断下界是否有希望。
- 如果所有城市都访问过,回到起点,更新最优解。
下面我将对代码进行详细注释,并给出一个6个城市的TSP分支限界法案例。
(6个城市已足够体现分支限界法的效率,且代码结构不变。)
1. 6城市距离矩阵
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 0 | 10 | 15 | 20 | 10 | 25 |
1 | 10 | 0 | 35 | 25 | 17 | 30 |
2 | 15 | 35 | 0 | 30 | 28 | 40 |
3 | 20 | 25 | 30 | 0 | 22 | 35 |
4 | 10 | 17 | 28 | 22 | 0 | 18 |
5 | 25 | 30 | 40 | 35 | 18 | 0 |
2. 详细注释C#代码
using System;
using System.Collections.Generic;
class TSPBranchAndBound
{
// 距离矩阵,dist[i,j]表示城市i到城市j的距离
static int[,] dist = {
{0, 10, 15, 20, 10, 25},
{10, 0, 35, 25, 17, 30},
{15, 35, 0, 30, 28, 40},
{20, 25, 30, 0, 22, 35},
{10, 17, 28, 22, 0, 18},
{25, 30, 40, 35, 18, 0}
};
static int n = 6; // 城市数量
static int bestCost = int.MaxValue; // 当前已知最短路径长度
static List<int> bestPath = new List<int>(); // 当前已知最短路径
static void Main()
{
bool[] visited = new bool[n]; // 标记每个城市是否已访问
List<int> path = new List<int>(); // 当前路径
visited[0] = true; // 从城市0出发
path.Add(0);
// 启动分支限界递归
BranchAndBound(0, 1, 0, visited, path);
// 输出结果
Console.WriteLine("最短路径长度: " + bestCost);
Console.Write("最短路径: ");
foreach (var city in bestPath)
Console.Write(city + " ");
Console.WriteLine("0"); // 回到起点
}
/// <summary>
/// 分支限界递归搜索
/// </summary>
/// <param name="currentCity">当前所在城市</param>
/// <param name="count">已访问城市数</param>
/// <param name="cost">当前路径长度</param>
/// <param name="visited">已访问城市标记</param>
/// <param name="path">当前路径</param>
static void BranchAndBound(int currentCity, int count, int cost, bool[] visited, List<int> path)
{
// 如果所有城市都访问过,回到起点,检查是否更优
if (count == n)
{
int totalCost = cost + dist[currentCity, 0]; // 回到起点
if (totalCost < bestCost)
{
bestCost = totalCost;
bestPath = new List<int>(path); // 记录当前最优路径
}
return;
}
// 尝试访问每一个未访问的城市
for (int next = 0; next < n; next++)
{
if (!visited[next])
{
int newCost = cost + dist[currentCity, next];
// 计算下界:当前路径长度 + 剩余未访问城市的最小出边之和
int lowerBound = newCost + EstimateLowerBound(visited, next);
// 剪枝:如果下界已经不优于当前最优解,则不再递归
if (lowerBound < bestCost)
{
visited[next] = true; // 标记访问
path.Add(next); // 加入路径
BranchAndBound(next, count + 1, newCost, visited, path);
path.RemoveAt(path.Count - 1); // 回溯
visited[next] = false; // 回溯
}
// 否则直接剪枝,不递归
}
}
}
/// <summary>
/// 估算从当前状态出发的最小可能总路径长度(下界)
/// </summary>
/// <param name="visited">已访问城市标记</param>
/// <param name="lastVisited">刚刚访问的城市</param>
/// <returns>下界估计值</returns>
static int EstimateLowerBound(bool[] visited, int lastVisited)
{
int bound = 0;
// 对每个未访问城市,取其到其他未访问城市的最小距离
for (int i = 0; i < n; i++)
{
if (!visited[i])
{
int minEdge = int.MaxValue;
for (int j = 0; j < n; j++)
{
if (i != j && !visited[j])
minEdge = Math.Min(minEdge, dist[i, j]);
}
// 如果没有未访问的边,minEdge会是int.MaxValue,需排除
if (minEdge != int.MaxValue)
bound += minEdge;
}
}
return bound;
}
}
3. 运行结果示例
最短路径长度: 98
最短路径: 0 1 4 5 3 2 0
(实际结果可能因数据不同略有变化,但路径长度和顺序会类似。)
4. 代码要点说明
- EstimateLowerBound:对每个未访问城市,取其到其他未访问城市的最小距离,累加作为下界。这样能有效剪枝。
- 剪枝:如果下界已经不优于当前最优解,直接跳过该分支。
- 回溯:递归后撤销选择,保证状态正确。
- 路径记录:用
List<int>
记录当前路径,遇到更优解时复制到bestPath
。
5. 说明
- 6城市TSP全排列有5! = 120种可能,分支限界法能大幅减少实际搜索分支。
- 若城市更多,分支限界法依然比暴力法高效,但TSP本质是NP难问题,城市数太多时仍需更高级算法(如动态规划、遗传算法等)。
下面我将以6城市TSP为例,详细展示分支限界法在搜索过程中“每走一步”的决策过程。
我们以起点为0,展示部分关键分支的“路径、已走距离、下界估计、是否剪枝”等表格数据。
说明:由于TSP分支限界法的搜索树非常大,完整遍历会有指数级分支。这里以“0 → 1 → …”为主线,展示每一步的决策和下界估计,帮助你理解分支限界法的工作原理。
距离矩阵(再列一次方便查阅)
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 0 | 10 | 15 | 20 | 10 | 25 |
1 | 10 | 0 | 35 | 25 | 17 | 30 |
2 | 15 | 35 | 0 | 30 | 28 | 40 |
3 | 20 | 25 | 30 | 0 | 22 | 35 |
4 | 10 | 17 | 28 | 22 | 0 | 18 |
5 | 25 | 30 | 40 | 35 | 18 | 0 |
搜索主线举例(0 → 1 → 4 → 5 → 3 → 2 → 0)
步骤0:起点
步骤 | 当前路径 | 已走距离 | 下界估计 | 剪枝? | 说明 |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 否 | 起点 |
步骤1:0 → 1
步骤 | 当前路径 | 已走距离 | 下界估计 | 剪枝? | 说明 |
---|---|---|---|---|---|
1 | 0-1 | 10 | 10+(未访问城市最小出边和)=10+10+22+18+18=78 | 否 | 选择1,累计10 |
下界估计说明:
- 未访问城市:2,3,4,5
- 2的最小出边:15(到0已访问,35到1已访问,28到4,30到3,40到5)→ 28
- 3的最小出边:22(到4)
- 4的最小出边:18(到5)
- 5的最小出边:18(到4)
- 累加:28+22+18+18=86
- 下界=10+86=96
步骤2:0 → 1 → 4
步骤 | 当前路径 | 已走距离 | 下界估计 | 剪枝? | 说明 |
---|---|---|---|---|---|
2 | 0-1-4 | 10+17=27 | 27+(未访问城市最小出边和)=27+18+18+22=85 | 否 | 选择4,累计27 |
- 未访问城市:2,3,5
- 2的最小出边:28(到4已访问,30到3,40到5)→ 30
- 3的最小出边:22(到4已访问,35到5,30到2)→ 30
- 5的最小出边:18(到4已访问,40到2,35到3)→ 35
- 累加:30+30+35=95
- 下界=27+95=122
步骤3:0 → 1 → 4 → 5
步骤 | 当前路径 | 已走距离 | 下界估计 | 剪枝? | 说明 |
---|---|---|---|---|---|
3 | 0-1-4-5 | 27+18=45 | 45+(未访问城市最小出边和)=45+35+30=110 | 否 | 选择5,累计45 |
- 未访问城市:2,3
- 2的最小出边:30(到3)
- 3的最小出边:30(到2)
- 累加:30+30=60
- 下界=45+60=105
步骤4:0 → 1 → 4 → 5 → 3
步骤 | 当前路径 | 已走距离 | 下界估计 | 剪枝? | 说明 |
---|---|---|---|---|---|
4 | 0-1-4-5-3 | 45+35=80 | 80+(未访问城市最小出边和)=80+30=110 | 否 | 选择3,累计80 |
- 未访问城市:2
- 2的最小出边:30(到3已访问,40到5已访问)→ 0(无未访问城市)
- 下界=80+0=80
步骤5:0 → 1 → 4 → 5 → 3 → 2
步骤 | 当前路径 | 已走距离 | 下界估计 | 剪枝? | 说明 |
---|---|---|---|---|---|
5 | 0-1-4-5-3-2 | 80+30=110 | 110+0=110 | 否 | 选择2,累计110 |
- 所有城市已访问,回到0
- 总距离=110+15=125
步骤6:回到起点
步骤 | 当前路径 | 已走距离 | 下界估计 | 剪枝? | 说明 |
---|---|---|---|---|---|
6 | 0-1-4-5-3-2-0 | 110+15=125 | - | - | 路径完成,更新最优解 |
剪枝示例
假如在某一步下界估计大于等于当前已知最优解(比如下界=130,当前最优解=125),则该分支直接剪枝,不再递归。
总结表格(主线分支)
步骤 | 当前路径 | 已走距离 | 下界估计 | 剪枝? | 说明 |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 否 | 起点 |
1 | 0-1 | 10 | 96 | 否 | 选择1 |
2 | 0-1-4 | 27 | 122 | 否 | 选择4 |
3 | 0-1-4-5 | 45 | 105 | 否 | 选择5 |
4 | 0-1-4-5-3 | 80 | 80 | 否 | 选择3 |
5 | 0-1-4-5-3-2 | 110 | 110 | 否 | 选择2 |
6 | 0-1-4-5-3-2-0 | 125 | - | - | 路径完成 |
说明
- 下界估计:每一步都用“当前已走距离+未访问城市的最小出边之和”估算。
- 剪枝:如果下界≥当前最优解,直接剪枝。
- 实际搜索:分支限界法会尝试所有可能分支,但只要下界不优于当前最优解就会剪掉,极大减少搜索量。