建议先阅读: T 图 Graph


原理

高级图算法是什么

BFS/DFS/Dijkstra 解决的是”无限制”或”非负权”的图问题。但现实中有更复杂的需求:编译器需要处理循环依赖(负环检测)、物流网络有容量限制(最大流)、社交网络需要发现互关圈(强连通分量)。高级图算法就是解决这些”放宽限制”后的图问题。

与基础图算法的关系

基础高级扩展放宽的限制
BFS拓扑排序有向无环图(DAG)
DFSTarjan SCC发现强连通分量
DijkstraBellman-Ford允许负权边
DijkstraFloyd-Warshall全源(所有点对)
BFSDinic 最大流边有容量限制

高级图算法在哪里

  • 拓扑排序:npm install 的依赖安装顺序、大学课程的先修关系、Makefile 的编译顺序
  • Tarjan SCC:社交网络中”互关圈”分析、编译器循环依赖检测、缩点后在 DAG 上做 DP
  • Bellman-Ford/SPFA:含负权边的最短路径(如汇率套利检测)、负环检测
  • Floyd-Warshall:小规模全源最短路径(V ≤ 500)、传递闭包(任意两点是否可达)
  • Dinic 最大流:二分图最大匹配、物流配送网络优化、图像分割

算法全景

算法问题时间复杂度核心洞见
KahnDAG 拓扑排序零入度节点的队列驱逐
Tarjan SCC强连通分量DFS 生成树 + low-link 值判定跨分量边
Bellman-Ford单源含负权最短路径至多 轮松弛——超过即有负环
Floyd-Warshall全源最短路径DP:
Dinic最大流BFS 分层图 + DFS 阻塞流

拓扑排序(Kahn 算法)

适用于有向无环图(DAG)。统计每个顶点的入度——入度为 0 的顶点没有未解决的前驱依赖,可立即输出。输出后将被其指向的顶点的入度减 1,新产生的入度 0 顶点入队。若最终输出的顶点数 ,则图中存在环。拓扑排序是编译器构建系统(Makefile、Gradle 任务)和任务依赖调度(PERT 网络)的基础。

Tarjan 强连通分量(SCC)

Tarjan 算法在一次 DFS 中同时完成 SCC 的发现和划分。核心是两个时间戳:

  • dfn[v](discovery/finish number):DFS 首次访问 的时间(时间戳递增)
  • low[v] 通过最多一条回边能到达的顶点中 dfn 的最小值

dfn[v] == low[v] 时, 是其 SCC 的根——构成该 SCC 的所有顶点都在 DFS 栈中、在 之上。

// Tarjan SCC 核心
void tarjan(int u, int* dfn, int* low, int* in_stack, int* stk, int* top, int* timer) {
 dfn[u] = low[u] = ++(*timer);
 stk[(*top)++] = u; in_stack[u] = 1;
 
 for (each neighbor v of u) {
 if (dfn[v] == 0) { // 树边
 tarjan(v, dfn, low, in_stack, stk, top, timer);
 low[u] = MIN(low[u], low[v]); // 子节点的 low 值回传
 } else if (in_stack[v]) { // 回边(当前栈中的后裔)
 low[u] = MIN(low[u], dfn[v]);
 }
 // 横跨边:忽略(dfn[v] 已定且不在栈中)
 }
 
 if (low[u] == dfn[u]) { // u 是 SCC 的根
 // 弹出栈直到 u,所有弹出的顶点构成一个 SCC
 while (stk[--(*top)] != u) { ... }
 }
}

Bellman-Ford 与负环检测

Bellman-Ford 的每一轮松弛(relaxation)都检查每条边 :如果当前 ,则更新 轮后,所有最短路径(至多 条边)均已找到。若第 轮仍能更新任何 ,则存在负权环——因为正确的最短路径不会超过 条边。

Bellman-Ford 是动态规划在最短路径上的直接体现——第 轮松弛等价于”至多使用 条边的最短路径”。

Floyd-Warshall 的 DP 递推

Floyd-Warshall 是经典的动态规划全源最短路径算法:

含义:加入顶点 作为中间节点后, 的最短路径要么不经过 (保持原值),要么经过 (路径分为 两段)。三重循环 但常数因子极小——3 层嵌套循环访问连续的二维数组,cache 利用率高。

拓扑排序手算轨迹

DAG(6 个顶点 0-5):

0 → 1 → 3 → 5
↓   ↓
2 → 4

边:(0→1), (0→2), (1→3), (1→4), (2→4), (3→5), (4→5)

Kahn 算法过程

入度为0的队列输出更新入度
初始[0]0:0, 1:1, 2:1, 3:1, 4:2, 5:2
1[0] → 空01:0, 2:0
2[1, 2]
3[1, 2] → [2]13:0, 4:1
4[2, 3] → [3]24:0
5[3, 4] → [4]35:1
6[4] → 空45:0
7[5] → 空5

拓扑序列:0 → 1 → 2 → 3 → 4 → 5(合法:所有边从左到右)。注意拓扑序列不唯一——0 → 2 → 1 → 4 → 3 → 5 也是合法的。

核心推演:拓扑排序

DAG(4 个顶点 0-3),边:(0→1), (0→2), (1→3), (2→3)。写出所有合法拓扑序列。

答案:

入度:0:0, 1:1, 2:1, 3:2。

第一步只能输出 0。0 输出后 1 和 2 入度变 0,可以任选。所以合法序列有:

  • 0 → 1 → 2 → 3
  • 0 → 2 → 1 → 3

2 种合法拓扑序列。

Floyd-Warshall 手算轨迹

4 个顶点(0-3)的有向加权图,邻接矩阵:

      0    1    2    3
0 [  0    3    ∞    7  ]
1 [  8    0    2    ∞  ]
2 [  5    ∞    0    1  ]
3 [  2    ∞    ∞    0  ]

初始化 = 邻接矩阵(∞ 表示不可达)

k=0(以 0 为中间节点):

(i,j)d(i,j)d(i,0)+d(0,j)更新
(1,2)28+3=11不更新
(1,3)8+7=1515
(2,1)5+3=88
(2,3)15+7=12不更新

k=1(以 1 为中间节点):

(i,j)d(i,j)d(i,1)+d(1,j)更新
(0,2)3+2=55
(0,3)73+15=18不更新
(2,3)18+2=10不更新
(3,2)∞+2=∞不更新

k=2(以 2 为中间节点):

(i,j)d(i,j)d(i,2)+d(2,j)更新
(0,3)75+1=66
(1,3)152+1=33
(3,1)∞+8=∞不更新

k=3(以 3 为中间节点):无新更新(3 的出边只有到自身)。

最终

      0    1    2    3
0 [  0    3    5    6  ]
1 [  8    0    2    3  ]
2 [  5    8    0    1  ]
3 [  2    5    8    0  ]

验证:,路径 1→2→3(权重 2+1=3)[正确]

核心推演:Floyd-Warshall

3 个顶点,邻接矩阵:[[0,1,4],[∞,0,2],[∞,∞,0]]。执行 Floyd 后 d(0,2) = ?

答案:

初始化:d(0,2)=4, d(0,1)=1, d(1,2)=2

k=0:d(1,2)=min(2, d(1,0)+d(0,2))=min(2,∞+4)=2(不变)

k=1:d(0,2)=min(4, d(0,1)+d(1,2))=min(4, 1+2)=3

路径 0→1→2,权重 1+2=3。


实现

拓扑排序(Kahn 算法)

#include <stdlib.h>
 
// 邻接表图结构(沿用 H_图的定义)
// 返回结果需要调用者 free
int* topological_sort(int V, int** adj, int* adj_sizes, int* result_size) {
 int* in_degree = calloc(V, sizeof(int));
 for (int u = 0; u < V; u++)
 for (int j = 0; j < adj_sizes[u]; j++)
 in_degree[adj[u][j]]++;
 
 int* queue = malloc(V * sizeof(int));
 int head = 0, tail = 0;
 for (int i = 0; i < V; i++)
 if (in_degree[i] == 0) queue[tail++] = i;
 
 int* result = malloc(V * sizeof(int));
 int ri = 0;
 while (head < tail) {
 int u = queue[head++];
 result[ri++] = u;
 for (int j = 0; j < adj_sizes[u]; j++) {
 int v = adj[u][j];
 if (--in_degree[v] == 0)
 queue[tail++] = v;
 }
 }
 free(in_degree);
 free(queue);
 
 if (ri < V) { // 存在环
 free(result);
 *result_size = 0;
 return NULL;
 }
 *result_size = ri;
 return result;
}

Tarjan SCC

#include <stdlib.h>
#include <string.h>
 
typedef struct {
 int* dfn, *low, *scc_id;
 int* stack;
 int* on_stack;
 int timer, scc_count, stack_top;
 int V;
 int** adj;
 int* adj_sizes;
} TarjanSCC;
 
void tarjan_init(TarjanSCC* ts, int V) {
 ts->V = V;
 ts->dfn = calloc(V, sizeof(int));
 ts->low = calloc(V, sizeof(int));
 ts->scc_id = calloc(V, sizeof(int));
 ts->stack = malloc(V * sizeof(int));
 ts->on_stack = calloc(V, sizeof(int));
 ts->timer = 0;
 ts->scc_count = 0;
 ts->stack_top = 0;
}
 
void tarjan_destroy(TarjanSCC* ts) {
 free(ts->dfn); free(ts->low); free(ts->scc_id);
 free(ts->stack); free(ts->on_stack);
}
 
static void tarjan_dfs(TarjanSCC* ts, int u) {
 ts->dfn[u] = ts->low[u] = ++ts->timer;
 ts->stack[ts->stack_top++] = u;
 ts->on_stack[u] = 1;
 
 for (int j = 0; j < ts->adj_sizes[u]; j++) {
 int v = ts->adj[u][j];
 if (!ts->dfn[v]) {
 tarjan_dfs(ts, v);
 if (ts->low[v] < ts->low[u]) ts->low[u] = ts->low[v];
 } else if (ts->on_stack[v]) {
 if (ts->dfn[v] < ts->low[u]) ts->low[u] = ts->dfn[v];
 }
 }
 
 if (ts->dfn[u] == ts->low[u]) {
 ts->scc_count++;
 while (1) {
 int v = ts->stack[--ts->stack_top];
 ts->on_stack[v] = 0;
 ts->scc_id[v] = ts->scc_count;
 if (v == u) break;
 }
 }
}
 
int tarjan_solve(TarjanSCC* ts) {
 for (int i = 0; i < ts->V; i++)
 if (!ts->dfn[i]) tarjan_dfs(ts, i);
 return ts->scc_count;
}

Floyd-Warshall

#include <limits.h>
#include <stdlib.h>
 
typedef struct {
 long long** dist;
 int V;
} FloydWarshall;
 
void floyd_init(FloydWarshall* fw, int V) {
 fw->V = V;
 fw->dist = malloc(V * sizeof(long long*));
 for (int i = 0; i < V; i++) {
 fw->dist[i] = malloc(V * sizeof(long long));
 for (int j = 0; j < V; j++)
 fw->dist[i][j] = (i == j) ? 0 : LLONG_MAX / 2;
 }
}
 
void floyd_destroy(FloydWarshall* fw) {
 for (int i = 0; i < fw->V; i++) free(fw->dist[i]);
 free(fw->dist);
}
 
void floyd_add_edge(FloydWarshall* fw, int u, int v, int w) {
 fw->dist[u][v] = w;
}
 
// 返回 1 成功,0 表示存在负环
int floyd_solve(FloydWarshall* fw) {
 int V = fw->V;
 for (int k = 0; k < V; k++)
 for (int i = 0; i < V; i++)
 for (int j = 0; j < V; j++)
 if (fw->dist[i][k] < LLONG_MAX / 2 &&
 fw->dist[k][j] < LLONG_MAX / 2 &&
 fw->dist[i][k] + fw->dist[k][j] < fw->dist[i][j])
 fw->dist[i][j] = fw->dist[i][k] + fw->dist[k][j];
 
 for (int i = 0; i < V; i++)
 if (fw->dist[i][i] < 0) return 0;
 return 1;
}
 
long long floyd_get_dist(FloydWarshall* fw, int u, int v) {
 return fw->dist[u][v];
}

Bellman-Ford

#include <limits.h>
#include <stdlib.h>
 
typedef struct { int from, to, weight; } Edge;
 
// 返回结果需要调用者 free,has_neg_cycle 为 1 表示存在负环
long long* bellman_ford(int V, const Edge* edges, int E, int start, int* has_neg_cycle) {
 long long* dist = malloc(V * sizeof(long long));
 for (int i = 0; i < V; i++) dist[i] = LLONG_MAX / 2;
 dist[start] = 0;
 
 for (int i = 0; i < V - 1; i++) {
 int updated = 0;
 for (int j = 0; j < E; j++) {
 if (dist[edges[j].from] < LLONG_MAX / 2 &&
 dist[edges[j].from] + edges[j].weight < dist[edges[j].to]) {
 dist[edges[j].to] = dist[edges[j].from] + edges[j].weight;
 updated = 1;
 }
 }
 if (!updated) break;
 }
 
 *has_neg_cycle = 0;
 for (int j = 0; j < E; j++) {
 if (dist[edges[j].from] < LLONG_MAX / 2 &&
 dist[edges[j].from] + edges[j].weight < dist[edges[j].to]) {
 *has_neg_cycle = 1;
 break;
 }
 }
 return dist;
}

Dinic 最大流

#include <limits.h>
#include <stdlib.h>
#include <string.h>
 
typedef struct { int to, rev; long long cap; } FlowEdge;
 
typedef struct {
 FlowEdge** graph;
 int* graph_sizes;
 int* graph_caps;
 int* level;
 int* iter;
 int V;
} Dinic;
 
void dinic_init(Dinic* dn, int V) {
 dn->V = V;
 dn->graph = calloc(V, sizeof(FlowEdge*));
 dn->graph_sizes = calloc(V, sizeof(int));
 dn->graph_caps = calloc(V, sizeof(int));
 dn->level = malloc(V * sizeof(int));
 dn->iter = malloc(V * sizeof(int));
}
 
void dinic_destroy(Dinic* dn) {
 for (int i = 0; i < dn->V; i++) free(dn->graph[i]);
 free(dn->graph); free(dn->graph_sizes); free(dn->graph_caps);
 free(dn->level); free(dn->iter);
}
 
static void dinic_add_edge_inner(Dinic* dn, int from, int to, long long cap) {
 if (dn->graph_sizes[from] >= dn->graph_caps[from]) {
 dn->graph_caps[from] = dn->graph_caps[from] ? dn->graph_caps[from] * 2 : 4;
 dn->graph[from] = realloc(dn->graph[from],
 dn->graph_caps[from] * sizeof(FlowEdge));
 }
 dn->graph[from][dn->graph_sizes[from]++] = (FlowEdge){to, 0, cap};
}
 
void dinic_add_edge(Dinic* dn, int from, int to, long long cap) {
 dinic_add_edge_inner(dn, from, to, cap);
 dinic_add_edge_inner(dn, to, from, 0);
 int from_idx = dn->graph_sizes[from] - 1;
 int to_idx = dn->graph_sizes[to] - 1;
 dn->graph[from][from_idx].rev = to_idx;
 dn->graph[to][to_idx].rev = from_idx;
}
 
static int dinic_bfs(Dinic* dn, int s, int t) {
 for (int i = 0; i < dn->V; i++) dn->level[i] = -1;
 int* q = malloc(dn->V * sizeof(int));
 int head = 0, tail = 0;
 dn->level[s] = 0; q[tail++] = s;
 while (head < tail) {
 int u = q[head++];
 for (int i = 0; i < dn->graph_sizes[u]; i++) {
 FlowEdge* e = &dn->graph[u][i];
 if (e->cap > 0 && dn->level[e->to] < 0) {
 dn->level[e->to] = dn->level[u] + 1;
 q[tail++] = e->to;
 }
 }
 }
 free(q);
 return dn->level[t] >= 0;
}
 
static long long dinic_dfs(Dinic* dn, int u, int t, long long f) {
 if (u == t) return f;
 for (int* i = &dn->iter[u]; *i < dn->graph_sizes[u]; (*i)++) {
 FlowEdge* e = &dn->graph[u][*i];
 if (e->cap > 0 && dn->level[e->to] == dn->level[u] + 1) {
 long long d = dinic_dfs(dn, e->to, t, f < e->cap ? f : e->cap);
 if (d > 0) {
 e->cap -= d;
 dn->graph[e->to][e->rev].cap += d;
 return d;
 }
 }
 }
 return 0;
}
 
long long dinic_max_flow(Dinic* dn, int s, int t) {
 long long flow = 0;
 while (dinic_bfs(dn, s, t)) {
 for (int i = 0; i < dn->V; i++) dn->iter[i] = 0;
 long long f;
 while ((f = dinic_dfs(dn, s, t, LLONG_MAX)) > 0)
 flow += f;
 }
 return flow;
}

应用场景

  • 拓扑排序: 课程安排、编译依赖、任务调度
  • Tarjan SCC: 社交网络互关圈分析、缩点后 DAG 上 DP
  • Floyd-Warshall: 小规模全源最短路径(V <= 500)
  • Bellman-Ford/SPFA: 含负权边的最短路径、负环检测
  • Dinic: 二分图最大匹配、物流配送网络优化

练习

题号题目说明
207课程表拓扑排序
210课程表 II拓扑排序输出序列
787K 站中转最便宜航班Bellman-Ford / DP
1192查找集群内的关键连接Tarjan SCC
332重新安排行程欧拉回路 + DFS
743网络延迟时间Dijkstra(复习)

力扣 (LeetCode) 有对应题型,竞赛方向推荐力扣/Codeforces。

动手实验

编号题目说明
E1有向无环图拓扑排序生成一个 20 个节点的随机 DAG,分别用 Kahn 算法和 DFS 后序遍历输出拓扑序列,验证结果正确性(序列中所有边从左指向右)
E2Bellman-Ford vs SPFA随机生成含负权边的稀疏图,分别用 Bellman-Ford 和 SPFA 求最短路径,对比迭代次数和运行时间。构造一个 Worst Case 让 SPFA 退化