图论算法是处理图结构问题的核心工具,广泛应用于路径规划、社交网络分析、计算机网络等领域。以下从基础概念、经典算法及其代码实现展开详细介绍,涵盖 DFS、BFS、最短路径、最小生成树等核心内容,并附 C++ 代码示例及注释。
一、图的基础概念
- 图的定义:由顶点(Vertex)集合 V 和边(Edge)集合 E 组成,记作 G=(V,E)。
- 分类:
- 无向图:边无方向(如社交网络中的朋友关系)。
- 有向图:边有方向(如网页链接关系)。
- 带权图:边附带权重(如地图中的距离)。
- 存储方式:
- 邻接矩阵:用二维数组表示顶点间连接,适合稠密图。
- 邻接表:用链表 / 向量存储相邻顶点,适合稀疏图(本文代码均基于邻接表)。
二、图遍历算法
1. 深度优先搜索(DFS)
- 思想:从起点出发,尽可能深入遍历,遇终点或无法继续时回溯。
- 适用场景:寻找连通分量、检测环、拓扑排序预处理。
- 代码实现(递归与迭代):
cpp
#include <iostream> #include <vector> #include <stack> using namespace std; vector<vector<int>> graph; // 邻接表 vector<bool> visited; // 访问标记 // 递归版DFS void dfs_recursive(int u) { visited[u] = true; cout << u << " "; // 处理当前节点 for (int v : graph[u]) { // 遍历邻接节点 if (!visited[v]) dfs_recursive(v); // 递归访问未访问节点 } } // 迭代版DFS(用栈模拟递归) void dfs_iterative(int start) { stack<int> s; s.push(start); visited[start] = true; while (!s.empty()) { int u = s.top(); s.pop(); cout << u << " "; // 逆序入栈以保证遍历顺序与递归一致(无向图无需逆序) for (int i = graph[u].size() - 1; i >= 0; --i) { int v = graph[u][i]; if (!visited[v]) { visited[v] = true; s.push(v); } } } } // 测试示例 int main() { int n = 5; // 顶点0~4 graph.resize(n); visited.resize(n, false); // 无向图边连接(0-1, 0-2, 1-3, 2-4) graph[0] = {1, 2}; graph[1] = {0, 3}; graph[2] = {0, 4}; graph[3] = {1}; graph[4] = {2}; cout << "递归DFS遍历: "; dfs_recursive(0); // 输出: 0 1 3 2 4 cout << "\n迭代DFS遍历: "; fill(visited.begin(), visited.end(), false); // 重置标记 dfs_iterative(0); // 输出: 0 2 4 1 3 return 0; }
2. 广度优先搜索(BFS)
- 思想:从起点出发,按层遍历所有相邻节点(用队列实现)。
- 适用场景:无权图最短路径、分层结构搜索(如二叉树层序遍历)。
- 代码实现:
cpp
#include <queue> void bfs(int start) { queue<int> q; q.push(start); visited[start] = true; while (!q.empty()) { int u = q.front(); q.pop(); cout << u << " "; for (int v : graph[u]) { // 遍历邻接节点 if (!visited[v]) { visited[v] = true; q.push(v); // 子节点入队 } } } } // 测试示例(沿用DFS的图结构) cout << "\nBFS遍历: "; fill(visited.begin(), visited.end(), false); // 重置标记 bfs(0); // 输出: 0 1 2 3 4
三、最短路径算法
1. Dijkstra 算法(单源最短路径,非负权)
- 思想:贪心策略,维护顶点到起点的最短距离,用优先队列选择当前最短路径的顶点扩展。
- 适用场景:非负权图,如导航系统、网络路由。
- 代码实现(邻接表 + 优先队列优化):
cpp
#include <queue> #include <vector> #include <climits> using namespace std; // 带权图:邻接表存储(顶点,权重) vector<vector<pair<int, int>>> weighted_graph; // dist数组存储起点到各顶点的最短距离 void dijkstra(int start, vector<int>& dist) { int n = weighted_graph.size(); dist.assign(n, INT_MAX); // 初始化为无穷大 dist[start] = 0; priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq; // 小根堆 pq.push({0, start}); // (当前距离,顶点) while (!pq.empty()) { int current_dist, u; tie(current_dist, u) = pq.top(); // 取出当前最短距离的顶点 pq.pop(); if (current_dist > dist[u]) continue; // 跳过旧距离(已找到更短路径) for (auto& [v, w] : weighted_graph[u]) { // 遍历邻接边 if (dist[v] > dist[u] + w) { // 松弛操作 dist[v] = dist[u] + w; pq.push({dist[v], v}); // 新距离入堆 } } } } // 测试示例 int main() { int n = 4; // 顶点0~3 weighted_graph.resize(n); // 边:0→1(2), 0→2(5), 1→2(1), 1→3(3), 2→3(2) weighted_graph[0] = {{1, 2}, {2, 5}}; weighted_graph[1] = {{2, 1}, {3, 3}}; weighted_graph[2] = {{3, 2}}; vector<int> dist(n); dijkstra(0, dist); // 起点为0 // 输出各顶点最短距离:0→0(0), 0→1(2), 0→2(3), 0→3(5) cout << "Dijkstra最短距离: "; for (int d : dist) cout << d << " "; // 输出: 0 2 3 5 return 0; }
2. Floyd-Warshall 算法(全源最短路径)
- 思想:动态规划,用二维数组
dp[i][j]
存储顶点i
到j
的最短路径。 - 适用场景:稠密图,允许负权边(但不能有负权环)。
- 代码实现:
cpp
#include <vector> #include <climits> using namespace std; void floyd_warshall(vector<vector<int>>& dist) { int n = dist.size(); for (int k = 0; k < n; ++k) { // 中间顶点 for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { if (dist[i][k] != INT_MAX && dist[k][j] != INT_MAX) { dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]); // 状态转移 } } } } } // 测试示例 int main() { int n = 4; // 邻接矩阵初始化(INT_MAX表示无直接边) vector<vector<int>> dist = { {0, 2, 5, INT_MAX}, {INT_MAX, 0, 1, 3}, {INT_MAX, INT_MAX, 0, 2}, {INT_MAX, INT_MAX, INT_MAX, 0} }; floyd_warshall(dist); // 输出顶点0到各顶点的最短距离:0→0(0), 0→1(2), 0→2(3), 0→3(5) cout << "Floyd-Warshall 0到3的最短距离: " << dist[0][3] << endl; // 输出: 5 return 0; }
四、最小生成树(MST)算法
1. Kruskal 算法(基于并查集)
- 思想:按边权从小到大排序,用并查集检测环,无环则加入生成树。
- 适用场景:稀疏图的最小生成树。
- 代码实现:
cpp
#include <vector> #include <algorithm> using namespace std; // 边结构:(权重,顶点1,顶点2) struct Edge { int w, u, v; }; // 并查集(Union-Find) class UnionFind { private: vector<int> parent, rank; public: UnionFind(int n) : parent(n), rank(n, 1) { for (int i = 0; i < n; ++i) parent[i] = i; } int find(int x) { // 路径压缩 return parent[x] == x ? x : parent[x] = find(parent[x]); } bool unite(int x, int y) { // 按秩合并 x = find(x); y = find(y); if (x == y) return false; if (rank[x] < rank[y]) swap(x, y); parent[y] = x; rank[x] += rank[y]; return true; } }; int kruskal(int n, vector<Edge>& edges) { sort(edges.begin(), edges.end(), [](Edge a, Edge b) { return a.w < b.w; // 按权重升序排序 }); UnionFind uf(n); int mst_weight = 0; for (Edge e : edges) { if (uf.unite(e.u, e.v)) { // 若不构成环,加入生成树 mst_weight += e.w; } } return mst_weight; // 最小生成树总权重 } // 测试示例 int main() { int n = 4; // 4个顶点 vector<Edge> edges = { {2, 0, 1}, {1, 1, 2}, {3, 1, 3}, {5, 0, 2}, {2, 2, 3} }; // 边权依次为2,1,3,5,2 int total_weight = kruskal(n, edges); // 最小生成树总权重为1+2+2=5 cout << "Kruskal最小生成树权重: " << total_weight << endl; // 输出: 5 return 0; }
2. Prim 算法(邻接表 + 优先队列)
- 思想:从任意顶点出发,每次选择连接已选顶点和未选顶点的最小权边。
- 适用场景:稠密图的最小生成树。
- 代码实现:
cpp
#include <queue> #include <vector> #include <climits> using namespace std; int prim(int n, vector<vector<pair<int, int>>>& graph) { vector<int> key(n, INT_MAX); // 存储各顶点到生成树的最小边权 vector<bool> in_mst(n, false); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq; key[0] = 0; // 从顶点0开始 pq.push({0, 0}); int mst_weight = 0; while (!pq.empty()) { int u_weight, u; tie(u_weight, u) = pq.top(); pq.pop(); if (in_mst[u]) continue; // 已加入生成树,跳过 in_mst[u] = true; mst_weight += u_weight; // 累加边权 for (auto& [v, w] : graph[u]) { // 遍历邻接边 if (!in_mst[v] && w < key[v]) { // 发现更短连接 key[v] = w; pq.push({w, v}); } } } return mst_weight; } // 测试示例(沿用Kruskal的图结构) int main() { int n = 4; vector<vector<pair<int, int>>> graph(n); // 边:0-1(2), 0-2(5), 1-2(1), 1-3(3), 2-3(2) graph[0] = {{1, 2}, {2, 5}}; graph[1] = {{0, 2}, {2, 1}, {3, 3}}; graph[2] = {{0, 5}, {1, 1}, {3, 2}}; graph[3] = {{1, 3}, {2, 2}}; int total_weight = prim(n, graph); // 输出: 5 cout << "Prim最小生成树权重: " << total_weight << endl; return 0; }
五、拓扑排序算法(Kahn 算法)
- 思想:统计入度,每次选择入度为 0 的顶点,更新相邻顶点的入度。
- 适用场景:有向无环图(DAG)的任务调度。
- 代码实现:
cpp
#include <vector> #include <queue> using namespace std; vector<int> topological_sort(vector<vector<int>>& graph, vector<int>& in_degree) { int n = graph.size(); queue<int> q; vector<int> order; // 初始化:入度为0的顶点入队 for (int i = 0; i < n; ++i) { if (in_degree[i] == 0) q.push(i); } while (!q.empty()) { int u = q.front(); q.pop(); order.push_back(u); // 加入拓扑序 for (int v : graph[u]) { // 减少邻接顶点的入度 if (--in_degree[v] == 0) q.push(v); } } return (order.size() == n) ? order : vector<int>(); // 若含环,返回空 } // 测试示例(任务调度:0→1, 0→2, 1→3, 2→3) int main() { int n = 4; vector<vector<int>> graph(n); vector<int> in_degree(n, 0); // 边:0→1, 0→2, 1→3, 2→3 graph[0] = {1, 2}; graph[1] = {3}; graph[2] = {3}; in_degree[1] = 1; in_degree[2] = 1; in_degree[3] = 2; vector<int> order = topological_sort(graph, in_degree); if (order.empty()) cout << "有环!"; else { cout << "拓扑序: "; for (int v : order) cout << v << " "; // 输出: 0 1 2 3 或 0 2 1 3 } return 0; }
六、算法选择指南
问题类型 | 推荐算法 | 时间复杂度 | 关键条件 |
---|---|---|---|
图遍历 | DFS/BFS | O(V+E) | 无向 / 有向图 |
单源最短路径(非负权) | Dijkstra | O(ElogV) | 边权非负 |
单源最短路径(含负权) | Bellman |