Algorithm · Graph

图算法

关系的数学:BFS/DFS · 拓扑排序 · 二分图 · Dijkstra 与 MST —— 依赖调度、社交网络、地图路由的底层引擎

遍历

BFS 管层次/无权最短,DFS 管结构/连通——两大遍历撑起一半图题

有序与二分

拓扑排序管依赖调度;染色法管二分图判定

加权图

Dijkstra(非负最短路)· Bellman-Ford(负权/负环)· Kruskal/Prim(MST)

定位:图是"关系"的抽象——课程依赖、社交网络、地图、任务调度全是图。三块主线:遍历(BFS/DFS)、有序结构(拓扑/二分图)、加权问题(最短路/MST)。

Why Graph Algorithms

先看一类人脑会崩、机器秒解的问题

现实问题抽象成图换来的能力
100 个模块,先编译哪个节点 = 模块,边 = 依赖关系拓扑排序 O(V+E) 一次排好,还顺带告诉你有没有循环依赖
地图上 A 到 B 最快怎么走节点 = 路口,边 = 路段(带通行时间)Dijkstra 一次算出 A 到所有点的最短路
一张图里有多少块连通的陆地格子 = 节点,相邻 = 边一次 DFS 数出连通块个数
先看清困难到底在哪:这三个问题的共同点不是"算得慢",而是人脑装不下那么多两两关系。依赖关系一多(上百个节点、上千条边),你没法同时追踪"谁在谁前面";而循环依赖(A→B→C→A)更是肉眼几乎看不出来。
图算法换掉的就是这件事:它把"一堆杂乱的两两关系"统一建模成节点 + 边,然后交给一套固定流程(沿边一步一步走)机械地跑完。代价是要先做一步翻译——把你的问题说成"什么是点、什么是边"。建模错了,后面算法再熟也白搭,这也是图题真正的难点所在。

一个能在脑子里跑的最小例子

三个任务 A、B、C:A 要在 B 之后做、B 要在 C 之后做 → 顺序自然是 C → B → A。现在再加一条"C 要在 A 之后做"——没有任何顺序可行,你撞上了循环依赖。这两件事(排出顺序 + 发现排不出来)就是拓扑排序的全部

为什么手写一定会崩

三个任务你自己排得出来,一百个任务呢?人脑一次最多追踪几条依赖链,而图算法处理一千个节点也是 O(V+E) 毫秒级。这不是"快一点",是从"做不到"变成"做得到"

它还在我们身边

社交网络的"几度好友"、路由器之间的选路、推荐系统里的物品关系、编译器的依赖分析、数据库的外键拓扑——背后全都是同一套遍历与最短路。学会图的建模,比背十个模板更有价值。

本 deck 的路线

先讲"边怎么存"(下一页)→ 两大遍历 BFS/DFS → 网格图 → 拓扑排序与二分图 → 加权图三把钥匙(Dijkstra / Bellman-Ford / Floyd)→ 最小生成树。

动机页:用"100 个模块先编译哪个"这个具体困境开场,把三个真实场景并列成"抽象成图 → 换来什么能力",点破图题的难点在"关系建模"而非计算。最小例子用三个任务的依赖链,加一条边就变成循环依赖——这个例子后面拓扑排序页直接复用,形成呼应。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(先记住这个,细节后面展开)
顶点 / 节点 vertex被研究的那个东西:一个模块、一个路口、一个用户
边 edge两个节点之间的关系无向边双向通行(好友关系),有向边单向(依赖、关注)
权 weight边上的数字:距离、耗时、代价。有权图和无权图解法完全不同
度 / 入度 / 出度无向图=邻居数;有向图分入度(指向我的边数)与出度入度是拓扑排序的核心信号
邻接表 / 邻接矩阵存边的两种方式:省空间适合稀疏图,矩阵查边 O(1) 适合稠密图
路径 / 最短路径沿边走出来的一串节点;最短按"边的条数"(无权)或"权的总和"(带权)度量
环 cycle能从某点出发沿边走回自己。有环就没法拓扑排序(依赖成死结)
DAG有向无环图(Directed Acyclic Graph)。拓扑排序与 DP on DAG 的地盘
连通分量互相可达的节点聚成的一团;不连通的图要分几团分别处理,否则会漏算
松弛 relaxation最短路里的核心动作:如果发现更近的走法,就把距离改小——所有最短路算法都在反复做这件事

如果这些词还有一个是陌生的

先读这几篇再回来。本 deck 默认你已经能读懂数组、递归和队列:
栈与队列 → BFS 的队列、DFS 的栈
堆与优先队列 → Dijkstra 与 Prim 的标配外设
并查集 → Kruskal 与动态连通的引擎
复杂度分析 → O(V+E) 这些账怎么算

本 deck 的最小心智模型

把图算法记成一个固定动作:先把"东西"写成节点、"东西之间的关系"写成边,然后从某个起点出发,沿着边一步一步走,走到哪就处理到哪。后面所有算法的差别只在三件事:① 用什么容器决定"下一步走哪"(队列 / 栈 / 堆);② 走的时候记什么(距离 / 颜色 / 时间戳);③ 什么时候停

为什么"松弛"值得单独记

它是三把最短路钥匙(Dijkstra / Bellman-Ford / Floyd)唯一的共同动作:发现更近的走法就把距离改小。三个算法的差别只是"以什么顺序反复做这个动作"。把这层看穿,最短路就不再需要背三份代码。

阅读提示:术语不用背,忘了回来查这一页;真正要能默写的只有 BFS/DFS 两段骨架,以及速查页里那张"算法选型表 + 五条检查清单"。
前置页:十个术语先定义再使用。心智模型把整个 deck 的算法统一成"沿边走"这一个动作 + 三个可变点(容器/记录/停止),并点破三把最短路钥匙的唯一共同动作是"松弛"——把"背三份代码"降维成"记住一个动作 + 三种顺序"。

Adjacency List vs Matrix

图的表示:先选对数据结构

上一页说"把东西写成节点、关系写成边"——这一页讲这些边具体怎么存。存法选错,后面每个算法的常数都会吃亏。

// 邻接表(Go 习惯:map 或切片数组)
graph := make([][]int, n)
graph[u] = append(graph[u], v)

// 带权图:存 (to, w) 对
type Edge struct{ to, w int }
g := make([][]Edge, n)
g[u] = append(g[u], Edge{v, w})

// 邻接矩阵:稠密图 / O(1) 查边
mat := make([][]int, n) // n×n
mat[u][v] = w
建图三步:① 数节点数 n(小心 1-indexed 输入);② 加边(无向图加两条);③ 选遍历起点。LeetCode 图题 80% 的 bug 在建图,不在算法。

邻接表 vs 邻接矩阵

:空间 O(V+E),遍历邻居 O(deg),查边 O(deg)——稀疏图默认选择(真实系统 E≪V²)。矩阵:空间 O(V²),查边/加边 O(1)——稠密图、Floyd、快速判边时用。

其他表示

边列表(Kruskal 的输入形态)、CSR 压缩(工业图引擎)、隐式图(棋盘/状态空间不显式建图,直接在坐标上走)。

度、入度、出度

无向图度 = 邻居数;有向图分入度/出度——入度是拓扑排序的核心信号(下一页)。度数序列、握手定理 Σdeg=2E 是基础自查工具。

图的分类速查

有向/无向 · 带权/无权 · 稀疏/稠密 · DAG(拓扑的前提)· 连通/不连通——先分类再套算法,每类算法都有前提(Dijkstra 要非负权、拓扑要 DAG)。

表示法的选择决定后面所有算法的常数:稀疏图用表、稠密用矩阵。建图三步是 LeetCode 图题的实操建议。"先分类再套算法"是本 deck 的总纲。

Two Traversals

BFS 与 DFS:一次遍历两个世界

// BFS:队列 + 层次(无权最短路)
func bfs(g [][]int, s int) []int {
    dist := make([]int, len(g))
    for i := range dist { dist[i] = -1 }
    dist[s] = 0
    q := []int{s}
    for len(q) > 0 {
        u := q[0]; q = q[1:]
        for _, v := range g[u] {
            if dist[v] == -1 {
                dist[v] = dist[u] + 1
                q = append(q, v)
            }
        }
    }
    return dist
}
// DFS:递归栈 + 结构信息
func dfs(g [][]int, u int, vis []bool) {
    vis[u] = true
    for _, v := range g[u] {
        if !vis[v] { dfs(g, v, vis) }
    }
}

BFS:层次即最短

FIFO 保证先访问的层次浅——无权图最短路径的正确性来源。入队时标记 visited(不是出队时,否则重复入队爆炸)。应用:最短步数、层序、多源 BFS(994 腐烂橘子——把所有源点一起入队)。

DFS:结构信息的采集器

递归栈天然给出"当前路径";进入/离开时间戳可判环(有向图)、求拓扑序、找桥/割点(Tarjan)。应用:连通分量、环检测、拓扑。

复杂度相同,性格不同

都是 O(V+E);BFS 费内存(队列最宽 O(V))、DFS 费栈(最深 O(V),Go 栈可增长)。求最短用 BFS,找结构用 DFS——一句话选型。

连通分量

对每个未访问点启动一次遍历 → 启动次数 = 分量数;或用并查集(动态加边场景完胜)。

两段模板是全 deck 的引擎。三个细节点:BFS 入队时标记、多源 BFS、DFS 时间戳的用途。选型一句话"最短 BFS、结构 DFS"贯穿后面所有页面。

Grid Graph · LC 200/695/130

网格图:岛屿家族的统一视角

把网格看成图

每个'1'格子是节点,上下左右相邻是边——隐式图,不需要建邻接表,直接在矩阵上走。遍历框架不变,"邻居"换成四个方向向量。

200 岛屿数量

扫每个格子,遇 '1' 启动一次 DFS/BFS 并沉岛('1'→'0' 原地标记,免 visited 数组)——启动次数即岛数。O(MN) 时间。

695 最大岛屿面积 / 463 岛屿周长

695:DFS 返回子树大小求 max;463:不遍历,总周长 = 4·块数 − 2·共享边,纯计数。

130 被环绕的区域

反向思维:只从边界上的 O 启动 DFS 标记"不被围的",剩下全改 X——"从边界反推"是网格题的常用巧劲。

动态版

305 岛屿数量 II(逐个加陆地):静态遍历失效,必须并查集增量维护(并查集 deck)。

问法解法
200 岛屿数量 M连通块个数遍历 + 沉岛
695 最大面积 M最大连通块DFS 返回子树大小
463 岛屿周长 E边界总长纯计数:4·块数 − 2·共享边
130 被围区域 M保留非边界连通 O边界反向 DFS 标记
305 岛屿 II(会员)动态加陆地并查集
面试金句:"岛屿家族的本质是隐式图上的连通分量——遍历算法一个字不用改,改的只是'邻居'的定义。静态用 DFS,动态用并查集。"
网格图是面试最常考的"图"。沉岛技巧、130 的反向思维、305 的动态版对比——三个层次都覆盖。"隐式图"概念把网格题和图论统一。

Topological Sort · LC 207/210

拓扑排序:DAG 上的依赖调度

// Kahn 算法:入度表 + 队列
func canFinish(n int, pre [][]int) bool {
    g := make([][]int, n)
    indeg := make([]int, n)
    for _, p := range pre {
        g[p[1]] = append(g[p[1]], p[0])
        indeg[p[0]]++
    }
    q := []int{}
    for i := 0; i < n; i++ {
        if indeg[i] == 0 { q = append(q, i) }
    }
    done := 0
    for len(q) > 0 {
        u := q[0]; q = q[1:]
        done++
        for _, v := range g[u] {
            if indeg[v]--; indeg[v] == 0 {
                q = append(q, v)
            }
        }
    }
    return done == n // 有环则 < n
}

定义与前提

DAG 的线性序:所有边 u→v 都有 u 在 v 前。前提是 DAG——有向有环图没有拓扑序,"能否拓扑排序"="是否有环"。

两种实现

Kahn(本页):入度 0 的进队、出队时削邻居入度——直观、能顺便判环(done < n);② DFS 逆后序:节点完成时入栈,栈序即拓扑序(回溯/DFS 的副产品)。

207 vs 210

207 只问"能否修完"(判环,返回 done==n);210 要求"输出一个顺序"(记录出队序列)。同一算法,两个返回。

应用清单

构建系统依赖调度、任务编排(Airflow/DAG 工作流)、包管理器安装顺序、编译单元排序——"先修关系"在工程里的名字叫 DAG 调度

Kahn 的代码三段:建图+入度 → 队列初始化 → 削入度。"done==n 判环"是 207 的一句话答案。两种实现都要求会写;工程应用(工作流调度)把算法拉回现实。

Bipartite · LC 785/886

二分图判定:染色法的两步逻辑

定义与判定定理

节点可分成两组、所有边横跨两组(组内无边)。定理:图是二分图 ⇔ 无奇数环。判定 = 染色法:相邻节点异色, DFS/BFS 染色遇冲突即非二分。

染色法流程(LC 785)

color 数组 0 未染 / 1 / −1;对每个未染分量:起点染 1,DFS 中邻居染相反色;邻居已染色且与当前相同 → 冲突 → false。O(V+E)。

应用场景

可能的二分法(785/886 含 dislikes 约束:把"互相讨厌"建成边,染开两营);② 任务分配/匹配问题的建模前置(匈牙利算法求最大匹配——面试点到为止);③ 判定奇环存在性。

为什么"无奇环"等价

二分图沿边走必然两组交替 → 走回起点经过偶数步 → 所有环偶长。反之含奇环染色必冲突。证明一句话能说出来即可。

工程映射

调度中的"冲突互斥分组"、广告主与流量的二部匹配、编译器寄存器分配的干涉图(读多写少的入门案例)。

要点内容
判定染色法 DFS/BFS,O(V+E)
等价条件无奇数环
不连通图每个分量独立染色
进阶匈牙利算法:最大匹配 O(VE)(了解)
变形题886 可能的二分法(约束建边)
面试金句:"二分图判定是'染色冲突'问题——本质还是遍历 + 一个额外状态位。把'互斥关系'建成图,很多分组问题瞬间变成染色。"
二分图是"遍历+状态位"的又一个应用:颜色就是额外状态。无奇环等价性的一句话证明、886 的建边建模、工程映射三层都点到。

Non-negative Shortest Path · LC 743

Dijkstra:非负权最短路的贪心

// 网络延迟时间(LC 743):单源最短路
func networkDelayTime(times [][]int,
        n, k int) int {
    g := make([][]Edge, n+1)
    for _, t := range times {
        g[t[0]] = append(g[t[0]],
            Edge{t[1], t[2]})
    }
    dist := make([]int, n+1)
    for i := range dist { dist[i] = 1<<62 }
    dist[k] = 0
    h := &MinHeap{} // (dist, node)
    heap.Push(h, Item{k, 0})
    for h.Len() > 0 {
        it := heap.Pop(h).(Item)
        u, d := it.node, it.dist
        if d > dist[u] { continue } // 惰性删除
        for _, e := range g[u] {
            if nd := d + e.w; nd < dist[e.to] {
                dist[e.to] = nd
                heap.Push(h, Item{e.to, nd})
            }
        }
    }
    // dist 全可达 → 答案 = max(dist)
}

贪心逻辑与正确性前提

每轮取出当前 dist 最小且未定的节点,它已不可能被绕路改善——这个"锁定"只在边权非负时成立。负权边会让"绕路更短",贪心失效(→ Bellman-Ford)。

两个工程细节

惰性删除:同一节点可能多次入堆,弹出时 d > dist[u] 直接跳过——比维护索引堆简单,代价是堆稍大;② 堆版复杂度 O((V+E) log V),稀疏图远优于朴素 O(V²) 扫描。

变体与延展

多源:所有源点入堆(多源 BFS 的加权版);终点提前停(target 弹出即返回);A*:dist + 启发函数 h(有 h≤真实距离保证时仍最优)。

为什么配堆

"反复取当前最小"正是优先队列的语义——堆是图算法的标配外设(贪心 deck:Dijkstra 是可证明的贪心)。

代码三要素:建图、堆 + 惰性删除、松弛。正确性前提"非负权"要着重讲——它是 Dijkstra 与 Bellman-Ford 的分界线。LC 743 是标准模板题。

Bellman-Ford · Floyd

负权边与多源:另外两把最短路钥匙

Bellman-Ford:容忍负权

对所有边松弛 V−1 轮:第 k 轮结束后"最多 k 条边"的最短路已正确——数学归纳保证。O(V·E),慢但稳。

负环检测

第 V 轮还能松弛 → 存在负环(绕一圈更短,最短路无定义)。检测负环是 BF 独有的能力,Dijkstra 做不了。

SPFA:队列优化的 BF

只松弛"上轮被更新的点"(队列去重)——平均快、最坏仍 O(VE)。竞赛常用,工程负权场景也可用。

Floyd:多源 O(V³)

dp[k][i][j] = 经由前 k 个点中转的 i→j 最短路:d[i][j] = min(d[i][j], d[i][k]+d[k][j])本质是"允许中转点集合"的 DP——三维压二维。V≤500 以内的稠密图多源查询直接用它。

三把钥匙的选型

单源非负 → Dijkstra;有负权/要检测负环 → Bellman-Ford/SPFA;多源或图很小 → Floyd。先把"单源还是多源、有没有负权"两问答完

算法复杂度负权场景
BFSO(V+E)仅无权无权最短/步数
Dijkstra + 堆O((V+E)logV)非负权单源默认
Bellman-FordO(V·E)✓ + 负环检测负权/限制边数(BF 限定 k 轮)
SPFA均摊快竞赛常用
FloydO(V³)✓(无负环)多源/小图/传递闭包
面试金句:"最短路选型只要两问:单源还是多源?有没有负权?——两问一出,算法唯一。BF 的'限定 k 轮'变体还能解'最多中转 k 站'这类题(LC 787)。"
BF 的归纳本质(第 k 轮=最多 k 条边)、负环检测、Floyd 的 DP 本质是三个知识点。"两问选型"金句收束本页。LC 787(K 站中转)是 BF 变体的代表题。

MST · Kruskal / Prim

最小生成树:把所有点连起来的最小代价

定义与贪心本质

n 个点选 n−1 条边连通且总权最小。贪心可证(切割性质:横跨任意切割的最小边必在某个 MST 里)——两大实现是同一性质的两种用法。

Kruskal:边视角 + 并查集

边按权排序,逐条试加:两端不同根(并查集)就选入。O(E log E)——排序主导。实现最简单,稀疏图首选。

Prim:点视角 + 堆

从任意点开始,每次取"树外到树内最短的边"扩张——堆维护横切边 O(E log V)。稠密图可用朴素 O(V²) 版。

怎么选

稀疏图 → Kruskal(排序+并查集,代码短);稠密图 → Prim(堆版或朴素版)。两者都要求图连通——不连通得到的是最小生成森林。

衍生

LC 1584 连接所有点的最小费用(曼哈顿距离建边 + Kruskal)、1135 最低成本联通(会员)。MST 在网络布线、聚类(单链接聚类)里是真实生产算法。

维度KruskalPrim
视角边排序逐条试点扩张养树
数据结构排序 + 并查集堆(横切边)
复杂度O(E log E)O(E log V)
适合稀疏图、边列表输入稠密图、邻接矩阵
代码量最短中等
面试金句:"MST 两个算法是同一贪心性质的两种消费方式:Kruskal 按边全局排序,Prim 局部维护横切边堆——切割性质是它们共同的爹。"
切割性质是两大算法共同的理论基础。Kruskal 的"排序+并查集"把三个 deck(排序/并查集/贪心)串起来——图论是知识网络的最佳收束点。

LeetCode Shortlist

必刷题单:图的六个专题

专题题目(编号 · 难度)要点
遍历/岛屿200 岛屿 M · 695 面积 M · 130 被围区域 M · 994 腐烂橘子 M994 是多源 BFS 标准题
最短步数542 01 矩阵 M · 127 单词接龙 H · 752 打开转盘锁 M无权最短 = BFS 全家桶
拓扑排序207 课程表 M · 210 课程表 II M · 802 找到安全状态 MKahn 判环 + 输出序
二分图/染色785 判断二分图 M · 886 可能的二分法 M染色冲突判定
最短路743 网络延迟 M · 1631 最小体力消耗 M · 787 K 站中转 M743 Dijkstra;787 限定轮 BF
MST/并查集1584 连接所有点 M · 684 冗余连接 M · 721 账户合并 MKruskal + 并查集组合拳
刷法建议:200/994 练遍历 → 207 拓扑 → 785 染色 → 743 Dijkstra 模板 → 1584 收束(建边 + Kruskal + 并查集一次串三个 deck)。
六专题覆盖图题主干。994(多源 BFS)、207(拓扑)、743(Dijkstra)、1584(MST+并查集)四题是各自专题的模板题,必须默写级熟练。

Cheat Sheet

一页带走:选型三问 · 算法全家桶 · 五条检查

① 看到题先回答三个问题

先问答案决定你用谁
边有没有权?无权 → BFS(层数是步数);带权 → Dijkstra / Bellman-Ford / Floyd
单源还是多源?单源 → BFS / Dijkstra / BF;多源 → Floyd,或把所有源点一起入队的多源 BFS / Dijkstra
权里有没有负数?有 → Bellman-Ford(还顺带能检负环);没有 → Dijkstra

② 算法全家桶:解决什么 · 复杂度 · 前提

算法解决什么复杂度前提
BFS无权最短路 / 最少步数O(V+E)边无权
拓扑排序依赖排序 + 判环O(V+E)必须是 DAG
DFS 染色二分图判定O(V+E)
Dijkstra + 堆非负权单源最短路O((V+E)logV)边权非负
Bellman-Ford负权最短路 + 负环检测O(V·E)
Floyd多源最短路O(V³)无负环
Kruskal / Prim最小生成树O(E log E) / O(E log V)图连通

③ 遍历骨架:BFS 与 DFS 只差一个容器

维度BFSDFS
容器队列(FIFO) / 递归
拿到的是层次——无权最短路结构——路径与时间戳
visited 时机入队时标记进入时标记
选型一句话求最短用 BFS找结构用 DFS

④ 检查清单(写完逐条扫一遍)

1 · 建图三步走完了吗?数节点数 n(小心 1-indexed 输入)、加边、选起点。图题 80% 的 bug 在建图,不在算法。

2 · 有向还是无向?无向图漏加反向边 = 一半的图凭空消失;有向图多加反向边 = 答案全错。

3 · BFS 的 visited 入队时标记了吗?出队时才标记会让同一节点被多个前驱重复入队,队列规模直接爆炸。

4 · 前提满足吗?Dijkstra 要非负权、拓扑要 DAG、MST 要连通——前提不满足不会报错,只会静默给出错答案

5 · 图连通吗?一次遍历覆盖不到的点要不要再启动一次?连通分量计数题漏了这一步就会少算。

一句话背下来:图算法 = 把"东西"写成点、"关系"写成边,然后沿边一步一步走;所有算法的差别只在"用什么容器决定下一步走哪、走的时候记什么、什么时候停"。
速查页:左半是"选型三问 + 算法全家桶复杂度与前提表"(选对算法),右半是"遍历骨架对照 + 五条检查清单"(写对代码)。前提列单独强调是因为图算法的典型失败模式是"前提不满足却静默给出错答案"——比报错更危险。

Interview QA · Part 1

高频追问:遍历与结构

先自答,再对照:每题先说出你的答案(哪怕只说关键词),再展开下面那行——卡住的那 30 秒才是真正长记性的部分。

1 · 邻接表 vs 邻接矩阵怎么选?

稀疏 vs 稠密

表:空间 O(V+E)、遍历邻居快——稀疏图默认;矩阵:O(V²) 空间、查边 O(1)——稠密图、需要 O(1) 判边(Floyd/传递闭包)时用。

2 · BFS 为什么能求无权最短路?

层次单调

队列 FIFO 保证 k 层节点全部处理完才碰 k+1 层——首次到达某节点时的层数就是最少边数。换成栈(DFS)层次序被破坏,最短性失效。

3 · BFS 的 visited 什么时候标记?

入队时

入队时标记,不是出队时——否则同一节点被多个前驱重复入队,队列规模爆炸。这是 BFS 最经典的实现错误。

4 · 多源 BFS 是什么?

超级源点

所有源点同时入队(dist=0)再扩散——等价于建一个虚拟超级源点连向所有源点。994 腐烂橘子、542 01 矩阵都是它,无需逐源跑 BFS。

5 · 拓扑排序两种实现的取舍?

Kahn vs 逆后序

Kahn:入度队列,能顺便判环(done<n)、能输出"当前可执行集合";DFS 逆后序:代码短但要先判环才能用。工程调度(需要知道每步可并行的任务)用 Kahn 更自然。

6 · 有向图判环除了拓扑还有什么法?

三色标记

DFS 三色:白(未访)/灰(递归栈中)/黑(已完成)——遇到节点即有环。灰=当前路径,比"visited+inStack 两数组"更统一。

7 · 二分图为什么等价于无奇环?

交替染色

二分图上任意环必在两组间交替 → 环长偶数。染色法遇冲突 ⇔ 存在奇环。判定 O(V+E),不连通图逐分量染。

8 · 200 岛屿的"沉岛"技巧省了什么?

原地标记

把访问过的 '1' 改 '0',省掉 visited 矩阵 O(MN) 空间。代价是破坏输入——不允许改输入时用 visited 或恢复现场(回溯语义)。

前八题:表示法选型、BFS 最短性、入队标记、多源 BFS、拓扑两实现、三色判环、二分图等价、沉岛技巧。都是"实现层"的高频细节坑。

Interview QA · Part 2

高频追问:加权图

先自答,再对照。这一组的重点是"选型"——看到题先自己回答"有没有负权、单源还是多源",再对照答案。

9 · Dijkstra 为什么不能有负权边?

贪心锁定

它把"当前 dist 最小"的节点永久锁定——负权边可能让"绕更多条边反而更短",锁定的贪心不再成立。Bellman-Ford 不锁定节点,逐轮松弛容忍负权。

10 · 惰性删除是什么?为什么可以?

过期条目跳过

同一节点多次松弛会多次入堆;弹出时若 d>dist[u] 说明是过期条目直接跳过。省掉 decrease-key 的复杂实现,堆多占 O(E) 空间——工程上的标准取舍。

11 · Bellman-Ford 为什么是 V−1 轮?怎么检测负环?

归纳

最短路最多 V−1 条边;第 k 轮后"≤k 条边"的最短路全部正确(归纳)。第 V 轮仍能松弛 → 有环被反复松弛 → 负环。LC 787 的"限 k 站"就是只用前 k 轮。

12 · Floyd 的状态定义是什么?

中转点集合

dp[k][i][j] = 只经前 k 个点中转的 i→j 最短路;转移:经过 k 与不经过 k 取 min。本质是"允许中转集合"逐步扩大的 DP——空间可压成二维原地更新。

13 · Kruskal 和 Prim 怎么选?

稀疏 Kruskal

稀疏图 E≈V:Kruskal 排序+并查集,O(E log E)、代码最短;稠密图 E≈V²:Prim 堆版 O(E log V) 或朴素 O(V²)。输入形态(边列表 vs 邻接矩阵)常常替你做了决定。

14 · 图算法在真实系统里长什么样?

工程映射

调度系统=拓扑排序(Airflow DAG)、社交推荐=图的 BFS/连通、地图导航=Dijkstra/A*、网络布线= MST、依赖安装=拓扑 + 环检测报错。面试把算法映射回系统是高级信号

后六题:负权边界、惰性删除、BF 归纳与负环、Floyd 的 DP 本质、Kruskal/Prim 选型、工程映射。第 14 题把整 deck 拉回工程视角收尾。

Related & References

相关知识点与参考

算法系列(本分类)

复杂度分析 →(V/E 记账的语言)
回溯 →(332 欧拉路径的 DFS 底座)
贪心 →(Dijkstra/MST 的正确性来源)
二分查找 →(最短路+二分答案的组合题)

数据结构系列

堆与优先队列 →(Dijkstra/Prim 的外设)
并查集 →(Kruskal 判环/连通分量)
栈与队列 →(BFS 队列/DFS 栈)

参考来源(本 deck 结论可溯源至下列一手材料)

CLRS ch22/23/24/25基本图算法 · MST · 单源最短路 · 全源最短路
Dijkstra (1959) · Bellman (1958) · Floyd (1962)三大最短路算法原始论文
LeetCode 200/207/785/743/787/1584 官方题解六专题代表题
oi-wiki.org/graph图论算法的中文系统讲解(含 SPFA/负环)
Airflow/Argo 文档 · DAG 工作流拓扑排序的工程落地佐证
收尾:图是知识网络的收束点——堆/并查集/栈队列/贪心/回溯全部在此汇合。三大最短路算法的原始论文给出学术锚点,Airflow 工作流文档给出工程锚点。总页数 13。