当前位置: 首页 > news >正文

最短路算法和最小生成树算法详解

核心概念区分

在开始之前,我们先明确两个概念的区别:

  • 最短路 (Shortest Path):关注的是一个点(源点)到另一个点(或所有其他点)的路径总权重最小。它是一条“路径”。
  • 最小生成树 (Minimum Spanning Tree, MST):关注的是如何用最少的总权重连接图中的所有点,形成一棵树。它是一个覆盖所有顶点的“子图”。

Part 1: 最短路算法 (Shortest Path Algorithms)

最短路问题主要分为两种:

  1. 单源最短路 (Single-Source Shortest Path):计算从一个指定的源点 s 到图中所有其他顶点的最短路径。
  2. 所有顶点对最短路 (All-Pairs Shortest Path):计算图中每一对顶点 (u, v) 之间的最短路径。

1. Dijkstra 算法

Dijkstra 算法是解决单源最短路问题的经典算法,但它有一个重要的前提:图中不能有负权边

  • 核心思想
    Dijkstra 算法采用迭代的方式,维护一个已找到最短路径的顶点集合 S

    1. 初始时,S 中只有源点 s
    2. 每次迭代,从尚未确定最短路径的顶点(V-S 集合)中,选择一个距离源点 s 最近的顶点 u
    3. u 加入 S 集合。
    4. 然后,通过顶点 u 来更新 u 的所有邻居 v 的距离。这个过程称为“松弛”(Relaxation):如果从 s 经过 u 到达 v 的路径比当前已知的 sv 的路径更短,则更新 sv 的距离。
    5. 重复这个过程,直到所有顶点都加入 S
  • 贪心策略分析
    是,Dijkstra 是一个典型的贪心算法。
    它的贪心策略在于:每一步都选择当前看来距离源点最近的未访问顶点。它相信这个局部最优选择(当前最近)就是全局最优选择(这个点的最终最短路径已经确定)。这个贪心策略的正确性依赖于“边的权重非负”这一前提。因为没有负权边,一旦一个点被选为“最近”,就不可能再通过其他未访问的点绕一下,从而找到一条更短的路径了。

  • C++ 代码实现 (使用优先队列优化)
    这是最常见的实现方式,也叫堆优化版的 Dijkstra。时间复杂度为 O(Elog⁡V)O(E \log V)O(ElogV)

#include <iostream>
#include <vector>
#include <queue>
#include <limits>const int INF = std::numeric_limits<int>::max();// 图的边结构
struct Edge {int to;int weight;
};// 优先队列中存储的节点状态
struct State {int u;int dist;// 自定义比较函数,使其成为最小堆bool operator>(const State& other) const {return dist > other.dist;}
};void dijkstra(int start, int n, const std::vector<std::vector<Edge>>& adj) {std::vector<int> dist(n + 1, INF);std::vector<bool> visited(n + 1, false);std::priority_queue<State, std::vector<State>, std::greater<State>> pq;dist[start] = 0;pq.push({start, 0});while (!pq.empty()) {State current = pq.top();pq.pop();int u = current.u;// 如果已经处理过,则跳过if (visited[u]) {continue;}visited[u] = true;// 松弛操作for (const auto& edge : adj[u]) {int v = edge.to;int weight = edge.weight;if (dist[u] != INF && dist[u] + weight < dist[v]) {dist[v] = dist[u] + weight;pq.push({v, dist[v]});}}}// 打印结果std::cout << "Dijkstra: Shortest distances from source " << start << ":\n";for (int i = 1; i <= n; ++i) {if (dist[i] == INF) {std::cout << "Node " << i << ": Infinity\n";} else {std::cout << "Node " << i << ": " << dist[i] << "\n";}}
}int main() {int n = 5, m = 7, start = 1;std::vector<std::vector<Edge>> adj(n + 1);// 添加边: from, to, weightadj[1].push_back({2, 10});adj[1].push_back({4, 5});adj[2].push_back({3, 1});adj[2].push_back({4, 2});adj[3].push_back({5, 4});adj[4].push_back({3, 9});adj[4].push_back({5, 2});dijkstra(start, n, adj);return 0;
}

2. Bellman-Ford 算法

Bellman-Ford 算法也是解决单源最短路问题,但它的优势在于可以处理带负权边的图,并且能够检测出负权环

  • 核心思想
    算法基于动态规划的思想。它对图中的所有边进行 V-1 轮松弛操作。

    1. 初始化源点 s 的距离为 0,其他所有点为无穷大。
    2. 进行 V-1 轮迭代。在每一轮中,遍历图中的所有边 (u, v),并进行松弛操作:如果 dist[u] + weight(u, v) < dist[v],则更新 dist[v]
    3. V-1 轮后,如果图中不存在负权环,那么所有顶点的最短路径就已经计算出来了。
    4. 为了检测负权环,再进行第 V 轮松弛。如果在这一轮中,仍然有边的距离可以被更新,说明图中存在一个从源点可达的负权环。
  • 贪心策略分析
    否,Bellman-Ford 不是贪心算法。
    它没有在每一步做出局部最优的选择。相反,它是一种非常“暴力”或者说“系统性”的方法。它通过 V-1 次迭代,系统地计算出所有最多经过1条边、2条边、…、直到 V-1 条边的最短路径。它的思想更接近于动态规划,dp[k][v] 表示从源点出发最多经过 k 条边到达 v 的最短路径。

  • C++ 代码实现
    时间复杂度为 O(V⋅E)O(V \cdot E)O(VE)

#include <iostream>
#include <vector>
#include <limits>const int INF = std::numeric_limits<int>::max();struct Edge {int from;int to;int weight;
};void bellman_ford(int start, int n, int m, const std::vector<Edge>& edges) {std::vector<int> dist(n + 1, INF);dist[start] = 0;// V-1 轮松弛for (int i = 0; i < n - 1; ++i) {for (const auto& edge : edges) {if (dist[edge.from] != INF && dist[edge.from] + edge.weight < dist[edge.to]) {dist[edge.to] = dist[edge.from] + edge.weight;}}}// 检测负权环bool has_negative_cycle = false;for (const auto& edge : edges) {if (dist[edge.from] != INF && dist[edge.from] + edge.weight < dist[edge.to]) {has_negative_cycle = true;break;}}if (has_negative_cycle) {std::cout << "Bellman-Ford: Graph contains a negative-weight cycle.\n";} else {std::cout << "Bellman-Ford: Shortest distances from source " << start << ":\n";for (int i = 1; i <= n; ++i) {if (dist[i] == INF) {std::cout << "Node " << i << ": Infinity\n";} else {std::cout << "Node " << i << ": " << dist[i] << "\n";}}}
}int main() {int n = 5, m = 8, start = 1;std::vector<Edge> edges = {{1, 2, 6}, {1, 3, 7}, {2, 4, 5}, {2, 5, -4},{3, 2, 8}, {3, 4, -3}, {4, 5, 9}, {5, 2, 2}};// 示例负权环// std::vector<Edge> edges_with_cycle = {//     {1, 2, 1}, {2, 3, 1}, {3, 1, -3}// };bellman_ford(start, n, m, edges);return 0;
}

3. SPFA 算法 (Shortest Path Faster Algorithm)

SPFA 是 Bellman-Ford 算法的队列优化版本,在很多情况下(特别是稀疏图)比 Bellman-Ford 更快。它同样可以处理负权边检测负权环

  • 核心思想
    Bellman-Ford 每一轮都盲目地松弛所有边。SPFA 观察到,只有当一个点 udist 值变小时,才可能引起其邻居 vdist 值变小。因此,SPFA 使用一个队列来维护那些 dist 值刚刚被更新过的顶点。

    1. 将源点 s 入队。
    2. 当队列不为空时,出队一个顶点 u
    3. u 的所有邻居 v 进行松弛操作。
    4. 如果 vdist 值被更新了,并且 v 不在队列中,则将 v 入队。
    5. 为了检测负权环,可以记录每个顶点入队的次数。如果一个顶点入队次数超过 V 次,则说明存在负权环。
  • 贪心策略分析
    否,SPFA 也不是贪心算法。
    它处理顶点的顺序是由队列的先进先出(FIFO)特性决定的,而不是基于任何局部最优的属性(如距离大小)。它是一种动态的、基于传播的松弛方法。

  • C++ 代码实现
    平均时间复杂度为 O(kE)O(kE)O(kE),其中 k 是一个小的常数。但在最坏情况下(例如在特殊构造的网格图中),会退化到 O(V⋅E)O(V \cdot E)O(VE)

#include <iostream>
#include <vector>
#include <queue>
#include <limits>const int INF = std::numeric_limits<int>::max();struct Edge {int to;int weight;
};bool spfa(int start, int n, const std::vector<std::vector<Edge>>& adj) {std::vector<int> dist(n + 1, INF);std::vector<bool> in_queue(n + 1, false);std::vector<int> count(n + 1, 0); // 记录入队次数std::queue<int> q;dist[start] = 0;q.push(start);in_queue[start] = true;count[start]++;while (!q.empty()) {int u = q.front();q.pop();in_queue[u] = false;for (const auto& edge : adj[u]) {int v = edge.to;int weight = edge.weight;if (dist[u] != INF && dist[u] + weight < dist[v]) {dist[v] = dist[u] + weight;if (!in_queue[v]) {q.push(v);in_queue[v] = true;count[v]++;if (count[v] > n) {std::cout << "SPFA: Graph contains a negative-weight cycle.\n";return false; // 发现负权环}}}}}std::cout << "SPFA: Shortest distances from source " << start << ":\n";for (int i = 1; i <= n; ++i) {if (dist[i] == INF) {std::cout << "Node " << i << ": Infinity\n";} else {std::cout << "Node " << i << ": " << dist[i] << "\n";}}return true;
}int main() {int n = 5, start = 1;std::vector<std::vector<Edge>> adj(n + 1);adj[1].push_back({2, 6});adj[1].push_back({3, 7});adj[2].push_back({4, 5});adj[2].push_back({5, -4});adj[3].push_back({2, 8});adj[3].push_back({4, -3});adj[4].push_back({5, 9});adj[5].push_back({2, 2});spfa(start, n, adj);return 0;
}

4. Floyd-Warshall 算法

Floyd-Warshall 算法用于解决所有顶点对最短路问题。它可以处理带负权边的图,但不能处理负权环(如果存在负权环,算法可以检测出来)。

  • 核心思想
    这是一个非常经典的动态规划算法。它使用一个二维数组 dp[i][j] 来存储从顶点 ij 的最短路径长度。算法的核心思想是:对于每一对顶点 (i, j),我们尝试通过一个中间点 k 来缩短它们的路径。

    1. 初始化 dp[i][j]ij 的直接边权(如果无直连边则为无穷大),dp[i][i] 为 0。
    2. 进行 V 轮迭代,k 从 1 到 V
    3. 在第 k 轮迭代中,对于所有的顶点对 (i, j),检查 dp[i][k] + dp[k][j] 是否小于 dp[i][j]。如果是,则更新 dp[i][j]
      dp[i][j]=min⁡(dp[i][j],dp[i][k]+dp[k][j]) dp[i][j] = \min(dp[i][j], dp[i][k] + dp[k][j]) dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])
    4. 所有迭代结束后,dp[i][j] 就存储了 ij 的最短路径。如果 dp[i][i] < 0,则说明存在从 i 出发的负权环。
  • 贪心策略分析
    否,Floyd-Warshall 是动态规划算法,不是贪心算法。
    它没有做出任何局部最优选择。相反,它通过引入中间点 k,系统地、由简到繁地构建出最终解。dp[k][i][j] 的含义是“只允许经过前 k 个点作为中间点时,从 ij 的最短路径”,这是典型的动态规划状态转移。

  • C++ 代码实现
    时间复杂度为 O(V3)O(V^3)O(V3)

#include <iostream>
#include <vector>
#include <algorithm>const int INF = 1e9; // 使用一个较大的数表示无穷大void floyd_warshall(int n, std::vector<std::vector<int>>& dist) {// 核心 DP 过程for (int k = 1; k <= n; ++k) {for (int i = 1; i <= n; ++i) {for (int j = 1; j <= n; ++j) {if (dist[i][k] != INF && dist[k][j] != INF) {dist[i][j] = std::min(dist[i][j], dist[i][k] + dist[k][j]);}}}}// 打印结果std::cout << "Floyd-Warshall: All-Pairs Shortest Paths:\n";for (int i = 1; i <= n; ++i) {for (int j = 1; j <= n; ++j) {if (dist[i][j] == INF) {std::cout << "INF\t";} else {std::cout << dist[i][j] << "\t";}}std::cout << "\n";}
}int main() {int n = 4;std::vector<std::vector<int>> dist(n + 1, std::vector<int>(n + 1, INF));// 初始化邻接矩阵for (int i = 1; i <= n; ++i) dist[i][i] = 0;dist[1][2] = 3;dist[1][4] = 5;dist[2][1] = 2;dist[2][4] = 4;dist[3][2] = -2;dist[4][3] = 1;floyd_warshall(n, dist);return 0;
}

Part 2: 最小生成树算法 (Minimum Spanning Tree Algorithms)

最小生成树(MST)的目标是在一个加权的无向连通图中,找出一个边的子集,这个子集连接了图中所有的顶点,且没有环,同时总权重最小。

1. Prim 算法

  • 核心思想
    Prim 算法和 Dijkstra 算法非常相似。它也是从一个顶点开始,逐步扩大一棵树。

    1. 选择任意一个顶点作为起始点,将其加入到生成树集合 T 中。
    2. 维护一个 dist 数组,dist[i] 表示顶点 i 到集合 T 的最短边的权重。
    3. 重复以下步骤 V-1 次:
      a. 从所有不在 T 中的顶点里,找到 dist 值最小的顶点 u
      b. 将 u 和连接它与 T 的那条最短边加入到生成树中。
      c. 更新 u 的所有邻居 vdist 值:如果 uv 的边权小于当前的 dist[v],则更新 dist[v]
  • 贪心策略分析
    是,Prim 是一个典型的贪心算法。
    它的贪心策略是:每一步都选择一条权重最小的边,这条边连接一个已在树中的顶点和一个未在树中的顶点。它总是选择“扩张”这棵树的“最便宜”的方式。这个贪心选择的正确性由图论中的“切割定理”(Cut Property)保证:对于图的任意一个切分(将顶点分为两部分),连接这两个部分的权重最小的边,必然属于图的某个最小生成树。Prim 算法的每一步选择都符合这个定理。

  • C++ 代码实现 (类似 Dijkstra 的优先队列优化)
    时间复杂度为 O(Elog⁡V)O(E \log V)O(ElogV)

#include <iostream>
#include <vector>
#include <queue>
#include <limits>const int INF = std::numeric_limits<int>::max();// 边结构
struct Edge {int to;int weight;
};// 优先队列中存储的状态
struct State {int u;int key; // 类似 dist,表示到树的最小边权bool operator>(const State& other) const {return key > other.key;}
};void prim(int n, const std::vector<std::vector<Edge>>& adj) {std::vector<int> key(n + 1, INF);std::vector<int> parent(n + 1, -1);std::vector<bool> in_mst(n + 1, false);std::priority_queue<State, std::vector<State>, std::greater<State>> pq;int start_node = 1;key[start_node] = 0;pq.push({start_node, 0});long long total_weight = 0;while (!pq.empty()) {State current = pq.top();pq.pop();int u = current.u;if (in_mst[u]) {continue;}in_mst[u] = true;total_weight += current.key;for (const auto& edge : adj[u]) {int v = edge.to;int weight = edge.weight;if (!in_mst[v] && weight < key[v]) {key[v] = weight;parent[v] = u;pq.push({v, key[v]});}}}std::cout << "Prim's Algorithm: Minimum Spanning Tree Weight = " << total_weight << "\n";std::cout << "Edges in MST:\n";for (int i = 2; i <= n; ++i) {std::cout << parent[i] << " - " << i << "\n";}
}int main() {int n = 5;std::vector<std::vector<Edge>> adj(n + 1);// 无向图,加两次边auto add_edge = [&](int u, int v, int w) {adj[u].push_back({v, w});adj[v].push_back({u, w});};add_edge(1, 2, 2);add_edge(1, 3, 6);add_edge(2, 3, 1);add_edge(2, 4, 5);add_edge(3, 4, 8);add_edge(3, 5, 7);add_edge(4, 5, 9);prim(n, adj);return 0;
}

2. Kruskal 算法

  • 核心思想
    Kruskal 算法的思路与 Prim 完全不同,它关注的是全局的边,而不是局部的点。

    1. 将图中所有的边按权重从小到大排序。
    2. 创建一个森林,其中每个顶点自成一棵树。
    3. 遍历排序后的边。对于每一条边 (u, v),如果 uv 不在同一棵树中(即加入这条边不会形成环),则将这条边加入到最小生成树中,并合并 uv 所在的树。
    4. 重复此过程,直到加入了 V-1 条边。
      判断两个顶点是否在同一棵树中,通常使用**并查集(Disjoint Set Union, DSU)**数据结构来高效实现。
  • 贪心策略分析
    是,Kruskal 也是一个经典的贪心算法。
    它的贪心策略是:在所有可选的边中,永远选择权重最小的,并且保证这个选择不会破坏树的结构(即不形成环)。它总是从全局视角做出最“便宜”的选择。这个贪心策略的正确性同样可以由“切割定理”证明。每当算法选择一条边时,这条边必然是连接某个切分两部分的最小权重边。

  • C++ 代码实现 (使用并查集)
    时间复杂度为 O(Elog⁡E)O(E \log E)O(ElogE)(主要是排序的开销)。

#include <iostream>
#include <vector>
#include <algorithm>struct Edge {int from, to, weight;
};bool compareEdges(const Edge& a, const Edge& b) {return a.weight < b.weight;
}// 并查集 (Disjoint Set Union)
struct DSU {std::vector<int> parent;DSU(int n) {parent.resize(n + 1);for (int i = 1; i <= n; ++i) parent[i] = i;}int find(int i) {if (parent[i] == i) return i;return parent[i] = find(parent[i]); // 路径压缩}void unite(int i, int j) {int root_i = find(i);int root_j = find(j);if (root_i != root_j) {parent[root_i] = root_j;}}
};void kruskal(int n, std::vector<Edge>& edges) {// 1. 排序所有边std::sort(edges.begin(), edges.end(), compareEdges);DSU dsu(n);long long total_weight = 0;std::vector<Edge> mst_edges;// 2. 遍历边并构建MSTfor (const auto& edge : edges) {if (dsu.find(edge.from) != dsu.find(edge.to)) {dsu.unite(edge.from, edge.to);total_weight += edge.weight;mst_edges.push_back(edge);}}std::cout << "Kruskal's Algorithm: Minimum Spanning Tree Weight = " << total_weight << "\n";std::cout << "Edges in MST:\n";for (const auto& edge : mst_edges) {std::cout << edge.from << " - " << edge.to << " (weight " << edge.weight << ")\n";}
}int main() {int n = 5, m = 7;std::vector<Edge> edges = {{1, 2, 2}, {1, 3, 6}, {2, 3, 1}, {2, 4, 5},{3, 4, 8}, {3, 5, 7}, {4, 5, 9}};kruskal(n, edges);return 0;
}

总结

算法解决问题核心思想时间复杂度负权边是否贪心
Dijkstra单源最短路选最近的点进行扩展O(Elog⁡V)O(E \log V)O(ElogV)不支持
Bellman-Ford单源最短路对所有边进行V-1轮松弛O(V⋅E)O(V \cdot E)O(VE)支持,可检负权环否 (DP)
SPFA单源最短路Bellman-Ford的队列优化平均O(kE)O(kE)O(kE),最坏O(V⋅E)O(V \cdot E)O(VE)支持,可检负权环
Floyd-Warshall所有顶点对最短路动态规划,枚举中转点O(V3)O(V^3)O(V3)支持,不可有负权环否 (DP)
Prim最小生成树从一个点开始,逐步加点入树O(Elog⁡V)O(E \log V)O(ElogV)支持 (无意义)
Kruskal最小生成树按边权排序,逐步加边成树O(Elog⁡E)O(E \log E)O(ElogE)支持 (无意义)

:对于最小生成树算法,负权边是“支持”的,因为其目标是总权重最小,负权边只会让这个总权重更小,不影响算法正确性。但在实际问题中,MST的边权通常表示成本、距离等非负概念。


文章转载自:

http://xPF8RWsD.jfjfk.cn
http://801Pq5w7.jfjfk.cn
http://iNMOa8Zm.jfjfk.cn
http://BEAenjy1.jfjfk.cn
http://zsuG0yB9.jfjfk.cn
http://I76PgRr9.jfjfk.cn
http://i53an04N.jfjfk.cn
http://U5Dqydji.jfjfk.cn
http://QCz9JXMR.jfjfk.cn
http://XX7SJSaE.jfjfk.cn
http://OyIQk8O6.jfjfk.cn
http://FrHpudE8.jfjfk.cn
http://h2RaKZZZ.jfjfk.cn
http://CqwbSfju.jfjfk.cn
http://n8hYv2hp.jfjfk.cn
http://8WwShrVC.jfjfk.cn
http://DLuqxYgK.jfjfk.cn
http://1Z07op06.jfjfk.cn
http://myps5mcj.jfjfk.cn
http://qnJVtHEY.jfjfk.cn
http://Lb7KJHIO.jfjfk.cn
http://cmiiXk7f.jfjfk.cn
http://mjPGy2Ti.jfjfk.cn
http://aMzqsbNk.jfjfk.cn
http://BiZ5JRCj.jfjfk.cn
http://YdqWVj0L.jfjfk.cn
http://Pnbam7vI.jfjfk.cn
http://5seQUTab.jfjfk.cn
http://LjMiDFFa.jfjfk.cn
http://Qpbu6hI6.jfjfk.cn
http://www.dtcms.com/a/373228.html

相关文章:

  • 2005–2021年中国城市级终端能源消费(含可再生能源)综合数据集
  • Redis入门(部署、持久化、缓存问题)
  • 聊一聊 .NET 中的 CompositeChangeToken
  • 视觉语言模型应用开发——Qwen 2.5 VL模型视频理解与定位能力深度解析及实践指南
  • 深入理解 MDC(Mapped Diagnostic Context):日志记录的利器
  • 工业相机如何通过光度立体成像技术实现高效精准的2.5D缺陷检测
  • qt+halcon开发相机拍照软件步骤
  • cs61A lab01
  • 大数据毕业设计选题推荐-基于大数据的国家医用消耗选品采集数据可视化分析系统-Hadoop-Spark-数据可视化-BigData
  • Oracle APEX 利用卡片实现翻转
  • Spring Security AuthenticationManager 接口详解与实战
  • 人机协同的智慧共生平台:跨学科知识中心暨融智中心,从认知到实践的闭环自动转化
  • AG32 ( MCU+FPGA二合一 )是如何卷入了三相电能计量市场的
  • 2025年- H119-Lc88. 合并两个有序数组(数组)--Java版
  • 树莓派 Ubuntu 24.04 开机换源总结
  • 简单的 k8s 部署分布式Go微服务集群实例
  • 旅行社旅游管理系统的设计与实现(代码+数据库+LW)
  • Three.js shader内置矩阵注入
  • 在公用同一公网IP和端口的K8S环境中,不同域名实现不同访问需求的解决方案
  • 【MFC视图和窗口基础:文档/视图的“双胞胎”魔法 + 单文档程序】
  • Cocos creator3.x 处理 16KB 问题
  • 【MFC文档与视图结构:数据“仓库”与“橱窗”的梦幻联动 + 初始化“黑箱”大揭秘!】
  • 【MFC】对话框属性:Use System Font(使用系统字体)
  • springboot3.3.5 集成elasticsearch8.12.2 ssl 通过 SSL bundle name 来实现
  • ARM寄存器以及异常处理
  • vim修订版本
  • 代码随想录刷题——栈与队列篇(理论)
  • 【机器学习】27 Latent variable models for discrete data
  • 【混合开发】vue+Android、iPhone、鸿蒙、win、macOS、Linux之video 的各种状态和生命周期调用说明
  • MAC在home下新建文件夹报错“mkdir: test: Operation not supported”