零基础数据结构与算法——第五章:高级算法-分支限界问题&旅行商
摘要:本文介绍了使用分支限界法解决旅行商问题的经典算法。该问题要求找到访问所有城市并返回起点的最短路径,属于NP-Hard问题。算法通过优先队列管理搜索空间,计算路径下界进行剪枝优化。文中给出了详细的下界计算方法、搜索树构建过程(以4城市为例)和Java实现代码,包括节点类定义和边界计算逻辑。时间复杂度最坏为O(n!),但实践中因剪枝可显著提升效率。与动态规划和贪心算法相比,分支限界法在中小规模问
5.4.3 经典分支限界问题
0-1背包问题(分支限界解法)
旅行商问题(分支限界解法)
问题描述:
给定n个城市和城市之间的距离,求解访问每个城市恰好一次并返回起点的最短路径。这是一个著名的NP-Hard问题,对于大规模问题,无法在多项式时间内求得精确解。
生活例子:
想象你是一名快递员,需要在一天内送货到城市的多个地点。你希望规划一条路线,从公司出发,依次访问所有送货点,最后回到公司,同时希望总行驶距离最短,以节省时间和燃料。
分支限界法思路:
分支限界法通过以下步骤解决旅行商问题:
- 使用优先队列管理搜索空间,按下界排序(这是最小化问题,所以按下界升序排序)
- 从起始城市开始,逐步构建路径
- 对于每个节点,尝试将未访问的城市添加到当前路径中
- 使用下界函数剪枝,如果节点的下界大于当前已知的最小成本,则不再探索该节点
下界计算方法:
下界是对最优解的估计,通常是一个比实际最优解更乐观的值。对于旅行商问题,下界计算方法为:
- 已确定路径的成本
- 对于每个未访问的城市,添加其连接到其他城市的最小成本边
图解过程:
假设有4个城市,距离矩阵如下:
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](起点)
/ | \
/ | \
[0,1] [0,2] [0,3]
/ \ / \ / \
[0,1,2] [0,1,3] ... ... ...
/ \
[0,1,2,3] [0,1,3,2]
代码实现:
public static int tspBranchAndBound(int[][] graph) {
int n = graph.length; // 城市数量
// 创建根节点
Node root = new Node();
root.level = 0; // 已访问城市数量(包括起点)
root.path.add(0); // 从城市0开始
root.bound = computeBound(root, graph, n); // 计算下界
// 使用优先队列,按下界排序(最小化问题,所以按下界升序排序)
PriorityQueue<Node> pq = new PriorityQueue<>((a, b) -> Integer.compare(a.bound, b.bound));
pq.add(root); // 将根节点加入优先队列
int minCost = Integer.MAX_VALUE; // 当前找到的最小成本
// 当优先队列非空时,继续搜索
while (!pq.isEmpty()) {
// 取出队首节点(下界最小的节点)
Node node = pq.poll();
// 剪枝:如果下界大于等于当前最小成本,则跳过
if (node.bound >= minCost) continue;
// 如果已经访问了所有城市
if (node.level == n - 1) {
// 添加最后一个未访问的城市
int lastCity = -1;
for (int i = 0; i < n; i++) {
if (!node.path.contains(i)) {
lastCity = i;
break;
}
}
node.path.add(lastCity); // 添加最后一个城市
// 计算完整路径的总成本(包括回到起点)
int currentCost = 0;
for (int i = 0; i < n; i++) {
currentCost += graph[node.path.get(i)][node.path.get((i + 1) % n)];
}
// 更新当前找到的最小成本
minCost = Math.min(minCost, currentCost);
continue;
}
// 尝试访问每个未访问的城市
for (int i = 0; i < n; i++) {
if (!node.path.contains(i)) { // 如果城市i尚未访问
// 创建新节点
Node newNode = new Node();
newNode.level = node.level + 1; // 已访问城市数量加1
newNode.path.addAll(node.path); // 复制当前路径
newNode.path.add(i); // 添加新城市
newNode.bound = computeBound(newNode, graph, n); // 重新计算下界
// 如果下界小于当前最小成本,则加入队列继续探索
if (newNode.bound < minCost) {
pq.add(newNode);
}
}
}
}
return minCost;
}
// 计算节点的下界
private static int computeBound(Node node, int[][] graph, int n) {
// 计算下界的方法:已经确定的路径成本 + 剩余城市的最小出边
int bound = 0;
// 1. 已确定路径的成本
for (int i = 0; i < node.path.size() - 1; i++) {
bound += graph[node.path.get(i)][node.path.get(i + 1)];
}
// 2. 对于每个未访问的城市,添加最小出边
// 这是一个乐观估计,假设每个城市都能以最小成本连接
for (int i = 0; i < n; i++) {
if (!node.path.contains(i)) { // 如果城市i尚未访问
int min = Integer.MAX_VALUE;
for (int j = 0; j < n; j++) {
if (i != j) { // 不考虑自环
min = Math.min(min, graph[i][j]); // 找出最小边
}
}
bound += min; // 将最小边加入下界
}
}
return bound;
}
// 搜索树节点类
static class Node {
int level; // 当前访问的城市数量
List<Integer> path = new ArrayList<>(); // 当前路径
int bound; // 下界(对最优解的估计)
}
时间复杂度分析:
最坏情况下,分支限界法需要探索所有可能的排列,时间复杂度为O(n!)。但在实际应用中,由于有效的剪枝策略,通常可以大大减少搜索空间。
空间复杂度分析:
O(n²),主要用于存储优先队列中的节点和路径信息。
与其他解法的比较:
| 特点 | 分支限界法 | 动态规划 | 贪心算法(近似解) |
|---|---|---|---|
| 时间复杂度 | 最坏O(n!) | O(n²·2ⁿ) | O(n²) |
| 空间复杂度 | O(n²) | O(n·2ⁿ) | O(n) |
| 解的质量 | 精确解 | 精确解 | 近似解 |
| 适用规模 | 中小规模 | 小规模 | 大规模 |
| 优点 | 可能在找到最优解前提前终止 | 保证在固定时间内找到最优解 | 速度快,易于实现 |
| 缺点 | 最坏情况下效率低 | 只适用于小规模问题 | 解的质量不保证 |
应用场景:
- 物流配送路线规划
- 电路板钻孔问题
- 网络路由优化
- 机器人路径规划
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐
所有评论(0)