图论核心知识点笔记 一、图的存储方式 图的存储核心是记录“顶点”与“边”的关联关系,需根据图的密度(边数/顶点数²)选择合适方式:
1. 邻接矩阵
实现原理 :用二维数组 graph[i][j] 表示顶点间关系
无权图:graph[i][j] = 1 表示顶点i与j有边,0 表示无边;
加权图:graph[i][j] = 边的权重(无边时设为无穷大);
有向图:graph[i][j] 仅表示“i→j”的边,方向不同值独立。
优缺点 : ✅ 优点:查询两点是否连通、修改边权效率高(O(1)); ❌ 缺点:空间复杂度O(n²)(n为顶点数),稀疏图浪费空间。
适用场景 :稠密图(边数接近n²)、需频繁判断两点连通性的场景。
2. 邻接表
实现原理 :用“数组+链表/vector”存储,数组下标对应顶点,每个元素存储该顶点的所有邻接顶点(及边权)。 例:vector<vector<int>> adj(n),adj[i] 存储顶点i的所有邻接顶点编号。
优缺点 : ✅ 优点:空间复杂度O(n+m)(m为边数),稀疏图高效;遍历某顶点所有邻接边方便; ❌ 缺点:查询两点是否连通需遍历邻接表(O(度),度为顶点的边数)。
适用场景 :稀疏图(边数远小于n²)、需频繁遍历邻接边的场景(如DFS/BFS、最短路径算法)。
二、图的遍历算法 遍历是图论基础操作,核心是“不重复访问顶点”,为后续复杂算法提供支撑:
1. 深度优先搜索(DFS)
void DFS_iterative (const AdjacencyList& adj, int start_node) { int N = adj.size (); if (N == 0 ) return ; std::vector<bool > visited (N, false ) ; std::stack<int > s; std::cout << "DFS 访问顺序 (迭代版从 " << start_node << "): " ; s.push (start_node); while (!s.empty ()) { int u = s.top (); s.pop (); if (visited[u]) { continue ; } visited[u] = true ; print_visit (u); for (int i = adj[u].size () - 1 ; i >= 0 ; --i) { int v = adj[u][i]; if (!visited[v]) { s.push (v); } } } std::cout << std::endl; } void DFS_recursive (const AdjacencyList& adj, int u, std::vector<bool >& visited) { visited[u] = true ; print_visit (u); for (int v : adj[u]) { if (!visited[v]) { DFS_recursive (adj, v, visited); } } } void DFS_Traversal (const AdjacencyList& adj, int start_node) { int N = adj.size (); if (N == 0 ) return ; std::vector<bool > visited (N, false ) ; std::cout << "DFS 访问顺序 (从 " << start_node << "): " ; DFS_recursive (adj, start_node, visited); std::cout << std::endl; } void DFS_Traversal (const AdjacencyList& adj, int start_node) { int N = adj.size (); if (N == 0 ) return ; std::vector<bool > visited (N, false ) ; std::cout << "DFS 访问顺序 (递归版从 " << start_node << "): " ; DFS_recursive (adj, start_node, visited); std::cout << std::endl; DFS_iterative (adj, start_node); }
2. 广度优先搜索(BFS)
void BFS_Traversal (const AdjacencyList& adj, int start_node) { int N = adj.size (); if (N == 0 ) return ; std::vector<bool > visited (N, false ) ; std::queue<int > q; std::cout << "BFS 访问顺序 (从 " << start_node << "): " ; q.push (start_node); visited[start_node] = true ; while (!q.empty ()) { int u = q.front (); q.pop (); print_visit (u); for (int v : adj[u]) { if (!visited[v]) { visited[v] = true ; q.push (v); } } } std::cout << std::endl; }
三、图的核心应用算法 1. 最小生成树(MST)
生成树:所以顶点均连在一起,但不存在回路的图
问题定义 :针对“无向加权连通图”,找到一棵包含所有顶点、边权和最小的生成树(无环,边数=n-1)。
核心目标 :连接所有顶点,总权值最小,无环。
(1)Prim算法
核心思想 :“加点法”——从任意起点出发,每次选择“连接当前生成树与非树顶点”的最小权边,将该非树顶点加入生成树,重复至所有顶点加入。
实现要点 :
用数组 dist[] 记录每个非树顶点到生成树的最小距离;
每次选 dist[] 中最小的顶点加入树,更新其他顶点的 dist[]。
时间复杂度 :O(m log n)(堆优化版)。
适用场景 :稠密图(顶点少、边多),堆优化后也适用于稀疏图。
(2)Kruskal算法
核心思想 :“加边法”——按边权从小到大排序,依次选边,若边的两个顶点不在同一连通分量(用并查集判断),则加入生成树,重复至生成树有n-1条边。
实现要点 :
依赖并查集(DSU)判断环(路径压缩+按秩合并优化);
边排序是核心步骤(时间复杂度O(m log m))。
时间复杂度 :O(m log m)(主要开销在边排序)。
适用场景 :稀疏图(边少,排序开销小)。
2. 最短路径 (1)Dijkstra算法
问题场景 :单源最短路径(从一个起点到所有其他顶点)、边权非负。
核心思想 :
==若顶点 U 是当前「未确定最短路径」的顶点中,距离起点 S 最近的那个,那么 S→U 的最短路径已经确定(后续不会再找到更短的路径)==。
确定最短路径顶点 → 用该顶点更新邻接顶点的距离 → 重复直到所有顶点都确定
实现要点 :
关键数组:dist [](距离记录数组)
作用:存储「起点 S 到每个顶点的当前最短距离」。
当我们确定顶点 U 的最短路径后,遍历 U 的所有邻接顶点 V。如果 dist[U] + 边权(U→V) < dist[V],说明找到了一条从 S→U→V 的更短路径,就把 dist[V] 更新为这个更小的值(这个过程叫「松弛操作」)。
优先队列(小根堆):高效找 “最近未确定顶点”
作用:存储「未确定最短路径的顶点 + 其当前距离起点的距离」,核心是 O (log n) 时间内快速取出当前距离最小的顶点 。
辅助数组:visited [](可选,标记已确定顶点)
当从堆中取出顶点 U 时,先检查 visited[U]。如果是 true,说明已经处理过更优路径,直接跳过;如果是 false,则处理 U 的邻接顶点,处理完后设 visited[U] = true。
时间复杂度 :O(m log n)(堆优化版)。
注意事项 :不能处理负权边(贪心策略会失效)。
#include <iostream> #include <vector> #include <queue> #include <climits> using namespace std;const int INF = INT_MAX;struct Edge { int to; int weight; Edge (int t, int w) : to (t), weight (w) {} }; vector<int > dijkstra (const vector<vector<Edge>>& graph, int start, int n) { vector<int > dist (n, INF) ; dist[start] = 0 ; priority_queue<pair<int , int >, vector<pair<int , int >>, greater<pair<int , int >>> pq; pq.push ({0 , start}); vector<bool > visited (n, false ) ; while (!pq.empty ()) { int current_dist = pq.top ().first; int u = pq.top ().second; pq.pop (); if (visited[u]) continue ; visited[u] = true ; for (const Edge& edge : graph[u]) { int v = edge.to; int weight = edge.weight; if (dist[u] != INF && dist[v] > dist[u] + weight) { dist[v] = dist[u] + weight; pq.push ({dist[v], v}); } } } return dist; } int main () { int n = 4 ; vector<vector<Edge>> graph (n); graph[0 ].emplace_back (1 , 4 ); graph[0 ].emplace_back (2 , 1 ); graph[2 ].emplace_back (1 , 2 ); graph[1 ].emplace_back (3 , 1 ); graph[2 ].emplace_back (3 , 5 ); int start = 0 ; vector<int > shortest_distances = dijkstra (graph, start, n); cout << "从起点 " << start << " 到各顶点的最短距离:" << endl; for (int i = 0 ; i < n; ++i) { if (shortest_distances[i] == INF) { cout << "到顶点 " << i << ":不可达" << endl; } else { cout << "到顶点 " << i << ":" << shortest_distances[i] << endl; } } return 0 ; }
(2)Bellman-Ford算法
问题场景 :单源最短路径、允许负权边、检测负权环。
核心思想 :
松弛操作:对于边 u→v(边权为 w),如果当前起点到 u 的距离 dist[u] 加上边权 w,比起点到 v 的当前距离 dist[v] 更小,就更新 dist[v] = dist[u] + w。
为什么要迭代 n-1 次:
最短路径不会包含环(如果包含正权环,去掉环路径更短;如果包含负权环,就没有最短路径),因此最短路径是「无环路径」,最多经过所有顶点一次,即 n-1 条边。
第 n-1 次迭代:能找到所有 “从起点出发,经过最多 n-1 条边” 的最短路径(即所有可能的最短路径)。
时间复杂度 : 。
适用场景 :需处理负权边或检测负权环的场景(如“判断图中是否存在总权为负的环”)。
#include <iostream> #include <vector> #include <climits> using namespace std;const int INF = INT_MAX;struct Edge { int from; int to; int weight; Edge (int f, int t, int w) : from (f), to (t), weight (w) {} }; vector<int > bellman_ford (const vector<Edge>& edges, int start, int n, bool & has_negative_cycle) { vector<int > dist (n, INF) ; dist[start] = 0 ; for (int i = 0 ; i < n - 1 ; ++i) { bool updated = false ; for (const Edge& edge : edges) { int u = edge.from; int v = edge.to; int weight = edge.weight; if (dist[u] != INF && dist[v] > dist[u] + weight) { dist[v] = dist[u] + weight; updated = true ; } } if (!updated) break ; } has_negative_cycle = false ; for (const Edge& edge : edges) { int u = edge.from; int v = edge.to; int weight = edge.weight; if (dist[u] != INF && dist[v] > dist[u] + weight) { has_negative_cycle = true ; break ; } } return dist; } int main () { int n = 4 ; vector<Edge> edges; edges.emplace_back (0 , 1 , 4 ); edges.emplace_back (0 , 2 , 1 ); edges.emplace_back (2 , 1 , 2 ); edges.emplace_back (1 , 3 , 1 ); edges.emplace_back (2 , 3 , -5 ); int start = 0 ; bool has_negative_cycle; vector<int > shortest_distances = bellman_ford (edges, start, n, has_negative_cycle); if (has_negative_cycle) { cout << "图中存在负权环,无法计算最短路径!" << endl; } else { cout << "从起点 " << start << " 到各顶点的最短距离:" << endl; for (int i = 0 ; i < n; ++i) { if (shortest_distances[i] == INF) { cout << "到顶点 " << i << ":不可达" << endl; } else { cout << "到顶点 " << i << ":" << shortest_distances[i] << endl; } } } cout << "\n--- 测试存在负权环的图 ---" << endl; int n2 = 3 ; vector<Edge> edges2; edges2. emplace_back (0 , 1 , 1 ); edges2. emplace_back (1 , 2 , -2 ); edges2. emplace_back (2 , 1 , -1 ); vector<int > shortest_distances2 = bellman_ford (edges2, 0 , n2, has_negative_cycle); if (has_negative_cycle) { cout << "图中存在负权环,无法计算最短路径!" << endl; } else { } return 0 ; }
(3)Floyd-Warshall算法
问题场景 :多源最短路径(所有顶点间的最短路径)、允许负权边(无负权环)。
核心思想 :动态规划——dp[i][j] 表示顶点i到j的最短路径,通过中间顶点k更新:dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j])。
实现要点 :
用二维数组 dp 存储最短路径矩阵,初始化为邻接矩阵;
三层循环:枚举中间顶点k→枚举起点i→枚举终点j。
时间复杂度 :O(n³)。
适用场景 :顶点数少(n≤100)的多源路径问题(代码简洁,无需复杂数据结构)。
#include <iostream> #include <vector> #include <climits> using namespace std;const int INF = INT_MAX;vector<vector<int >> floyd_warshall (vector<vector<int >> graph, int n) { vector<vector<int >> dp (n, vector <int >(n)); for (int i = 0 ; i < n; ++i) { for (int j = 0 ; j < n; ++j) { dp[i][j] = graph[i][j]; } } for (int k = 0 ; k < n; ++k) { for (int i = 0 ; i < n; ++i) { for (int j = 0 ; j < n; ++j) { if (dp[i][k] != INF && dp[k][j] != INF && dp[i][j] > dp[i][k] + dp[k][j]) { dp[i][j] = dp[i][k] + dp[k][j]; } } } } return dp; } int main () { int n = 4 ; vector<vector<int >> graph = { {0 , 4 , 1 , INF}, {INF, 0 , INF, 1 }, {INF, 2 , 0 , 5 }, {INF, INF, INF, 0 } }; vector<vector<int >> shortest_paths = floyd_warshall (graph, n); cout << "所有顶点间的最短路径矩阵:" << endl; for (int i = 0 ; i < n; ++i) { for (int j = 0 ; j < n; ++j) { if (shortest_paths[i][j] == INF) { cout << "INF\t" ; } else { cout << shortest_paths[i][j] << "\t" ; } } cout << endl; } return 0 ; }
3. 拓扑排序
问题场景 :有向无环图(DAG),输出一个“所有边的起点在终点之前”的顶点序列(若有环则无法拓扑排序)。
核心目标 :验证图是否为 DAG,或生成符合依赖关系的线性序列。
时间复杂度 : ,其中 是顶点数, 是边数。每个顶点入队/递归一次,每条边被扫描一次。
实现方式 : (1)BFS 版(Kahn 算法/入度表法) : 1.统计所有顶点的入度,入度为 0 的顶点入队;
2.出队顶点并加入结果序列,遍历其所有出边,对应邻接顶点的入度减 1;
3.若邻接顶点入度变为 0 则入队,重复至队空;
4.若最终输出顶点数 $\neq$ 总顶点数,说明图中存在环。
(2)DFS 版(回溯记录法) : 1.使用状态数组标记顶点(未访问、访问中、已完成);
2.对每个未访问顶点进行 DFS:若遇到“访问中”的节点,说明存在**后向边**(有环);
3.在顶点遍历结束(即将回溯)时,将其压入结果栈或列表;
4.最终将结果列表**逆序**输出,即为拓扑序列。
应用场景 :任务调度(如先修课规划)、编译依赖分析、项目进度管理。
#include <iostream> #include <vector> #include <queue> #include <algorithm> #include <stack> using namespace std;vector<int > topologicalSortBFS (const vector<vector<int >>& graph, int n) { vector<int > inDegree (n, 0 ) ; vector<int > topoOrder; for (int u = 0 ; u < n; ++u) { for (int v : graph[u]) inDegree[v]++; } queue<int > q; for (int i = 0 ; i < n; ++i) { if (inDegree[i] == 0 ) q.push (i); } while (!q.empty ()) { int u = q.front (); q.pop (); topoOrder.push_back (u); for (int v : graph[u]) { if (--inDegree[v] == 0 ) q.push (v); } } if (topoOrder.size () != n) return {}; return topoOrder; } bool dfs (int u, const vector<vector<int >>& graph, vector<int >& state, vector<int >& result) { state[u] = 1 ; for (int v : graph[u]) { if (state[v] == 1 ) return false ; if (state[v] == 0 ) { if (!dfs (v, graph, state, result)) return false ; } } state[u] = 2 ; result.push_back (u); return true ; } vector<int > topologicalSortDFS (const vector<vector<int >>& graph, int n) { vector<int > state (n, 0 ) ; vector<int > result; for (int i = 0 ; i < n; ++i) { if (state[i] == 0 ) { if (!dfs (i, graph, state, result)) return {}; } } reverse (result.begin (), result.end ()); return result; } void printOrder (const string& method, const vector<int >& order) { if (order.empty ()) { cout << method << ": 图中存在环,无法进行拓扑排序!" << endl; } else { cout << method << " 序列:" ; for (int u : order) cout << u << " " ; cout << endl; } } int main () { int n = 5 ; vector<vector<int >> graph (n); graph[0 ] = {1 , 2 }; graph[1 ] = {3 }; graph[2 ] = {3 }; graph[3 ] = {4 }; cout << "--- DAG 测试 ---" << endl; printOrder ("BFS" , topologicalSortBFS (graph, n)); printOrder ("DFS" , topologicalSortDFS (graph, n)); cout << "\n--- 有环图测试 ---" << endl; int n2 = 3 ; vector<vector<int >> graph2 (n2); graph2[0 ] = {1 }; graph2[1 ] = {2 }; graph2[2 ] = {0 }; printOrder ("BFS" , topologicalSortBFS (graph2, n2)); printOrder ("DFS" , topologicalSortDFS (graph2, n2)); return 0 ; }
4.连通分量 (1)连通分量的定义 对于无向图 而言,连通分量是图的一个极大连通子图 。
连通性 :在该子图中,任意两个顶点之间都存在路径相连。
极大性 :如果再加入原图中的任何其他顶点,该子图将不再连通。
(2)求连通分量的两种主要算法 求连通分量的本质是遍历图。通过从每一个未访问的顶点出发进行一次完整的搜索,每次搜索所覆盖的所有节点就属于同一个连通分量。(1) 深度优先搜索 (DFS) 法
两种方法的时间复杂度 :