前言
图论是计算机科学与数学的重要交叉领域,广泛应用于网络设计、路径规划、社交网络分析等场景。本文将从图的基础概念讲起,逐步深入核心算法,并提供完整的 C++ 实现代码,帮助读者从零构建图论知识体系。
一、图的基础概念
1.1 什么是图?
图 (Graph) 是由顶点 (Vertex) 和边 (Edge) 组成的一种数据结构,用于描述元素之间的多对多关系。数学上用 G=(V,E) 表示,其中:
- V 是顶点的有限集合
- E 是边的有限集合,每条边连接两个顶点
1.2 图的分类
根据边的特性,图可以分为:
- 无向图:边没有方向,(u,v) 与 (v,u) 表示同一条边
- 有向图:边有方向,<u,v> 表示从 u 到 v 的有向边
- 带权图:边具有权重(可以是距离、成本等)
- 无权图:边没有权重
1.3 基本术语
- 度 (Degree):顶点关联的边数,有向图中分为入度和出度
- 路径 (Path):从一个顶点到另一个顶点的边的序列
- 环 (Cycle):起点和终点相同的路径
- 连通性:两个顶点之间存在路径则称为连通
- 连通分量:图中最大的连通子图
二、图的存储方式
选择合适的存储方式对图论算法的效率至关重要,下面介绍三种常用的存储方式:
2.1 邻接矩阵
使用二维数组存储顶点间的连接关系,适用于稠密图。
cpp
const int MAXN = 1005;
int adj_matrix[MAXN][MAXN]; // 邻接矩阵
// 初始化
void init(int n) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
adj_matrix[i][j] = (i == j) ? 0 : INT_MAX; // 初始化距离
}
}
}
// 添加边
void addEdge(int u, int v, int w = 1) {
adj_matrix[u][v] = w;
// 无向图需要添加下面一行
// adj_matrix[v][u] = w;
}
优点:查询两点间是否有边的时间复杂度为 O (1)缺点:空间复杂度为 O (n²),不适合稀疏图
2.2 邻接表
使用向量数组存储每个顶点的邻接顶点,适用于稀疏图。
cpp
const int MAXN = 100005;
struct Edge {
int to; // 目标顶点
int weight;// 边的权重
Edge(int t, int w) : to(t), weight(w) {}
};
vector<Edge> adj_list[MAXN]; // 邻接表
// 添加边
void addEdge(int u, int v, int w = 1) {
adj_list[u].emplace_back(v, w);
// 无向图需要添加下面一行
// adj_list[v].emplace_back(u, w);
}
优点:空间复杂度为 O (n+m),适合稀疏图缺点:查询两点间是否有边的时间复杂度为 O (degree (u))
2.3 链式前向星
使用数组模拟链表存储边,适用于边数多的场景,效率高于邻接表。
cpp
const int MAXN = 100005;
const int MAXM = 200005;
struct Edge {
int to; // 目标顶点
int weight; // 边的权重
int next; // 下一条边的索引
} edges[MAXM];
int head[MAXN]; // 每个顶点的第一条边的索引
int cnt; // 边的数量计数器
// 初始化
void init() {
memset(head, -1, sizeof(head));
cnt = 0;
}
// 添加边
void addEdge(int u, int v, int w = 1) {
edges[cnt].to = v;
edges[cnt].weight = w;
edges[cnt].next = head[u];
head[u] = cnt++;
// 无向图需要添加下面一行
// addEdge(v, u, w);
}
优点:内存效率高,遍历边速度快缺点:实现相对复杂
三、图的遍历
图的遍历是访问图中所有顶点的过程,主要有两种方式:深度优先搜索 (DFS) 和广度优先搜索 (BFS)。
3.1 深度优先搜索 (DFS)
cpp
const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
bool visited[MAXN]; // 访问标记
// 递归实现
void dfs(int u) {
visited[u] = true;
cout << u << " "; // 访问顶点u
for (int v : adj[u]) {
if (!visited[v]) {
dfs(v); // 递归访问未访问的邻接顶点
}
}
}
// 非递归实现(栈)
void dfs_iterative(int start) {
stack<int> stk;
stk.push(start);
visited[start] = true;
while (!stk.empty()) {
int u = stk.top();
stk.pop();
cout << u << " "; // 访问顶点u
// 注意:为了保持与递归相同的访问顺序,这里逆序入栈
for (auto it = adj[u].rbegin(); it != adj[u].rend(); ++it) {
int v = *it;
if (!visited[v]) {
visited[v] = true;
stk.push(v);
}
}
}
}
3.2 广度优先搜索 (BFS)
cpp
const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
bool visited[MAXN]; // 访问标记
void bfs(int start) {
queue<int> q;
q.push(start);
visited[start] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
cout << u << " "; // 访问顶点u
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = true;
q.push(v);
}
}
}
}
3.3 连通分量查找
cpp
const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
bool visited[MAXN]; // 访问标记
// 使用DFS查找连通分量
void dfs(int u, vector<int>& component) {
visited[u] = true;
component.push_back(u);
for (int v : adj[u]) {
if (!visited[v]) {
dfs(v, component);
}
}
}
// 查找所有连通分量
vector<vector<int>> find_connected_components(int n) {
fill(visited, visited + n + 1, false);
vector<vector<int>> components;
for (int i = 1; i <= n; i++) {
if (!visited[i]) {
vector<int> component;
dfs(i, component);
components.push_back(component);
}
}
return components;
}
四、图论进阶算法
4.1 拓扑排序
拓扑排序用于有向无环图 (DAG),得到一个顶点线性序列,使得所有有向边均从序列前部指向后部。
const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
int in_degree[MAXN]; // 入度
int n, m;
// 拓扑排序
vector<int> topological_sort() {
vector<int> result;
queue<int> q;
// 初始化队列,将入度为0的顶点入队
for (int i = 1; i <= n; i++) {
if (in_degree[i] == 0) {
q.push(i);
}
}
while (!q.empty()) {
int u = q.front();
q.pop();
result.push_back(u);
// 减少邻接顶点的入度
for (int v : adj[u]) {
if (--in_degree[v] == 0) {
q.push(v);
}
}
}
// 如果结果包含所有顶点,则排序成功,否则图中存在环
if (result.size() != n) {
return {}; // 存在环,返回空
}
return result;
}
4.2 最短路径算法
4.2.1 Dijkstra 算法(单源最短路径,正权图)
cpp
const int MAXN = 100005;
const int INF = INT_MAX;
vector<pair<int, int>> adj[MAXN]; // 邻接表,pair<顶点, 权重>
int dist[MAXN]; // 距离数组
bool visited[MAXN]; // 访问标记
// Dijkstra算法,返回从start到所有顶点的最短距离
void dijkstra(int start, int n) {
// 初始化距离数组
for (int i = 1; i <= n; i++) {
dist[i] = INF;
visited[i] = false;
}
dist[start] = 0;
// 优先队列,pair<距离, 顶点>,按距离从小到大排序
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({0, start});
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
if (visited[u]) continue;
visited[u] = true;
// 松弛操作
for (auto& edge : adj[u]) {
int v = edge.first;
int w = edge.second;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
}
4.2.2 SPFA 算法(单源最短路径,可处理负权边)
cpp
const int MAXN = 100005;
const int INF = INT_MAX;
vector<pair<int, int>> adj[MAXN]; // 邻接表,pair<顶点, 权重>
int dist[MAXN]; // 距离数组
int in_queue[MAXN]; // 记录顶点入队次数
bool in_spfa_queue[MAXN]; // 是否在队列中
// SPFA算法,返回是否存在负环
bool spfa(int start, int n) {
// 初始化距离数组
for (int i = 1; i <= n; i++) {
dist[i] = INF;
in_queue[i] = 0;
in_spfa_queue[i] = false;
}
dist[start] = 0;
queue<int> q;
q.push(start);
in_spfa_queue[start] = true;
in_queue[start]++;
while (!q.empty()) {
int u = q.front();
q.pop();
in_spfa_queue[u] = false;
// 松弛操作
for (auto& edge : adj[u]) {
int v = edge.first;
int w = edge.second;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
if (!in_spfa_queue[v]) {
q.push(v);
in_spfa_queue[v] = true;
in_queue[v]++;
// 入队次数超过n,说明存在负环
if (in_queue[v] > n) {
return true;
}
}
}
}
}
return false; // 不存在负环
}
4.2.3 Floyd 算法(全源最短路径)
cpp
const int MAXN = 505;
const int INF = INT_MAX;
int dist[MAXN][MAXN]; // 距离矩阵
int n, m;
// Floyd算法,计算所有顶点对之间的最短路径
void floyd() {
// 初始化距离矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
dist[i][j] = (i == j) ? 0 : INF;
}
}
// 读入边
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
dist[u][v] = min(dist[u][v], w);
// 无向图需要添加下面一行
// dist[v][u] = min(dist[v][u], w);
}
// Floyd核心算法
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] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
}
4.3 最小生成树
4.3.1 Kruskal 算法
cpp
const int MAXN = 100005;
const int MAXM = 200005;
struct Edge {
int u, v, weight;
Edge(int u_, int v_, int w_) : u(u_), v(v_), weight(w_) {}
// 按权重排序
bool operator<(const Edge& other) const {
return weight < other.weight;
}
};
vector<Edge> edges;
int n, m;
// 并查集
struct DSU {
vector<int> parent;
vector<int> rank;
DSU(int n) {
parent.resize(n + 1);
rank.resize(n + 1, 1);
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
bool unite(int x, int y) {
int root_x = find(x);
int root_y = find(y);
if (root_x == root_y) return false; // 已在同一集合
// 按秩合并
if (rank[root_x] < rank[root_y]) {
parent[root_x] = root_y;
} else {
parent[root_y] = root_x;
if (rank[root_x] == rank[root_y]) {
rank[root_x]++;
}
}
return true;
}
};
// Kruskal算法,返回最小生成树的总权重,-1表示无法生成
int kruskal() {
sort(edges.begin(), edges.end()); // 按权重排序
DSU dsu(n);
int total_weight = 0;
int edges_used = 0;
for (auto& edge : edges) {
if (dsu.unite(edge.u, edge.v)) {
total_weight += edge.weight;
edges_used++;
if (edges_used == n - 1) break; // 已找到生成树
}
}
return (edges_used == n - 1) ? total_weight : -1;
}
4.3.2 Prim 算法
cpp
const int MAXN = 100005;
const int INF = INT_MAX;
vector<pair<int, int>> adj[MAXN]; // 邻接表,pair<顶点, 权重>
int n, m;
// Prim算法,返回最小生成树的总权重,-1表示无法生成
int prim() {
vector<bool> in_mst(n + 1, false); // 标记是否已加入MST
vector<int> dist(n + 1, INF); // 距离数组
// 优先队列,pair<距离, 顶点>,按距离从小到大排序
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
// 从节点1开始
dist[1] = 0;
pq.push({0, 1});
int total_weight = 0;
int nodes_used = 0;
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (in_mst[u]) continue;
in_mst[u] = true;
total_weight += d;
nodes_used++;
// 更新邻接顶点的距离
for (auto& [v, w] : adj[u]) {
if (!in_mst[v] && w < dist[v]) {
dist[v] = w;
pq.push({w, v});
}
}
}
return (nodes_used == n) ? total_weight : -1;
}
4.4 树上算法
4.4.1 最近公共祖先 (LCA) - 倍增算法
cpp
const int MAXN = 100005;
const int LOG = 20; // 2^20 > 1e6
vector<int> adj[MAXN]; // 邻接表
int up[MAXN][LOG]; // up[i][j]表示i的2^j级祖先
int depth[MAXN]; // 深度数组
int n;
// DFS预处理
void dfs_lca(int u, int parent) {
up[u][0] = parent;
for (int j = 1; j < LOG; j++) {
up[u][j] = up[up[u][j-1]][j-1];
}
for (int v : adj[u]) {
if (v != parent) {
depth[v] = depth[u] + 1;
dfs_lca(v, u);
}
}
}
// 初始化
void init_lca(int root) {
depth[root] = 0;
memset(up, 0, sizeof(up));
dfs_lca(root, root); // 根节点的父节点设为自身
}
// 计算u和v的LCA
int lca(int u, int v) {
if (depth[u] < depth[v]) swap(u, v);
// 将u提升到与v相同的深度
int diff = depth[u] - depth[v];
for (int j = 0; j < LOG; j++) {
if (diff & (1 << j)) {
u = up[u][j];
}
}
if (u == v) return u;
// 共同提升,直到找到LCA
for (int j = LOG - 1; j >= 0; j--) {
if (up[u][j] != up[v][j]) {
u = up[u][j];
v = up[v][j];
}
}
return up[u][0];
}
4.4.2 树的重心
cpp
const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
int size[MAXN]; // 子树大小
int n;
int min_balance; // 最小不平衡度
int center; // 重心
// 计算子树大小
void dfs_size(int u, int parent) {
size[u] = 1;
for (int v : adj[u]) {
if (v != parent) {
dfs_size(v, u);
size[u] += size[v];
}
}
}
// 查找重心
void dfs_center(int u, int parent) {
int max_subtree = n - size[u]; // 父节点方向的子树大小
for (int v : adj[u]) {
if (v != parent) {
dfs_center(v, u);
max_subtree = max(max_subtree, size[v]);
}
}
// 更新重心
if (max_subtree < min_balance) {
min_balance = max_subtree;
center = u;
}
}
// 查找树的重心
int find_center() {
dfs_size(1, -1); // 假设根为1
min_balance = n;
dfs_center(1, -1);
return center;
}
4.4.3 树上差分
cpp
const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
int diff[MAXN]; // 差分数组
int res[MAXN]; // 结果数组
int n;
// 计算前缀和
void dfs_diff(int u, int parent) {
res[u] = diff[u];
for (int v : adj[u]) {
if (v != parent) {
dfs_diff(v, u);
res[u] += res[v];
}
}
}
// 对路径u-v上的所有节点加val
void update_path(int u, int v, int val) {
int ancestor = lca(u, v); // 使用前面实现的LCA函数
diff[u] += val;
diff[v] += val;
diff[ancestor] -= val;
if (up[ancestor][0] != ancestor) { // 如果不是根节点
diff[up[ancestor][0]] -= val;
}
}
// 初始化并计算结果
void init_diff() {
memset(diff, 0, sizeof(diff));
memset(res, 0, sizeof(res));
}
// 获取最终结果
void compute_result() {
dfs_diff(1, -1); // 假设根为1
}
4.5 Tarjan 算法(割点、割边、强连通分量)
cpp
const int MAXN = 100005;
// 1. 割点和割边
vector<int> adj_cut[MAXN];
int dfn_cut[MAXN], low_cut[MAXN];
bool is_cut_point[MAXN];
vector<pair<int, int>> cut_edges;
int time_cut = 0;
void tarjan_cut(int u, int parent, bool is_root) {
dfn_cut[u] = low_cut[u] = ++time_cut;
int children = 0; // 子树数量(根节点用)
for (int v : adj_cut[u]) {
if (v == parent) continue;
if (!dfn_cut[v]) { // 未访问过
children++;
tarjan_cut(v, u, false);
low_cut[u] = min(low_cut[u], low_cut[v]);
// 判断割点
if (!is_root && low_cut[v] >= dfn_cut[u]) {
is_cut_point[u] = true;
}
// 判断割边
if (low_cut[v] > dfn_cut[u]) {
cut_edges.emplace_back(min(u, v), max(u, v)); // 避免重复
}
} else { // 回边
low_cut[u] = min(low_cut[u], dfn_cut[v]);
}
}
// 根节点割点判断
if (is_root && children >= 2) {
is_cut_point[u] = true;
}
}
// 2. 强连通分量
vector<int> adj_scc[MAXN];
int dfn_scc[MAXN], low_scc[MAXN];
bool in_stack[MAXN];
stack<int> stk;
vector<vector<int>> sccs;
int time_scc = 0;
void tarjan_scc(int u) {
dfn_scc[u] = low_scc[u] = ++time_scc;
stk.push(u);
in_stack[u] = true;
for (int v : adj_scc[u]) {
if (!dfn_scc[v]) { // 未访问过
tarjan_scc(v);
low_scc[u] = min(low_scc[u], low_scc[v]);
} else if (in_stack[v]) { // 回边且在栈中
low_scc[u] = min(low_scc[u], dfn_scc[v]);
}
}
// 找到一个SCC
if (dfn_scc[u] == low_scc[u]) {
vector<int> scc;
while (true) {
int v = stk.top();
stk.pop();
in_stack[v] = false;
scc.push_back(v);
if (v == u) break;
}
sccs.push_back(scc);
}
}
五、总结与学习建议
图论是算法领域的重要组成部分,本文涵盖了从基础概念到高级算法的主要内容,包括:
- 图的基本概念和存储方式
- 图的遍历算法(DFS、BFS)
- 拓扑排序
- 最短路径算法(Dijkstra、SPFA、Floyd)
- 最小生成树(Kruskal、Prim)
- 树上算法(LCA、树的重心、树上差分)
- Tarjan 算法(割点、割边、强连通分量)
学习建议:
- 打好基础:先理解图的基本概念和存储方式,这是后续学习的基础
- 动手实践:图论算法比较抽象,一定要动手实现,才能真正理解
- 由浅入深:从简单的遍历算法开始,逐步学习复杂算法
- 多做练习:通过实际问题巩固所学知识,推荐在 OJ 平台上练习相关题目
- 理解原理:不仅要记住代码,更要理解算法的核心思想和适用场景
图论算法在实际应用中非常广泛,掌握这些算法将为你的编程能力带来质的提升。祝你学习顺利!
参考资料
- 《算法导论》
- 《图论及其应用》
- 信息学奥赛相关教程和资料
7711

被折叠的 条评论
为什么被折叠?



