《算法导论》第 24 章 - 单源最短路径
引言
单源最短路径问题是图论中的核心问题之一,旨在求解从图中一个特定源节点到其他所有节点的最短路径。这一问题在导航系统、网络路由、交通规划等领域有着广泛应用。本章将详细解析《算法导论》第 24 章介绍的四种经典算法,并提供完整可运行的 C++ 实现代码。
24.1 Bellman-Ford 算法
Bellman-Ford 算法是求解单源最短路径问题的经典算法,它能够处理包含负权边的图,并且可以检测出图中是否存在从源节点可达的负权回路。
算法思想
- 初始化:将源节点到所有其他节点的距离初始化为无穷大,源节点到自身的距离初始化为 0。
- 松弛操作:对图中所有边进行 V-1 次松弛操作(V 为节点数)。每次松弛操作遍历所有边 (u, v),若
distance[v] > distance[u] + weight(u, v)
,则更新distance[v] = distance[u] + weight(u, v)
。 - 检测负权回路:在完成 V-1 次松弛后,再对所有边进行一次检查。如果仍能进行松弛操作,则说明图中存在从源节点可达的负权回路。
代码实现
#include <iostream>
#include <vector>
#include <climits>
using namespace std;// 边的结构体
struct Edge {int u; // 起点int v; // 终点int weight; // 权重Edge(int u_, int v_, int weight_) : u(u_), v(v_), weight(weight_) {}
};/*** Bellman-Ford算法实现* @param edges 边的集合* @param V 节点数量* @param s 源节点* @param distance 存储最短距离的数组* @return 是否存在负权回路*/
bool bellmanFord(const vector<Edge>& edges, int V, int s, vector<int>& distance) {// 1. 初始化距离distance.assign(V, INT_MAX);distance[s] = 0;// 2. 进行V-1次松弛操作for (int i = 1; i <= V - 1; ++i) {bool updated = false;for (const Edge& e : edges) {if (distance[e.u] != INT_MAX && distance[e.v] > distance[e.u] + e.weight) {distance[e.v] = distance[e.u] + e.weight;updated = true;}}// 如果没有更新,可以提前退出if (!updated) break;}// 3. 检测负权回路for (const Edge& e : edges) {if (distance[e.u] != INT_MAX && distance[e.v] > distance[e.u] + e.weight) {return true; // 存在负权回路}}return false; // 不存在负权回路
}int main() {// 示例图: 包含5个节点(0-4)int V = 5;vector<Edge> edges;// 添加边edges.emplace_back(0, 1, -1);edges.emplace_back(0, 2, 4);edges.emplace_back(1, 2, 3);edges.emplace_back(1, 3, 2);edges.emplace_back(1, 4, 2);edges.emplace_back(3, 1, 1);edges.emplace_back(3, 2, 5);edges.emplace_back(4, 3, -3);int source = 0;vector<int> distance;bool hasNegativeCycle = bellmanFord(edges, V, source, distance);if (hasNegativeCycle) {cout << "图中存在负权回路" << endl;} else {cout << "从源节点 " << source << " 到各节点的最短距离:" << endl;for (int i = 0; i < V; ++i) {if (distance[i] == INT_MAX) {cout << "节点 " << i << ": 不可达" << endl;} else {cout << "节点 " << i << ": " << distance[i] << endl;}}}return 0;
}
综合案例及应用
下面是一个物流路径规划的案例,展示如何使用 Bellman-Ford 算法处理可能包含负权边(如折扣路线)的场景:
#include <iostream>
#include <vector>
#include <climits>
#include <string>
using namespace std;struct Edge {int from;int to;int cost; // 可以是负数,表示折扣Edge(int f, int t, int c) : from(f), to(t), cost(c) {}
};bool bellmanFord(const vector<Edge>& edges, int V, int s, vector<int>& distance, vector<int>& parent) {distance.assign(V, INT_MAX);parent.assign(V, -1);distance[s] = 0;for (int i = 1; i <= V - 1; ++i) {for (const Edge& e : edges) {if (distance[e.from] != INT_MAX && distance[e.to] > distance[e.from] + e.cost) {distance[e.to] = distance[e.from] + e.cost;parent[e.to] = e.from;}}}for (const Edge& e : edges) {if (distance[e.from] != INT_MAX && distance[e.to] > distance[e.from] + e.cost) {return true;}}return false;
}// 打印从源节点到目标节点的路径
void printPath(int target, const vector<int>& parent) {if (parent[target] == -1) {cout << target;return;}printPath(parent[target], parent);cout << " -> " << target;
}int main() {// 物流网络: 5个仓库节点(0-4)int V = 5;vector<Edge> edges;// 添加运输路线及成本(负数表示折扣)edges.emplace_back(0, 1, 10); // 仓库0到1,成本10edges.emplace_back(0, 2, 30); // 仓库0到2,成本30edges.emplace_back(0, 3, 100); // 仓库0到3,成本100edges.emplace_back(1, 2, 5); // 仓库1到2,成本5edges.emplace_back(2, 3, 20); // 仓库2到3,成本20edges.emplace_back(3, 4, 10); // 仓库3到4,成本10edges.emplace_back(2, 4, 60); // 仓库2到4,成本60edges.emplace_back(1, 4, -50); // 仓库1到4,折扣路线,成本-50(实际是收益)int source = 0;vector<int> distance, parent;bool hasNegativeCycle = bellmanFord(edges, V, source, distance, parent);if (hasNegativeCycle) {cout << "物流网络中存在异常折扣循环,可能导致成本无限降低!" << endl;} else {cout << "从仓库 " << source << " 到各仓库的最低成本:" << endl;for (int i = 0; i < V; ++i) {if (distance[i] == INT_MAX) {cout << "仓库 " << i << ": 不可达" << endl;} else {cout << "仓库 " << i << ": 成本 " << distance[i] << ", 路径: ";printPath(i, parent);cout << endl;}}}return 0;
}
24.2 有向无环图中的单源最短路径问题
有向无环图(DAG)的单源最短路径问题可以通过拓扑排序结合松弛操作高效解决,时间复杂度为 O (V+E),优于 Bellman-Ford 算法。
算法思想
- 对 DAG 进行拓扑排序,得到一个线性排列的节点序列。
- 初始化距离数组:源节点距离为 0,其他节点为无穷大。
- 按照拓扑排序的顺序,对每个节点的所有出边进行松弛操作。
由于 DAG 中没有环,且我们按照拓扑顺序处理节点,每个节点只会被处理一次,因此算法效率很高。该算法可以处理负权边,但不能处理有环的图。
代码实现
#include <iostream>
#include <vector>
#include <stack>
#include <climits>
using namespace std;/*** 对DAG进行拓扑排序* @param adj 邻接表表示的图* @param V 节点数量* @return 拓扑排序结果*/
vector<int> topologicalSort(const vector<vector<pair<int, int>>>& adj, int V) {vector<int> inDegree(V, 0);// 计算入度for (int u = 0; u < V; ++u) {for (const auto& edge : adj[u]) {int v = edge.first;inDegree[v]++;}}// 使用栈存储入度为0的节点stack<int> s;for (int u = 0; u < V; ++u) {if (inDegree[u] == 0) {s.push(u);}}vector<int> topoOrder;while (!s.empty()) {int u = s.top();s.pop();topoOrder.push_back(u);// 减少相邻节点的入度for (const auto& edge : adj[u]) {int v = edge.first;inDegree[v]--;if (inDegree[v] == 0) {s.push(v);}}}return topoOrder;
}/*** DAG中的单源最短路径算法* @param adj 邻接表表示的图* @param V 节点数量* @param s 源节点* @param distance 存储最短距离的数组* @param parent 存储前驱节点的数组*/
void dagShortestPath(const vector<vector<pair<int, int>>>& adj, int V, int s, vector<int>& distance, vector<int>& parent) {// 1. 拓扑排序vector<int> topoOrder = topologicalSort(adj, V);// 2. 初始化distance.assign(V, INT_MAX);parent.assign(V, -1);distance[s] = 0;// 3. 按拓扑顺序松弛边for (int u : topoOrder) {if (distance[u] != INT_MAX) { // 仅处理可达节点for (const auto& edge : adj[u]) {int v = edge.first;int weight = edge.second;// 松弛操作if (distance[v] > distance[u] + weight) {distance[v] = distance[u] + weight;parent[v] = u;}}}}
}// 打印路径
void printPath(int v, const vector<int>& parent) {if (parent[v] == -1) {cout << v;return;}printPath(parent[v], parent);cout << " -> " << v;
}int main() {// 示例DAG: 5个节点(0-4)int V = 5;vector<vector<pair<int, int>>> adj(V);// 添加边 (u, v, weight)adj[0].emplace_back(1, 5);adj[0].emplace_back(2, 3);adj[1].emplace_back(2, 2);adj[1].emplace_back(3, 6);adj[2].emplace_back(3, 7);adj[2].emplace_back(4, 4);adj[2].emplace_back(1, 1);adj[3].emplace_back(4, -1);adj[4].emplace_back(3, -2);int source = 1;vector<int> distance, parent;dagShortestPath(adj, V, source, distance, parent);cout << "从源节点 " << source << " 到各节点的最短距离:" << endl;for (int i = 0; i < V; ++i) {if (distance[i] == INT_MAX) {cout << "节点 " << i << ": 不可达" << endl;} else {cout << "节点 " << i << ": " << distance[i] << ", 路径: ";printPath(i, parent);cout << endl;}}return 0;
}
综合案例及应用
下面是一个项目调度的案例,展示如何使用 DAG 最短路径算法解决关键路径问题:
#include <iostream>
#include <vector>
#include <stack>
#include <climits>
#include <algorithm>
using namespace std;// 对DAG进行拓扑排序
vector<int> topologicalSort(const vector<vector<pair<int, int>>>& adj, int V) {vector<int> inDegree(V, 0);for (int u = 0; u < V; ++u) {for (const auto& edge : adj[u]) {int v = edge.first;inDegree[v]++;}}stack<int> s;for (int u = 0; u < V; ++u) {if (inDegree[u] == 0) {s.push(u);}}vector<int> topoOrder;while (!s.empty()) {int u = s.top();s.pop();topoOrder.push_back(u);for (const auto& edge : adj[u]) {int v = edge.first;inDegree[v]--;if (inDegree[v] == 0) {s.push(v);}}}return topoOrder;
}// 计算最长路径(关键路径)
void criticalPathAnalysis(const vector<vector<pair<int, int>>>& adj, int V, vector<int>& earliest, vector<int>& latest, vector<int>& parent) {vector<int> topoOrder = topologicalSort(adj, V);// 计算最早开始时间(最长路径)earliest.assign(V, 0);parent.assign(V, -1);for (int u : topoOrder) {for (const auto& edge : adj[u]) {int v = edge.first;int weight = edge.second;if (earliest[v] < earliest[u] + weight) {earliest[v] = earliest[u] + weight;parent[v] = u;}}}// 计算最晚开始时间latest.assign(V, earliest.back()); // 最后一个节点的最晚时间等于最早时间// 反转拓扑排序reverse(topoOrder.begin(), topoOrder.end());for (int u : topoOrder) {for (const auto& edge : adj[u]) {int v = edge.first;int weight = edge.second;if (latest[u] > latest[v] - weight) {latest[u] = latest[v] - weight;}}}
}// 打印关键路径
void printCriticalPath(int v, const vector<int>& parent) {if (parent[v] == -1) {cout << v;return;}printCriticalPath(parent[v], parent);cout << " -> " << v;
}int main() {// 项目任务节点: 0(开始), 1-6(任务), 7(结束)int V = 8;vector<vector<pair<int, int>>> adj(V);// 添加任务依赖和持续时间 (u, v, 持续时间)adj[0].emplace_back(1, 3); // 开始 -> 任务1, 3天adj[0].emplace_back(2, 2); // 开始 -> 任务2, 2天adj[1].emplace_back(3, 4); // 任务1 -> 任务3, 4天adj[1].emplace_back(4, 3); // 任务1 -> 任务4, 3天adj[2].emplace_back(4, 2); // 任务2 -> 任务4, 2天adj[3].emplace_back(5, 1); // 任务3 -> 任务5, 1天adj[4].emplace_back(5, 2); // 任务4 -> 任务5, 2天adj[4].emplace_back(6, 3); // 任务4 -> 任务6, 3天adj[5].emplace_back(7, 3); // 任务5 -> 结束, 3天adj[6].emplace_back(7, 2); // 任务6 -> 结束, 2天vector<int> earliest, latest, parent;criticalPathAnalysis(adj, V, earliest, latest, parent);cout << "项目计划分析结果:" << endl;cout << "项目最短完成时间: " << earliest.back() << "天" << endl;cout << endl;cout << "各任务时间分析:" << endl;cout << "任务ID\t最早开始\t最晚开始\t浮动时间" << endl;for (int i = 1; i < V - 1; ++i) { // 跳过开始(0)和结束(V-1)节点int slack = latest[i] - earliest[i];cout << i << "\t" << earliest[i] << "\t\t" << latest[i] << "\t\t" << slack << endl;}cout << endl << "关键路径(无浮动时间的任务序列): ";printCriticalPath(V - 1, parent);cout << endl;return 0;
}
24.3 Dijkstra 算法
Dijkstra 算法是一种贪心算法,用于求解非负权图的单源最短路径问题。它的效率高于 Bellman-Ford 算法,尤其在使用优先队列优化后,性能表现优异。
算法思想
- 初始化:源节点距离为 0,其他节点距离为无穷大。维护一个优先队列(最小堆),用于选择当前距离最小的节点。
- 选择节点:每次从优先队列中取出距离最小的节点 u。
- 松弛操作:对节点 u 的所有出边进行松弛操作。如果通过 u 到达 v 的路径比当前已知路径更短,则更新 v 的距离,并将 v 加入优先队列。
- 标记已处理:一旦节点 u 被取出并处理,就标记为已处理,不再重新处理。
由于图中所有边的权重都是非负数,一旦某个节点被标记为已处理,我们就找到了从源节点到该节点的最短路径,无需再次考虑。
代码实现
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <utility>
using namespace std;/*** Dijkstra算法实现(使用优先队列优化)* @param adj 邻接表表示的图* @param V 节点数量* @param s 源节点* @param distance 存储最短距离的数组* @param parent 存储前驱节点的数组*/
void dijkstra(const vector<vector<pair<int, int>>>& adj, int V, int s, vector<int>& distance, vector<int>& parent) {// 初始化distance.assign(V, INT_MAX);parent.assign(V, -1);distance[s] = 0;// 优先队列: (距离, 节点), 使用最小堆priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;pq.push({0, s});// 记录已处理的节点vector<bool> processed(V, false);while (!pq.empty()) {// 提取距离最小的节点int u = pq.top().second;pq.pop();// 如果已处理,跳过if (processed[u]) continue;processed[u] = true;// 松弛所有出边for (const auto& edge : adj[u]) {int v = edge.first;int weight = edge.second;// 松弛操作if (distance[v] > distance[u] + weight) {distance[v] = distance[u] + weight;parent[v] = u;pq.push({distance[v], v});}}}
}// 打印路径
void printPath(int v, const vector<int>& parent) {if (parent[v] == -1) {cout << v;return;}printPath(parent[v], parent);cout << " -> " << v;
}int main() {// 示例图: 6个节点(0-5)int V = 6;vector<vector<pair<int, int>>> adj(V);// 添加边 (u, v, weight)adj[0].emplace_back(1, 7);adj[0].emplace_back(2, 9);adj[0].emplace_back(5, 14);adj[1].emplace_back(0, 7);adj[1].emplace_back(2, 10);adj[1].emplace_back(3, 15);adj[2].emplace_back(0, 9);adj[2].emplace_back(1, 10);adj[2].emplace_back(3, 11);adj[2].emplace_back(5, 2);adj[3].emplace_back(1, 15);adj[3].emplace_back(2, 11);adj[3].emplace_back(4, 6);adj[4].emplace_back(3, 6);adj[4].emplace_back(5, 9);adj[5].emplace_back(0, 14);adj[5].emplace_back(2, 2);adj[5].emplace_back(4, 9);int source = 0;vector<int> distance, parent;dijkstra(adj, V, source, distance, parent);cout << "从源节点 " << source << " 到各节点的最短距离:" << endl;for (int i = 0; i < V; ++i) {if (distance[i] == INT_MAX) {cout << "节点 " << i << ": 不可达" << endl;} else {cout << "节点 " << i << ": " << distance[i] << ", 路径: ";printPath(i, parent);cout << endl;}}return 0;
}
综合案例及应用
下面是一个城市交通导航系统的案例,展示如何使用 Dijkstra 算法规划最短路径:
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <utility>
#include <string>
using namespace std;// 城市节点类
class CityGraph {
private:vector<vector<pair<int, int>>> adj; // 邻接表: (城市ID, 距离)vector<string> cityNames; // 城市名称public:// 构造函数CityGraph(int n) : adj(n) {}// 添加城市名称void addCityName(int id, const string& name) {if (id >= cityNames.size()) {cityNames.resize(id + 1);}cityNames[id] = name;}// 添加道路void addRoad(int from, int to, int distance) {adj[from].emplace_back(to, distance);adj[to].emplace_back(from, distance); // 假设道路是双向的}// 查找最短路径void findShortestPath(int start, int end, vector<int>& distance, vector<int>& parent) {int V = adj.size();distance.assign(V, INT_MAX);parent.assign(V, -1);distance[start] = 0;priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;pq.push({0, start});vector<bool> processed(V, false);while (!pq.empty()) {int u = pq.top().second;pq.pop();if (u == end) break; // 提前结束,找到目标城市if (processed[u]) continue;processed[u] = true;for (const auto& edge : adj[u]) {int v = edge.first;int dist = edge.second;if (distance[v] > distance[u] + dist) {distance[v] = distance[u] + dist;parent[v] = u;pq.push({distance[v], v});}}}}// 打印路径void printPath(int v, const vector<int>& parent) {if (parent[v] == -1) {cout << cityNames[v];return;}printPath(parent[v], parent);cout << " -> " << cityNames[v];}// 获取城市名称string getCityName(int id) const {if (id >= 0 && id < cityNames.size()) {return cityNames[id];}return "未知城市";}
};int main() {// 创建城市图(8个城市)CityGraph graph(8);// 添加城市名称graph.addCityName(0, "北京");graph.addCityName(1, "天津");graph.addCityName(2, "石家庄");graph.addCityName(3, "太原");graph.addCityName(4, "济南");graph.addCityName(5, "郑州");graph.addCityName(6, "西安");graph.addCityName(7, "武汉");// 添加城市间道路(距离单位: 公里)graph.addRoad(0, 1, 137); // 北京-天津graph.addRoad(0, 2, 283); // 北京-石家庄graph.addRoad(0, 4, 497); // 北京-济南graph.addRoad(1, 4, 357); // 天津-济南graph.addRoad(2, 3, 231); // 石家庄-太原graph.addRoad(2, 5, 412); // 石家庄-郑州graph.addRoad(3, 6, 511); // 太原-西安graph.addRoad(4, 5, 374); // 济南-郑州graph.addRoad(5, 6, 480); // 郑州-西安graph.addRoad(5, 7, 536); // 郑州-武汉graph.addRoad(6, 7, 691); // 西安-武汉int start = 0; // 北京int end = 7; // 武汉vector<int> distance, parent;graph.findShortestPath(start, end, distance, parent);cout << "城市导航结果:" << endl;cout << "从 " << graph.getCityName(start) << " 到 " << graph.getCityName(end) << " 的最短距离为: " << distance[end] << " 公里" << endl;cout << "推荐路线: ";graph.printPath(end, parent);cout << endl;return 0;
}
24.4 差分约束和最短路径
差分约束系统是一类特殊的线性规划问题,它可以转化为单源最短路径问题来求解。通过将每个约束条件转化为图中的一条边,我们可以利用最短路径算法找到满足所有约束的解。
算法思想
- 建模:将每个变量(x_i)表示为图中的一个节点i。对于每个约束条件(x_j - x_i \leq w),创建一条从节点i到节点j、权重为w的边。
- 引入超级源节点:为了保证图的连通性,引入一个超级源节点 0,并添加从 0 到所有其他节点的边,权重为 0。
- 求解最短路径:使用 Bellman-Ford 算法计算从超级源节点到所有其他节点的最短路径。如果图中存在负权回路,则差分约束系统无解;否则,最短路径距离就是系统的一个可行解。
差分约束系统建模示意图
约束条件:
x1 - x0 ≤ 3
x2 - x0 ≤ 5
x3 - x1 ≤ 2
x2 - x1 ≤ 2
x3 - x2 ≤ -1转化为图:
0 -> 1 (权重3)
0 -> 2 (权重5)
1 -> 3 (权重2)
1 -> 2 (权重2)
2 -> 3 (权重-1)
代码实现
#include <iostream>
#include <vector>
#include <climits>
using namespace std;struct Edge {int u, v, weight;Edge(int u_, int v_, int w_) : u(u_), v(v_), weight(w_) {}
};/*** 求解差分约束系统* @param edges 约束条件转化的边* @param n 变量数量* @param x 存储解的数组* @return 是否有解*/
bool solveDifferenceConstraints(const vector<Edge>& edges, int n, vector<int>& x) {// 添加超级源节点0,连接到所有其他节点vector<Edge> newEdges = edges;for (int i = 1; i <= n; ++i) {newEdges.emplace_back(0, i, 0);}// 使用Bellman-Ford算法求解x.assign(n + 1, INT_MAX); // x[0]到x[n]x[0] = 0;// 松弛n次for (int i = 0; i < n; ++i) {for (const Edge& e : newEdges) {if (x[e.u] != INT_MAX && x[e.v] > x[e.u] + e.weight) {x[e.v] = x[e.u] + e.weight;}}}// 检测负权回路for (const Edge& e : newEdges) {if (x[e.u] != INT_MAX && x[e.v] > x[e.u] + e.weight) {return false; // 存在负权回路,无解}}return true; // 有解
}int main() {// 示例: 求解差分约束系统// 变量: x1, x2, x3// 约束:// x1 - x2 <= 1// x1 - x3 <= 3// x2 - x3 <= 2// x3 - x2 <= -1// x2 - x1 <= -2int n = 3; // 变量数量vector<Edge> edges;// 添加约束对应的边edges.emplace_back(2, 1, 1); // x1 - x2 <= 1edges.emplace_back(3, 1, 3); // x1 - x3 <= 3edges.emplace_back(3, 2, 2); // x2 - x3 <= 2edges.emplace_back(2, 3, -1); // x3 - x2 <= -1edges.emplace_back(1, 2, -2); // x2 - x1 <= -2vector<int> x;bool hasSolution = solveDifferenceConstraints(edges, n, x);if (hasSolution) {cout << "差分约束系统的一个解为:" << endl;for (int i = 1; i <= n; ++i) {cout << "x" << i << " = " << x[i] << endl;}} else {cout << "差分约束系统无解" << endl;}return 0;
}
综合案例及应用
下面是一个课程安排的案例,展示如何使用差分约束系统解决实际问题:
#include <iostream>
#include <vector>
#include <climits>
#include <string>
using namespace std;struct Edge {int u, v, weight;Edge(int u_, int v_, int w_) : u(u_), v(v_), weight(w_) {}
};bool solveCourseScheduling(const vector<Edge>& edges, int n, vector<int>& schedule) {vector<Edge> newEdges = edges;// 添加超级源节点0for (int i = 1; i <= n; ++i) {newEdges.emplace_back(0, i, 0);}schedule.assign(n + 1, INT_MAX);schedule[0] = 0;// 松弛操作for (int i = 0; i < n; ++i) {for (const Edge& e : newEdges) {if (schedule[e.u] != INT_MAX && schedule[e.v] > schedule[e.u] + e.weight) {schedule[e.v] = schedule[e.u] + e.weight;}}}// 检测负权回路for (const Edge& e : newEdges) {if (schedule[e.u] != INT_MAX && schedule[e.v] > schedule[e.u] + e.weight) {return false;}}return true;
}int main() {// 课程安排问题// 课程: 1(算法), 2(数据结构), 3(程序设计), 4(计算机网络), 5(操作系统)// 约束条件:// 1. 数据结构必须在算法之后至少1周: x1 + 1 <= x2 → x2 - x1 >= 1 → x1 - x2 <= -1// 2. 算法必须在程序设计之后: x3 <= x1 → x1 - x3 >= 0 → x3 - x1 <= 0// 3. 计算机网络必须在数据结构之后至少2周: x2 + 2 <= x4 → x4 - x2 >= 2 → x2 - x4 <= -2// 4. 操作系统必须在计算机网络之后: x4 <= x5 → x5 - x4 >= 0 → x4 - x5 <= 0// 5. 操作系统必须在数据结构之后至少3周: x2 + 3 <= x5 → x5 - x2 >= 3 → x2 - x5 <= -3// 6. 所有课程必须在10周内完成: xi <= 10 → xi - x0 <= 10 (x0=0)int n = 5; // 5门课程vector<Edge> edges;vector<string> courseNames = {"", "算法", "数据结构", "程序设计", "计算机网络", "操作系统"};// 添加约束对应的边edges.emplace_back(2, 1, -1); // 约束1edges.emplace_back(1, 3, 0); // 约束2edges.emplace_back(4, 2, -2); // 约束3edges.emplace_back(5, 4, 0); // 约束4edges.emplace_back(5, 2, -3); // 约束5// 所有课程必须在10周内完成for (int i = 1; i <= n; ++i) {edges.emplace_back(i, 0, 10); // xi - x0 <= 10}vector<int> schedule;bool possible = solveCourseScheduling(edges, n, schedule);if (possible) {cout << "课程安排方案:" << endl;for (int i = 1; i <= n; ++i) {cout << courseNames[i] << " 安排在第 " << schedule[i] << " 周" << endl;}cout << "所有课程将在第 " << schedule[5] << " 周前完成" << endl;} else {cout << "无法安排这些课程,存在冲突的先修关系" << endl;}return 0;
}
思考题
比较不同算法:对于一个有 1000 个节点和 10000 条边的图,分别讨论在什么情况下选择 Bellman-Ford、DAG 最短路径算法或 Dijkstra 算法更合适。
负权边处理:如果图中存在负权边但不存在负权回路,Dijkstra 算法是否仍然适用?为什么?设计一个反例来说明你的结论。
算法优化:如何优化 Dijkstra 算法使其在稀疏图上的性能更好?除了优先队列,还有其他可能的优化方法吗?
差分约束扩展:如何将不等式 (x_j - x_i \geq w) 转化为适合差分约束系统的形式?这对应图中的什么边?
最短路径树:证明最短路径树是唯一的当且仅当对于每个节点 v,从源节点 s 到 v 的最短路径是唯一的。
本章注记
单源最短路径问题是图论中的基础问题,本章介绍的算法各有其适用场景:
Bellman-Ford 算法:适用于存在负权边的图,能够检测负权回路,但时间复杂度较高(O (VE))。在实际应用中,其队列优化版本(SPFA 算法)更为常用。
DAG 最短路径算法:是处理有向无环图的高效算法(O (V+E)),可以处理负权边,特别适合任务调度、依赖分析等场景。
Dijkstra 算法:是处理非负权图的高效算法,使用优先队列优化后时间复杂度为 O (ElogV),广泛应用于导航系统、网络路由等领域。
差分约束系统:展示了图论与线性规划的深刻联系,提供了一种求解特定类型约束问题的有效方法。
最短路径算法的研究仍在继续,新的优化方法和变体不断涌现,以适应不同的应用场景和性能需求。例如,针对大规模图的分布式最短路径算法、考虑时效性的动态最短路径算法等,都是当前研究的热点方向。
理解这些算法的原理和性质,不仅有助于我们在实际问题中选择合适的方法,也为学习更复杂的图算法奠定了基础。