图算法的重点是建模
很多图算法题表面上不说图。
它可能说的是:
- 使用一次折扣券,找最便宜路线
- 选择一组课程,使先修关系满足
- 给学生分配项目
- 判断城市之间是否能互相到达
- 从起点到终点,路径边数必须是 3 的倍数
- 搬运供需,最小化运输成本
这些题的核心都不是“写 BFS”或“写 Dijkstra”。
核心是把原问题里的对象、状态、约束、目标,编码成图。
最重要的一句话:
节点 = 状态,边 = 状态转移,权重 = 代价。
建图五问
不管什么题,先问这五个问题:
1. 我最终需要知道什么?
↓
2. 这个“什么”依赖哪些中间状态?
↓
3. 每个状态如何从已知量转移过来?
↓
4. 图的节点/边应该编码什么信息?
↓
5. 起点终点是什么?跑什么算法?
如果题目只是标准最短路,节点就是原图节点,边就是道路,权重就是距离。
如果题目多了限制,节点通常就不能只表示位置。它还要表示“当前已经用了几次操作”“上一条边是什么颜色”“当前步数 mod k 是多少”。
这就是状态扩张。
先看图的基本结构
建模后,先按结构选算法:
| 图结构 / 权重条件 | 先想到 |
|---|---|
| 无权图 | BFS |
| 非负边权 | Dijkstra |
| 有负边权,无负环 | Bellman-Ford |
| DAG | Topological order + DP |
| 所有点对最短路 | Floyd-Warshall / Johnson |
| 无向图连通性 | DFS / BFS |
| 有向图互相可达 | SCC |
这里的判断来自算法前提。
Dijkstra 需要非负边权。Bellman-Ford 可以处理负边,但成本更高。DAG 上即使有负边,也可以按拓扑序做 DP,因为没有环。
算法不是按喜好选的,是按图结构和约束选的。
Layered Graph:给路径加状态
Layered graph 适合所有“路径 + 有限状态”的问题。
典型信号:
- 有一张 coupon
- 可以对最多两条边减半
- 边数必须满足 mod k
- 路径必须使用某几类颜色
- 某个特殊操作最多用几次
原图节点是 $v$,扩展后节点变成:
$$ (v, state) $$state 记录当前已经发生了什么。
一次折扣
题目:从 $s$ 到 $t$,可以让一条边半价,求最短路。
构造:
- $V' = V \times \{0,1\}$
- $(v,0)$ 表示到达 $v$ 且还没用折扣
- $(v,1)$ 表示到达 $v$ 且已经用过折扣
对每条原边 $(u,v)$,权重为 $w(u,v)$:
- $(u,0) \to (v,0)$,权重 $w(u,v)$
- $(u,1) \to (v,1)$,权重 $w(u,v)$
- $(u,0) \to (v,1)$,权重 $w(u,v)/2$
然后求:
$$ (s,0) \to (t,1) $$或者如果允许不用折扣,也可以取 $(t,0)$ 和 $(t,1)$ 的 min。
这里的关键在于状态必须记录折扣是否已经使用,“复制两层图”只是实现这个状态记录的方式。
边数 mod k
题目:从 $s$ 到 $t$,路径边数必须能被 $k$ 整除。
状态记录当前走了多少条边 mod $k$。
构造:
- $V' = V \times \{0,1,\ldots,k-1\}$
- 若原图有边 $(u,v)$,则新图中有:
权重保持不变。
目标:
$$ (s,0) \to (t,0) $$这个建模非常通用。凡是路径上有一个有限状态机一样的限制,都可以把 finite state 乘到节点上。
颜色约束
如果限制是“相邻边颜色不能相同”,状态可以记录上一条边颜色。
节点从 $v$ 变成:
$$ (v, lastColor) $$如果限制是“必须至少用过红边和蓝边”,状态可以记录已经用过的颜色集合:
$$ state \in \{0, R, B, RB\} $$红边负责把:
- $0 \to R$
- $B \to RB$
蓝边负责把:
- $0 \to B$
- $R \to RB$
普通边留在同一层。
这类题不要把颜色当成边的附属信息看完就算了。颜色会影响未来能怎么走,所以它必须进入状态。
反向图 + 两次最短路
题目:求从 $s$ 到 $t$ 的最短路径,但路径必须经过集合 $A$ 中某个点。
直接枚举每个 $a \in A$,求:
$$ dist(s,a) + dist(a,t) $$第一项可以从 $s$ 在原图跑一次最短路得到。
第二项如果对每个 $a$ 都跑一次,就太慢。
技巧是建反向图,从 $t$ 跑一次最短路。反向图上从 $t$ 到 $a$ 的距离,等于原图上从 $a$ 到 $t$ 的距离。
所以答案是:
$$ \min_{a \in A} dist_s[a] + dist_t^{rev}[a] $$这类技巧的核心是:很多“到终点的距离”可以通过反向图一次性预处理。
SCC 缩点
有向图里的 strongly connected component(SCC)是互相可达的一组点。
把每个 SCC 缩成一个点,得到的图一定是 DAG。
流程:
第1步:求 SCC
第2步:缩点得到 DAG H
第3步:在 DAG H 上做 DP / DFS / 拓扑排序
这个套路适合:
- 找所有在环上的点/边
- 缩点后做可达性
- 缩点后做最大收益
- 判断图结构性质
- 在 SCC 之间做路径统计
为什么缩点后是 DAG?
如果缩点图里还有一个有向环,那么这些 SCC 之间其实互相可达,应该属于同一个 SCC,矛盾。
SCC 的建模意义
SCC 缩点的价值是把“内部随便走”的复杂结构压成一个节点。
在一个 SCC 内部,任意两个点互相可达,所以对外部决策来说,它们常常可以看成一个整体。
比如想找最少选几个起点,使得所有点都可达。
缩点成 DAG 后,只需要在每个入度为 0 的 source SCC 中选一个点。
因为 source SCC 没有从外部进入的边,不选它就永远到不了。其他 SCC 都可以从某个 source SCC 沿 DAG 到达。
这就是“先压缩结构,再在 DAG 上解决”的典型思路。
DAG 上的 DP
DAG 没有环,所以天然有拓扑序。
很多 DAG 问题都可以按拓扑序做 DP:
| 目标 | 递推形式 |
|---|---|
| 可达性 | $P[v] = \bigvee_{u\to v} P[u]$ |
| 路径计数 | $cnt[v] = \sum_{u\to v} cnt[u]$ |
| 最短路 | $dist[v] = \min_{u\to v}(dist[u]+w(u,v))$ |
| 最长路 | $dist[v] = \max_{u\to v}(dist[u]+w(u,v))$ |
DAG shortest path 可以处理负边权,因为没有负环,也没有任何环。
如果题目限制是 $O(m+n)$,而图又是 DAG,通常就是拓扑序 DP。
修改 relaxation:最宽路径
不是所有路径问题都是求权重和。
最宽路径(widest path)要求最大化路径上的最小边权。
定义:
$$ H[v] = \text{从 } s \text{ 到 } v \text{ 的路径中,最小边权能达到的最大值} $$从 $u$ 走到 $v$,候选值是:
$$ \min(H[u], w(u,v)) $$因为一条路径的宽度由最窄的边决定。
更新:
$$ H[v] = \max(H[v], \min(H[u], w(u,v))) $$这和 Dijkstra 的结构很像,只是把“加法 + 取最小距离”换成了“取 min + 最大化瓶颈”。
高层上看,这是同一个 graph search 框架下换了路径代数。
Matching 和 Flow 的触发信号
如果问题是两类对象之间的一对一分配,先想到 bipartite matching。
典型结构:
- 学生 vs 项目
- 工人 vs 任务
- 左侧位置 vs 右侧位置
- 行 vs 列
如果每个对象有容量,或者一个人可以拿多个,matching 往往要升级成 flow。
判断时先问:
被配对的另一方,有没有自己的约束?
如果只是“物品不在乎谁拿”,可能是简单选择或 bipartite matching。
如果双方都有容量、互斥、费用、上下限,那更像 flow / min-cost flow。
图构造的心智模型
图建模的通用原则可以压成一句:
把“合法变化”变成边,把“当前必须记住的信息”变成节点。
如果一个信息会影响未来选择,它就应该进入状态。
如果一个信息只影响当前这一步的成本,它通常可以作为边权。
如果一个约束是“一次性资源”,比如 coupon,就用层数记录是否消耗。
如果一个约束是“路径全局属性”,比如颜色集合或 mod k,就把这个属性压成有限状态。
如果一个约束是“对象不能重复使用”,通常需要 matching、flow 或拆点。
图算法真正难的是这个编码过程。编码一旦正确,后面通常只是选择合适的黑盒算法。