百日算法修炼 · Day 18

百日算法修炼 · Day 18

主题:图的 BFS、DFS、拓扑排序、Kruskal、Prim、Dijkstra

语言:Java | 建议用时:120~180 分钟


阅读方式

本文继续采用"先练习、后答案"的方式:

  • 上半篇是独立练习区:只给题目、示例、思考问题和可折叠提示;
  • 下半篇是参考答案区:包含代码诊断、优化代码、过程图解、复杂度分析与易错点;
  • 推荐先独立写完 6 题,再通过"查看参考答案"链接逐题核对;
  • 图算法很容易因为"重复访问、环、方向、权重"等细节出错,建议每题都先说清楚数据结构的语义再写代码。

今天的核心不是记住 6 段代码,而是建立一套"图上的信息推进"思维:

  1. BFS / DFS:决定"下一个访问谁";
  2. 拓扑排序 :不断消除入度为 0 的节点;
  3. 最小生成树:不断选择"安全的最小边";
  4. Dijkstra:不断确定"当前最短距离已经不会再变"的节点。

今日任务地图

#mermaid-svg-zHR2xMw91BWeid12{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zHR2xMw91BWeid12 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zHR2xMw91BWeid12 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zHR2xMw91BWeid12 .error-icon{fill:#552222;}#mermaid-svg-zHR2xMw91BWeid12 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zHR2xMw91BWeid12 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zHR2xMw91BWeid12 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zHR2xMw91BWeid12 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zHR2xMw91BWeid12 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zHR2xMw91BWeid12 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zHR2xMw91BWeid12 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zHR2xMw91BWeid12 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zHR2xMw91BWeid12 .marker.cross{stroke:#333333;}#mermaid-svg-zHR2xMw91BWeid12 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zHR2xMw91BWeid12 p{margin:0;}#mermaid-svg-zHR2xMw91BWeid12 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zHR2xMw91BWeid12 .cluster-label text{fill:#333;}#mermaid-svg-zHR2xMw91BWeid12 .cluster-label span{color:#333;}#mermaid-svg-zHR2xMw91BWeid12 .cluster-label span p{background-color:transparent;}#mermaid-svg-zHR2xMw91BWeid12 .label text,#mermaid-svg-zHR2xMw91BWeid12 span{fill:#333;color:#333;}#mermaid-svg-zHR2xMw91BWeid12 .node rect,#mermaid-svg-zHR2xMw91BWeid12 .node circle,#mermaid-svg-zHR2xMw91BWeid12 .node ellipse,#mermaid-svg-zHR2xMw91BWeid12 .node polygon,#mermaid-svg-zHR2xMw91BWeid12 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zHR2xMw91BWeid12 .rough-node .label text,#mermaid-svg-zHR2xMw91BWeid12 .node .label text,#mermaid-svg-zHR2xMw91BWeid12 .image-shape .label,#mermaid-svg-zHR2xMw91BWeid12 .icon-shape .label{text-anchor:middle;}#mermaid-svg-zHR2xMw91BWeid12 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zHR2xMw91BWeid12 .rough-node .label,#mermaid-svg-zHR2xMw91BWeid12 .node .label,#mermaid-svg-zHR2xMw91BWeid12 .image-shape .label,#mermaid-svg-zHR2xMw91BWeid12 .icon-shape .label{text-align:center;}#mermaid-svg-zHR2xMw91BWeid12 .node.clickable{cursor:pointer;}#mermaid-svg-zHR2xMw91BWeid12 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zHR2xMw91BWeid12 .arrowheadPath{fill:#333333;}#mermaid-svg-zHR2xMw91BWeid12 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zHR2xMw91BWeid12 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zHR2xMw91BWeid12 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zHR2xMw91BWeid12 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zHR2xMw91BWeid12 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zHR2xMw91BWeid12 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zHR2xMw91BWeid12 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zHR2xMw91BWeid12 .cluster text{fill:#333;}#mermaid-svg-zHR2xMw91BWeid12 .cluster span{color:#333;}#mermaid-svg-zHR2xMw91BWeid12 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zHR2xMw91BWeid12 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zHR2xMw91BWeid12 rect.text{fill:none;stroke-width:0;}#mermaid-svg-zHR2xMw91BWeid12 .icon-shape,#mermaid-svg-zHR2xMw91BWeid12 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zHR2xMw91BWeid12 .icon-shape p,#mermaid-svg-zHR2xMw91BWeid12 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zHR2xMw91BWeid12 .icon-shape .label rect,#mermaid-svg-zHR2xMw91BWeid12 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zHR2xMw91BWeid12 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zHR2xMw91BWeid12 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zHR2xMw91BWeid12 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始 Day 18
BFS 宽度优先
DFS 深度优先
拓扑排序 Kahn
Kruskal 最小生成树
Prim 最小生成树
Dijkstra 单源最短路
统一理解 图上的贪心与搜索

题号 主题 核心能力
1 图的 BFS 队列、去重、按层扩散
2 图的 DFS 栈、去重、深入再回退
3 拓扑排序 入度表、零入度队列、环检测
4 Kruskal 边排序、并查集、避免成环
5 Prim 点解锁边、小根堆、切分思想
6 Dijkstra 距离松弛、确定节点、改写堆

第一部分:独立练习区

先不要向下翻到答案区。做图算法时,建议先回答三个问题:

我用什么结构保存"下一批候选"?如何避免重复处理?什么时候一个状态可以被最终确定?

统一图结构说明

今天默认使用如下抽象:

text 复制代码
Node
├── value   节点编号
├── in      入度
├── out     出度
├── nexts   从当前点可直接到达的邻接点
└── edges   从当前点出发的边

Edge
├── from
├── to
└── weight

Graph
├── nodes
└── edges

对 BFS、DFS、拓扑排序与 Dijkstra,可以直接理解为有向图。

对 Kruskal、Prim,本节讨论的是无向带权图 ;如果代码结构用有向 Edge 表示无向边,通常需要为一条无向边建立两个方向的边。


练习 1:图的宽度优先遍历 BFS

给定图中的起点 root,按照宽度优先的方式访问所有从 root 可达的节点,并保证:

  • 每个节点只处理一次;
  • 即使图中存在环,也不能死循环;
  • 使用迭代方式实现。

例如:

text 复制代码
1 -> 2 -> 4
|    |
v    v
3 -> 5

并且存在一条边:5 -> 1

若邻接表顺序为:

text 复制代码
1: [2, 3]
2: [4, 5]
3: [5]
5: [1]

一种 BFS 输出为:

text 复制代码
1 2 3 4 5

思考问题

  1. 为什么树的 BFS 可以不写 visited,而一般图的 BFS 几乎一定需要?
  2. visited 应该在"入队时"标记,还是"出队时"标记?两者有什么差别?
  3. 为什么队列适合表达"距离起点更近的节点优先处理"?
  4. 如果一个节点在发现时输出一次、出队时又输出一次,会发生什么?

提示 1:先占坑,再排队

一个邻居第一次被发现时就立即放入 visited,再入队。这样其他节点即使也指向它,也不会再次把它加入队列。
提示 2:只选一个明确的"处理时机"

建议把"节点出队"定义为真正处理节点的时刻,输出也只发生在这里。

写完后再看:[查看练习 1 参考答案](#查看练习 1 参考答案)


练习 2:图的深度优先遍历 DFS

仍从 root 出发,使用显式栈完成 DFS。

要求:

  • 每个可达节点只处理一次;
  • 图中有环时仍能正确结束;
  • 尽量让访问顺序与 nexts 中的邻接顺序一致。

例如:

text 复制代码
1 -> 2 -> 4
|    |
v    v
3 -> 5

若:

text 复制代码
1: [2, 3]
2: [4, 5]

希望优先沿 1 -> 2 -> 4 深入,而不是一开始就先处理 3

思考问题

  1. DFS 为什么用栈而不是队列?
  2. 如果按 nexts 正序压栈,真正弹出的顺序会怎样?
  3. 为了让 nexts 中靠前的邻居先被处理,为什么常常需要倒序压栈?
  4. visited 在入栈时标记有什么好处?

提示:注意 LIFO 会反转顺序

栈是后进先出。如果希望 nexts 中第 0 个邻居先被弹出,就可以从 nexts 的末尾向前压栈。

写完后再看:[查看练习 2 参考答案](#查看练习 2 参考答案)


练习 3:拓扑排序

给定一个有向图 graph,如果它是 DAG(Directed Acyclic Graph,有向无环图),请返回一种合法的拓扑序。

例如:

text 复制代码
A ---> C ---> D
 \           ^
  \-> B -----|

约束关系可以理解为:

text 复制代码
A 必须在 B、C 之前
B、C 必须在 D 之前

合法拓扑序之一:

text 复制代码
A B C D

也可能是:

text 复制代码
A C B D

思考问题

  1. 为什么"入度为 0"意味着一个节点当前没有前置依赖?
  2. 从结果中"删除"一个节点,应该如何体现在其他节点上?
  3. 为什么不能直接修改原图里的 node.in
  4. 如果最终输出节点数小于图中节点数,说明了什么?

提示 1:Kahn 算法

先把所有入度为 0 的节点放进队列。每弹出一个节点,就把它指向节点的剩余入度减 1;谁减到 0,谁进入队列。
提示 2:环不会产生新的零入度节点

如果剩余节点互相形成有向环,它们的入度无法通过删除环外节点降到 0,因此最终结果会缺节点。

写完后再看:[查看练习 3 参考答案](#查看练习 3 参考答案)


练习 4:Kruskal 最小生成树

给定一个无向连通带权图,求它的一棵最小生成树(MST)。

例如:

text 复制代码
      2
  A ----- B
  | \     |
4 |  \ 1  | 5
  |   \   |
  C ----- D
      3

你可以把 Kruskal 理解为:

text 复制代码
把所有边按权重从小到大检查

但是,不能为了便宜就把所有小边都拿走,因为生成树不能有环。

思考问题

  1. 每次拿到当前最小边时,怎样判断加入它会不会形成环?
  2. 为什么并查集特别适合回答"两个点是否已经连通"?
  3. 路径压缩与按大小合并分别优化了什么?
  4. 如果原图不连通,Kruskal 最终得到的是什么?

提示 1:连通性就是"集合"

如果边的两个端点已经属于同一个并查集,那么它们之间本来就有路径,再加这条边一定形成环。
提示 2:最小生成森林

对不连通图,同样的过程会在每个连通分量中得到一棵最小生成树,整体称为最小生成森林。

写完后再看:[查看练习 4 参考答案](#查看练习 4 参考答案)


练习 5:Prim 最小生成树

仍然求无向带权图的最小生成树,但这一次从"点"出发思考。

核心过程:

text 复制代码
选一个起点
-> 解锁它的边
-> 取最小候选边
-> 如果这条边能带来新节点,就收下
-> 新节点继续解锁新的边

思考问题

  1. Prim 为什么需要一个"已进入生成树的节点集合"?
  2. 小根堆里保存的是节点还是边?
  3. 为什么弹出的最小边也可能被丢弃?
  4. Kruskal 与 Prim 都是贪心,它们的"候选集合"有什么本质区别?
  5. 如何让 Prim 支持不连通图?

提示 1:边便宜还不够

如果一条最小边的目标节点已经在生成树中,那么加入它只会形成环,因此应该跳过。
提示 2:森林需要重新开局

遍历所有节点;如果某个节点还没访问过,就把它当作新连通分量的起点重新启动 Prim。

写完后再看:[查看练习 5 参考答案](#查看练习 5 参考答案)


练习 6:Dijkstra 单源最短路径

给定起点 from,求它到所有可达节点的最短距离。

例如:

text 复制代码
1 --2--> 2 --1--> 3 --1--> 4
 \       |
       |4
   v     v
   3     4

1 出发:

text 复制代码
dist(1) = 0
dist(2) = 2
dist(3) = 3
dist(4) = 4

今天建议写两版:

  1. 朴素版:每轮扫描所有候选距离,找出最小且未确定的节点;
  2. 堆优化版 :用支持 add / update / ignore 的小根堆维护候选节点。

思考问题

  1. Dijkstra 为什么要求边权不能为负数?
  2. 什么叫"松弛一条边"?
  3. 一个节点什么时候可以认为最短距离已经最终确定?
  4. 第一次发现邻居 to 时,新距离应该是 edge.weight,还是 dist(current) + edge.weight
  5. 改写堆为什么需要同时保存"节点位置"和"节点当前距离"?

提示 1:松弛公式

经过 current 到达 next 的候选距离是 distcurrent + weight(current, next)。如果它比 next 已知距离更小,就更新 next。
提示 2:三种堆状态

一个节点可能从未进堆、仍在堆中、已经弹出。三种状态分别对应 add、update 或 ignore。
重要前提

Dijkstra 不适用于包含负权边的图。负权场景需要考虑 Bellman-Ford 等其他算法。

写完后再看:[查看练习 6 参考答案](#查看练习 6 参考答案)


第二部分:参考答案与图解

到这里再开始核对。每题先看"题目回顾"和"核心思路",再看"原代码诊断"和优化代码。

重点比较的是:容器的语义、状态何时确定、去重发生在哪里


答案 1:图的宽度优先遍历 BFS

[返回练习 1](#返回练习 1)

题目回顾

从起点出发,按"离起点更近的节点优先"的方式扩散,并保证每个节点只处理一次。

核心思路:队列 + visited

#mermaid-svg-G3KcwHjUL5pr8AgU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-G3KcwHjUL5pr8AgU .error-icon{fill:#552222;}#mermaid-svg-G3KcwHjUL5pr8AgU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-G3KcwHjUL5pr8AgU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-G3KcwHjUL5pr8AgU .marker.cross{stroke:#333333;}#mermaid-svg-G3KcwHjUL5pr8AgU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-G3KcwHjUL5pr8AgU p{margin:0;}#mermaid-svg-G3KcwHjUL5pr8AgU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster-label text{fill:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster-label span{color:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster-label span p{background-color:transparent;}#mermaid-svg-G3KcwHjUL5pr8AgU .label text,#mermaid-svg-G3KcwHjUL5pr8AgU span{fill:#333;color:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU .node rect,#mermaid-svg-G3KcwHjUL5pr8AgU .node circle,#mermaid-svg-G3KcwHjUL5pr8AgU .node ellipse,#mermaid-svg-G3KcwHjUL5pr8AgU .node polygon,#mermaid-svg-G3KcwHjUL5pr8AgU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-G3KcwHjUL5pr8AgU .rough-node .label text,#mermaid-svg-G3KcwHjUL5pr8AgU .node .label text,#mermaid-svg-G3KcwHjUL5pr8AgU .image-shape .label,#mermaid-svg-G3KcwHjUL5pr8AgU .icon-shape .label{text-anchor:middle;}#mermaid-svg-G3KcwHjUL5pr8AgU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-G3KcwHjUL5pr8AgU .rough-node .label,#mermaid-svg-G3KcwHjUL5pr8AgU .node .label,#mermaid-svg-G3KcwHjUL5pr8AgU .image-shape .label,#mermaid-svg-G3KcwHjUL5pr8AgU .icon-shape .label{text-align:center;}#mermaid-svg-G3KcwHjUL5pr8AgU .node.clickable{cursor:pointer;}#mermaid-svg-G3KcwHjUL5pr8AgU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-G3KcwHjUL5pr8AgU .arrowheadPath{fill:#333333;}#mermaid-svg-G3KcwHjUL5pr8AgU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-G3KcwHjUL5pr8AgU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-G3KcwHjUL5pr8AgU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G3KcwHjUL5pr8AgU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-G3KcwHjUL5pr8AgU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G3KcwHjUL5pr8AgU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster text{fill:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster span{color:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-G3KcwHjUL5pr8AgU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU rect.text{fill:none;stroke-width:0;}#mermaid-svg-G3KcwHjUL5pr8AgU .icon-shape,#mermaid-svg-G3KcwHjUL5pr8AgU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G3KcwHjUL5pr8AgU .icon-shape p,#mermaid-svg-G3KcwHjUL5pr8AgU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-G3KcwHjUL5pr8AgU .icon-shape .label rect,#mermaid-svg-G3KcwHjUL5pr8AgU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G3KcwHjUL5pr8AgU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-G3KcwHjUL5pr8AgU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-G3KcwHjUL5pr8AgU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



起点入队并标记
队首出队
处理当前节点
扫描所有邻居
第一次访问?
标记并入队
跳过
队列为空?
结束

完整代码

java 复制代码
package class10;

import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;

/**
 * 图的宽度优先遍历(BFS)。
 *
 * 核心结构:
 * 1. 队列控制"先进先出";
 * 2. visited 防止有环图中的节点被重复处理。
 */
public class Code01_BFS {

    public static void bfs(Node root) {
        if (root == null) {
            return;
        }

        Queue<Node> queue = new ArrayDeque<>();
        Set<Node> visited = new HashSet<>();

        queue.offer(root);
        visited.add(root);

        while (!queue.isEmpty()) {
            Node current = queue.poll();

            // 一个节点只在真正出队处理时输出一次。
            System.out.println(current.value);

            for (Node next : current.nexts) {
                // add 返回 true,说明 next 是第一次被访问。
                if (visited.add(next)) {
                    queue.offer(next);
                }
            }
        }
    }
}

过程图解

假设:

text 复制代码
1: [2, 3]
2: [4, 5]
3: [5]
5: [1]
步骤 出队 新发现 队列 visited
1 1 2, 3 [2, 3] (1, 2, 3)
2 2 4, 5 [3, 4, 5] (1, 2, 3, 4, 5)
3 3 [4, 5] 不变
4 4 [5] 不变
5 5 1 已访问 [] 不变

复杂度

设可达点数为 V,可达边数为 E

  • 时间复杂度:O(V + E)
  • 额外空间:O(V) ,来自队列与 visited

易错点

  • 忘记 visited,有环图可能无限循环;
  • 一个节点重复入队,导致大量重复工作;
  • 同时在"发现"和"出队"两个阶段输出;
  • 把 BFS 和"按层返回列表"混为一谈:图 BFS 的核心是搜索顺序,不一定需要分层收集结果。

答案 2:图的深度优先遍历 DFS

[返回练习 2](#返回练习 2)

题目回顾

使用显式栈从起点不断深入,无法继续时再回到仍有候选分支的位置。

核心思路:栈 + visited

#mermaid-svg-kEQxCklfJtMQbDXa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kEQxCklfJtMQbDXa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kEQxCklfJtMQbDXa .error-icon{fill:#552222;}#mermaid-svg-kEQxCklfJtMQbDXa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kEQxCklfJtMQbDXa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kEQxCklfJtMQbDXa .marker.cross{stroke:#333333;}#mermaid-svg-kEQxCklfJtMQbDXa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kEQxCklfJtMQbDXa p{margin:0;}#mermaid-svg-kEQxCklfJtMQbDXa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kEQxCklfJtMQbDXa .cluster-label text{fill:#333;}#mermaid-svg-kEQxCklfJtMQbDXa .cluster-label span{color:#333;}#mermaid-svg-kEQxCklfJtMQbDXa .cluster-label span p{background-color:transparent;}#mermaid-svg-kEQxCklfJtMQbDXa .label text,#mermaid-svg-kEQxCklfJtMQbDXa span{fill:#333;color:#333;}#mermaid-svg-kEQxCklfJtMQbDXa .node rect,#mermaid-svg-kEQxCklfJtMQbDXa .node circle,#mermaid-svg-kEQxCklfJtMQbDXa .node ellipse,#mermaid-svg-kEQxCklfJtMQbDXa .node polygon,#mermaid-svg-kEQxCklfJtMQbDXa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kEQxCklfJtMQbDXa .rough-node .label text,#mermaid-svg-kEQxCklfJtMQbDXa .node .label text,#mermaid-svg-kEQxCklfJtMQbDXa .image-shape .label,#mermaid-svg-kEQxCklfJtMQbDXa .icon-shape .label{text-anchor:middle;}#mermaid-svg-kEQxCklfJtMQbDXa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kEQxCklfJtMQbDXa .rough-node .label,#mermaid-svg-kEQxCklfJtMQbDXa .node .label,#mermaid-svg-kEQxCklfJtMQbDXa .image-shape .label,#mermaid-svg-kEQxCklfJtMQbDXa .icon-shape .label{text-align:center;}#mermaid-svg-kEQxCklfJtMQbDXa .node.clickable{cursor:pointer;}#mermaid-svg-kEQxCklfJtMQbDXa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kEQxCklfJtMQbDXa .arrowheadPath{fill:#333333;}#mermaid-svg-kEQxCklfJtMQbDXa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kEQxCklfJtMQbDXa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kEQxCklfJtMQbDXa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kEQxCklfJtMQbDXa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kEQxCklfJtMQbDXa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kEQxCklfJtMQbDXa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kEQxCklfJtMQbDXa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kEQxCklfJtMQbDXa .cluster text{fill:#333;}#mermaid-svg-kEQxCklfJtMQbDXa .cluster span{color:#333;}#mermaid-svg-kEQxCklfJtMQbDXa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kEQxCklfJtMQbDXa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kEQxCklfJtMQbDXa rect.text{fill:none;stroke-width:0;}#mermaid-svg-kEQxCklfJtMQbDXa .icon-shape,#mermaid-svg-kEQxCklfJtMQbDXa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kEQxCklfJtMQbDXa .icon-shape p,#mermaid-svg-kEQxCklfJtMQbDXa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kEQxCklfJtMQbDXa .icon-shape .label rect,#mermaid-svg-kEQxCklfJtMQbDXa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kEQxCklfJtMQbDXa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kEQxCklfJtMQbDXa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kEQxCklfJtMQbDXa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否



起点压栈并标记
栈为空?
弹出栈顶节点
处理当前节点
倒序扫描邻居
邻居未访问?
标记并压栈
结束

为什么要倒序压栈?

假设:

text 复制代码
nexts = [2, 3, 4]

如果正序压栈:

text 复制代码
push 2
push 3
push 4

栈顶是 4,所以真实弹出顺序会变成:

text 复制代码
4 -> 3 -> 2

如果希望访问顺序更接近邻接表本身的 2 -> 3 -> 4,就倒序压栈。

完整代码

java 复制代码
package class10;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 图的深度优先遍历(DFS),使用显式栈实现。
 */
public class Code02_DFS {

    public static void dfs(Node root) {
        if (root == null) {
            return;
        }

        Deque<Node> stack = new ArrayDeque<>();
        Set<Node> visited = new HashSet<>();

        stack.push(root);
        visited.add(root);

        while (!stack.isEmpty()) {
            Node current = stack.pop();
            System.out.println(current.value);

            List<Node> nexts = current.nexts;

            // 栈是后进先出。倒序压栈后,弹出顺序与 nexts 原顺序一致。
            for (int i = nexts.size() - 1; i >= 0; i--) {
                Node next = nexts.get(i);
                if (visited.add(next)) {
                    stack.push(next);
                }
            }
        }
    }
}

复杂度

  • 时间复杂度:O(V + E)
  • 额外空间:O(V)

易错点

  • 没有去重导致环上死循环;
  • 正序压栈却误以为会正序访问;
  • visited 放得太晚,导致同一个节点被多个前驱重复压栈。

答案 3:拓扑排序

[返回练习 3](#返回练习 3)

题目回顾

在 DAG 中找出一个线性顺序,使每条有向边:

text 复制代码
u -> v

都满足:

text 复制代码
u 出现在 v 之前

核心思路:不断消除零入度节点

#mermaid-svg-rXNsmqAPhl5xwFpC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rXNsmqAPhl5xwFpC .error-icon{fill:#552222;}#mermaid-svg-rXNsmqAPhl5xwFpC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rXNsmqAPhl5xwFpC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rXNsmqAPhl5xwFpC .marker.cross{stroke:#333333;}#mermaid-svg-rXNsmqAPhl5xwFpC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rXNsmqAPhl5xwFpC p{margin:0;}#mermaid-svg-rXNsmqAPhl5xwFpC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster-label text{fill:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster-label span{color:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster-label span p{background-color:transparent;}#mermaid-svg-rXNsmqAPhl5xwFpC .label text,#mermaid-svg-rXNsmqAPhl5xwFpC span{fill:#333;color:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC .node rect,#mermaid-svg-rXNsmqAPhl5xwFpC .node circle,#mermaid-svg-rXNsmqAPhl5xwFpC .node ellipse,#mermaid-svg-rXNsmqAPhl5xwFpC .node polygon,#mermaid-svg-rXNsmqAPhl5xwFpC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rXNsmqAPhl5xwFpC .rough-node .label text,#mermaid-svg-rXNsmqAPhl5xwFpC .node .label text,#mermaid-svg-rXNsmqAPhl5xwFpC .image-shape .label,#mermaid-svg-rXNsmqAPhl5xwFpC .icon-shape .label{text-anchor:middle;}#mermaid-svg-rXNsmqAPhl5xwFpC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rXNsmqAPhl5xwFpC .rough-node .label,#mermaid-svg-rXNsmqAPhl5xwFpC .node .label,#mermaid-svg-rXNsmqAPhl5xwFpC .image-shape .label,#mermaid-svg-rXNsmqAPhl5xwFpC .icon-shape .label{text-align:center;}#mermaid-svg-rXNsmqAPhl5xwFpC .node.clickable{cursor:pointer;}#mermaid-svg-rXNsmqAPhl5xwFpC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rXNsmqAPhl5xwFpC .arrowheadPath{fill:#333333;}#mermaid-svg-rXNsmqAPhl5xwFpC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rXNsmqAPhl5xwFpC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rXNsmqAPhl5xwFpC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rXNsmqAPhl5xwFpC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rXNsmqAPhl5xwFpC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rXNsmqAPhl5xwFpC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster text{fill:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster span{color:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rXNsmqAPhl5xwFpC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC rect.text{fill:none;stroke-width:0;}#mermaid-svg-rXNsmqAPhl5xwFpC .icon-shape,#mermaid-svg-rXNsmqAPhl5xwFpC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rXNsmqAPhl5xwFpC .icon-shape p,#mermaid-svg-rXNsmqAPhl5xwFpC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rXNsmqAPhl5xwFpC .icon-shape .label rect,#mermaid-svg-rXNsmqAPhl5xwFpC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rXNsmqAPhl5xwFpC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rXNsmqAPhl5xwFpC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rXNsmqAPhl5xwFpC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否





复制所有节点入度
零入度节点入队
队列为空?
弹出一个零入度节点
加入拓扑序
所有后继剩余入度减 1
某后继变为 0?
该后继入队
结果数等于节点数?
得到拓扑序
存在有向环

完整代码

java 复制代码
package class10;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;

/**
 * Kahn 算法实现拓扑排序。
 *
 * 只适用于有向无环图(DAG)。
 */
public class Code03_TopologySort {

    public static List<Node> topologicalSort(Graph graph) {
        List<Node> result = new ArrayList<>();
        if (graph == null || graph.nodes.isEmpty()) {
            return result;
        }

        // 使用副本记录"剩余入度",避免修改原图中的 node.in。
        Map<Node, Integer> inDegree = new HashMap<>();
        Queue<Node> zeroInQueue = new ArrayDeque<>();

        for (Node node : graph.nodes.values()) {
            inDegree.put(node, node.in);
            if (node.in == 0) {
                zeroInQueue.offer(node);
            }
        }

        while (!zeroInQueue.isEmpty()) {
            Node current = zeroInQueue.poll();
            result.add(current);

            // 删除 current 等价于:它指向的所有节点入度减 1。
            for (Node next : current.nexts) {
                int nextIn = inDegree.get(next) - 1;
                inDegree.put(next, nextIn);

                if (nextIn == 0) {
                    zeroInQueue.offer(next);
                }
            }
        }

        // 若仍有节点无法被"消掉",说明存在有向环。
        if (result.size() != graph.nodes.size()) {
            throw new IllegalArgumentException(
                    "Graph contains a cycle; topological order does not exist.");
        }

        return result;
    }
}

为什么不修改 node.in

因为拓扑排序只是一次计算。

如果直接对原图做:

text 复制代码
node.in--

下一次再调用拓扑排序时,图的入度信息已经被污染。

因此使用:

java 复制代码
Map<Node, Integer> inDegree

保存副本更安全。

如何检测环?

如果:

text 复制代码
result.size() < graph.nodes.size()

说明还有节点从未进入零入度队列。

最常见原因就是这些剩余节点处在有向环中。

复杂度

  • 时间复杂度:O(V + E)
  • 额外空间:O(V)

易错点

  • 拓扑排序只针对有向图
  • 有合法拓扑序的前提是无环
  • 拓扑序通常不唯一;
  • 忘记维护入度副本,污染原图。

答案 4:Kruskal 最小生成树

[返回练习 4](#返回练习 4)

题目回顾

在无向带权图中选择若干条边,使所有节点连通、无环,并让总权重最小。

核心思路:边从小到大 + 并查集判断成环

#mermaid-svg-eSJKKoLuueEXJv2I{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eSJKKoLuueEXJv2I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eSJKKoLuueEXJv2I .error-icon{fill:#552222;}#mermaid-svg-eSJKKoLuueEXJv2I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eSJKKoLuueEXJv2I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eSJKKoLuueEXJv2I .marker.cross{stroke:#333333;}#mermaid-svg-eSJKKoLuueEXJv2I svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eSJKKoLuueEXJv2I p{margin:0;}#mermaid-svg-eSJKKoLuueEXJv2I .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eSJKKoLuueEXJv2I .cluster-label text{fill:#333;}#mermaid-svg-eSJKKoLuueEXJv2I .cluster-label span{color:#333;}#mermaid-svg-eSJKKoLuueEXJv2I .cluster-label span p{background-color:transparent;}#mermaid-svg-eSJKKoLuueEXJv2I .label text,#mermaid-svg-eSJKKoLuueEXJv2I span{fill:#333;color:#333;}#mermaid-svg-eSJKKoLuueEXJv2I .node rect,#mermaid-svg-eSJKKoLuueEXJv2I .node circle,#mermaid-svg-eSJKKoLuueEXJv2I .node ellipse,#mermaid-svg-eSJKKoLuueEXJv2I .node polygon,#mermaid-svg-eSJKKoLuueEXJv2I .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eSJKKoLuueEXJv2I .rough-node .label text,#mermaid-svg-eSJKKoLuueEXJv2I .node .label text,#mermaid-svg-eSJKKoLuueEXJv2I .image-shape .label,#mermaid-svg-eSJKKoLuueEXJv2I .icon-shape .label{text-anchor:middle;}#mermaid-svg-eSJKKoLuueEXJv2I .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eSJKKoLuueEXJv2I .rough-node .label,#mermaid-svg-eSJKKoLuueEXJv2I .node .label,#mermaid-svg-eSJKKoLuueEXJv2I .image-shape .label,#mermaid-svg-eSJKKoLuueEXJv2I .icon-shape .label{text-align:center;}#mermaid-svg-eSJKKoLuueEXJv2I .node.clickable{cursor:pointer;}#mermaid-svg-eSJKKoLuueEXJv2I .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eSJKKoLuueEXJv2I .arrowheadPath{fill:#333333;}#mermaid-svg-eSJKKoLuueEXJv2I .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eSJKKoLuueEXJv2I .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eSJKKoLuueEXJv2I .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eSJKKoLuueEXJv2I .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eSJKKoLuueEXJv2I .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eSJKKoLuueEXJv2I .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eSJKKoLuueEXJv2I .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eSJKKoLuueEXJv2I .cluster text{fill:#333;}#mermaid-svg-eSJKKoLuueEXJv2I .cluster span{color:#333;}#mermaid-svg-eSJKKoLuueEXJv2I div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eSJKKoLuueEXJv2I .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eSJKKoLuueEXJv2I rect.text{fill:none;stroke-width:0;}#mermaid-svg-eSJKKoLuueEXJv2I .icon-shape,#mermaid-svg-eSJKKoLuueEXJv2I .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eSJKKoLuueEXJv2I .icon-shape p,#mermaid-svg-eSJKKoLuueEXJv2I .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eSJKKoLuueEXJv2I .icon-shape .label rect,#mermaid-svg-eSJKKoLuueEXJv2I .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eSJKKoLuueEXJv2I .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eSJKKoLuueEXJv2I .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eSJKKoLuueEXJv2I :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



所有边进入小根堆
取当前最小边
两端已连通?
跳过 会成环
选择这条边
并查集合并两端集合
还有候选边?
结束

为什么"两个端点已连通"就一定会成环?

假设当前已经存在一条路径:

text 复制代码
A -> ... -> B

此时再加入边:

text 复制代码
A -- B

就会构成一个闭环。

所以 Kruskal 的关键问题不是:

text 复制代码
"这条边够不够小?"

而是:

text 复制代码
"这条边够小,并且安全吗?"

并查集图解

#mermaid-svg-WkSaLXUb2eWxIfCj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WkSaLXUb2eWxIfCj .error-icon{fill:#552222;}#mermaid-svg-WkSaLXUb2eWxIfCj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WkSaLXUb2eWxIfCj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WkSaLXUb2eWxIfCj .marker.cross{stroke:#333333;}#mermaid-svg-WkSaLXUb2eWxIfCj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WkSaLXUb2eWxIfCj p{margin:0;}#mermaid-svg-WkSaLXUb2eWxIfCj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster-label text{fill:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster-label span{color:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster-label span p{background-color:transparent;}#mermaid-svg-WkSaLXUb2eWxIfCj .label text,#mermaid-svg-WkSaLXUb2eWxIfCj span{fill:#333;color:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj .node rect,#mermaid-svg-WkSaLXUb2eWxIfCj .node circle,#mermaid-svg-WkSaLXUb2eWxIfCj .node ellipse,#mermaid-svg-WkSaLXUb2eWxIfCj .node polygon,#mermaid-svg-WkSaLXUb2eWxIfCj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WkSaLXUb2eWxIfCj .rough-node .label text,#mermaid-svg-WkSaLXUb2eWxIfCj .node .label text,#mermaid-svg-WkSaLXUb2eWxIfCj .image-shape .label,#mermaid-svg-WkSaLXUb2eWxIfCj .icon-shape .label{text-anchor:middle;}#mermaid-svg-WkSaLXUb2eWxIfCj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WkSaLXUb2eWxIfCj .rough-node .label,#mermaid-svg-WkSaLXUb2eWxIfCj .node .label,#mermaid-svg-WkSaLXUb2eWxIfCj .image-shape .label,#mermaid-svg-WkSaLXUb2eWxIfCj .icon-shape .label{text-align:center;}#mermaid-svg-WkSaLXUb2eWxIfCj .node.clickable{cursor:pointer;}#mermaid-svg-WkSaLXUb2eWxIfCj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WkSaLXUb2eWxIfCj .arrowheadPath{fill:#333333;}#mermaid-svg-WkSaLXUb2eWxIfCj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WkSaLXUb2eWxIfCj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WkSaLXUb2eWxIfCj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WkSaLXUb2eWxIfCj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WkSaLXUb2eWxIfCj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WkSaLXUb2eWxIfCj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster text{fill:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster span{color:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WkSaLXUb2eWxIfCj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj rect.text{fill:none;stroke-width:0;}#mermaid-svg-WkSaLXUb2eWxIfCj .icon-shape,#mermaid-svg-WkSaLXUb2eWxIfCj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WkSaLXUb2eWxIfCj .icon-shape p,#mermaid-svg-WkSaLXUb2eWxIfCj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WkSaLXUb2eWxIfCj .icon-shape .label rect,#mermaid-svg-WkSaLXUb2eWxIfCj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WkSaLXUb2eWxIfCj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WkSaLXUb2eWxIfCj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WkSaLXUb2eWxIfCj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

节点各自一个集合
find 查询代表节点
代表节点相同?
已经连通
union 合并
小集合挂到大集合
find 时路径压缩

完整代码

java 复制代码
package class10;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;

/**
 * Kruskal 最小生成树算法。
 *
 * 思路:按边权从小到大尝试加入边;
 * 若一条边的两个端点已经连通,则跳过它以避免形成环。
 *
 * 对不连通图,返回最小生成森林。
 */
public class Code04_Kruskal {

    public static List<Edge> kruskal(Graph graph) {
        List<Edge> result = new ArrayList<>();
        if (graph == null || graph.nodes.isEmpty()) {
            return result;
        }

        PriorityQueue<Edge> minEdgeHeap =
                new PriorityQueue<>(Comparator.comparingInt(edge -> edge.weight));
        minEdgeHeap.addAll(graph.edges);

        UnionFind<Node> unionFind = new UnionFind<>(graph.nodes.values());

        while (!minEdgeHeap.isEmpty()) {
            Edge edge = minEdgeHeap.poll();

            if (!unionFind.isSameSet(edge.from, edge.to)) {
                unionFind.union(edge.from, edge.to);
                result.add(edge);
            }
        }

        return result;
    }

    /**
     * 并查集:路径压缩 + 按集合大小合并。
     */
    public static class UnionFind<T> {
        private final Map<T, T> parent = new HashMap<>();
        private final Map<T, Integer> size = new HashMap<>();

        public UnionFind(Iterable<T> values) {
            for (T value : values) {
                parent.put(value, value);
                size.put(value, 1);
            }
        }

        /**
         * 找到代表节点,并把沿途节点直接挂到代表节点上。
         */
        public T find(T value) {
            if (!parent.containsKey(value)) {
                return null;
            }

            Deque<T> path = new ArrayDeque<>();
            T current = value;

            while (parent.get(current) != current) {
                path.push(current);
                current = parent.get(current);
            }

            while (!path.isEmpty()) {
                parent.put(path.pop(), current);
            }

            return current;
        }

        public boolean isSameSet(T a, T b) {
            T rootA = find(a);
            T rootB = find(b);
            return rootA != null && rootA == rootB;
        }

        public void union(T a, T b) {
            T rootA = find(a);
            T rootB = find(b);

            if (rootA == null || rootB == null || rootA == rootB) {
                return;
            }

            T big = size.get(rootA) >= size.get(rootB) ? rootA : rootB;
            T small = big == rootA ? rootB : rootA;

            parent.put(small, big);
            size.put(big, size.get(big) + size.get(small));
            size.remove(small);
        }

        public int getSetCount() {
            return size.size();
        }
    }
}

复杂度

若使用小根堆装入所有边:

  • 建堆 / 取边整体可近似理解为 O(E log E)
  • 并查集操作摊还接近常数;
  • 总体常写为 O(E log E)

易错点

  • MST 是无向图概念;
  • 只按权重选边、不检查连通性会形成环;
  • 对不连通图不存在"一棵覆盖全部节点的生成树",只有生成森林;
  • 如果无向边用两个方向的 Edge 表示,Kruskal 会看到重复方向,但并查集仍会阻止重复成环。

答案 5:Prim 最小生成树

[返回练习 5](#返回练习 5)

题目回顾

从某个点出发,让"已经进入生成树的节点集合"逐步扩大;每次选择一条最便宜、且能带来新节点的边。

核心思路:点解锁边

#mermaid-svg-4SAmdqjYZd2pDGvw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4SAmdqjYZd2pDGvw .error-icon{fill:#552222;}#mermaid-svg-4SAmdqjYZd2pDGvw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4SAmdqjYZd2pDGvw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4SAmdqjYZd2pDGvw .marker.cross{stroke:#333333;}#mermaid-svg-4SAmdqjYZd2pDGvw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4SAmdqjYZd2pDGvw p{margin:0;}#mermaid-svg-4SAmdqjYZd2pDGvw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster-label text{fill:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster-label span{color:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster-label span p{background-color:transparent;}#mermaid-svg-4SAmdqjYZd2pDGvw .label text,#mermaid-svg-4SAmdqjYZd2pDGvw span{fill:#333;color:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw .node rect,#mermaid-svg-4SAmdqjYZd2pDGvw .node circle,#mermaid-svg-4SAmdqjYZd2pDGvw .node ellipse,#mermaid-svg-4SAmdqjYZd2pDGvw .node polygon,#mermaid-svg-4SAmdqjYZd2pDGvw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4SAmdqjYZd2pDGvw .rough-node .label text,#mermaid-svg-4SAmdqjYZd2pDGvw .node .label text,#mermaid-svg-4SAmdqjYZd2pDGvw .image-shape .label,#mermaid-svg-4SAmdqjYZd2pDGvw .icon-shape .label{text-anchor:middle;}#mermaid-svg-4SAmdqjYZd2pDGvw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4SAmdqjYZd2pDGvw .rough-node .label,#mermaid-svg-4SAmdqjYZd2pDGvw .node .label,#mermaid-svg-4SAmdqjYZd2pDGvw .image-shape .label,#mermaid-svg-4SAmdqjYZd2pDGvw .icon-shape .label{text-align:center;}#mermaid-svg-4SAmdqjYZd2pDGvw .node.clickable{cursor:pointer;}#mermaid-svg-4SAmdqjYZd2pDGvw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4SAmdqjYZd2pDGvw .arrowheadPath{fill:#333333;}#mermaid-svg-4SAmdqjYZd2pDGvw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4SAmdqjYZd2pDGvw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4SAmdqjYZd2pDGvw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4SAmdqjYZd2pDGvw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4SAmdqjYZd2pDGvw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4SAmdqjYZd2pDGvw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster text{fill:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster span{color:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4SAmdqjYZd2pDGvw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw rect.text{fill:none;stroke-width:0;}#mermaid-svg-4SAmdqjYZd2pDGvw .icon-shape,#mermaid-svg-4SAmdqjYZd2pDGvw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4SAmdqjYZd2pDGvw .icon-shape p,#mermaid-svg-4SAmdqjYZd2pDGvw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4SAmdqjYZd2pDGvw .icon-shape .label rect,#mermaid-svg-4SAmdqjYZd2pDGvw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4SAmdqjYZd2pDGvw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4SAmdqjYZd2pDGvw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4SAmdqjYZd2pDGvw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否





选一个未访问起点
起点加入 visited
起点的边进入小根堆
堆为空?
弹出最小边
目标节点已访问?
选择该边
新节点加入 visited
新节点的边全部解锁
还有未访问节点?
结束

Kruskal 与 Prim 的区别

对比 Kruskal Prim
贪心对象 全图最小边 当前生成树边界上的最小边
防环方式 并查集 visited 节点集合
核心容器 边小根堆 + 并查集 边小根堆 + visited
视角 从边出发 从点扩张
不连通图 最小生成森林 逐分量启动后得到森林

完整代码

java 复制代码
package class10;

import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.PriorityQueue;
import java.util.Set;

/**
 * Prim 最小生成树算法。
 *
 * 每次从"已访问节点集合"向外选择权重最小、且能引入新节点的边。
 *
 * 对不连通图,外层循环会从每个未访问连通分量重新启动,
 * 因而返回最小生成森林。
 */
public class Code05_Prim {

    public static Set<Edge> prim(Graph graph) {
        Set<Edge> result = new LinkedHashSet<>();
        if (graph == null || graph.nodes.isEmpty()) {
            return result;
        }

        Set<Node> visited = new HashSet<>();
        PriorityQueue<Edge> minEdgeHeap =
                new PriorityQueue<>(Comparator.comparingInt(edge -> edge.weight));

        // 支持森林:每个尚未访问的节点都可以作为新连通分量的起点。
        for (Node start : graph.nodes.values()) {
            if (!visited.add(start)) {
                continue;
            }

            minEdgeHeap.addAll(start.edges);

            while (!minEdgeHeap.isEmpty()) {
                Edge edge = minEdgeHeap.poll();
                Node next = edge.to;

                // edge.to 已在树中,则这条边会形成环,直接跳过。
                if (!visited.add(next)) {
                    continue;
                }

                result.add(edge);

                // 新节点进入生成树后,解锁它能够提供的所有候选边。
                minEdgeHeap.addAll(next.edges);
            }
        }

        return result;
    }
}

关于无向图的表示

这份 Node / Edge 结构中:

text 复制代码
edge.from -> edge.to

本身带有方向。

因此如果要用它表达无向边:

text 复制代码
A -- B

常见做法是同时建立:

text 复制代码
A -> B
B -> A

Prim 才能从任意一端"解锁"到另一端。

复杂度

使用二叉堆时:

  • 每条候选边最多进入堆;
  • 时间复杂度通常写作 O(E log E)
  • 也常根据具体实现表述为 O(E log V)
  • 额外空间:O(V + E)

易错点

  • 只看最小边,不看它能否引入新节点;
  • 忘记支持不连通图时重新选择起点;
  • 图结构是有向边表示,却直接把它当无向图使用;
  • 误以为 Prim 需要并查集:标准 Prim 用 visited 就足够防环。

答案 6:Dijkstra 单源最短路径

[返回练习 6](#返回练习 6)

题目回顾

从起点出发,计算到所有可达节点的最短距离。

前提:

text 复制代码
所有边权 >= 0

一句话理解 Dijkstra

每一轮从"还没最终确定"的节点中,选当前距离最小者。

因为没有负权边,所以以后再绕路也不可能把它变得更短。

核心流程

#mermaid-svg-5UMzPJgXjRkiGjCt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5UMzPJgXjRkiGjCt .error-icon{fill:#552222;}#mermaid-svg-5UMzPJgXjRkiGjCt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5UMzPJgXjRkiGjCt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5UMzPJgXjRkiGjCt .marker.cross{stroke:#333333;}#mermaid-svg-5UMzPJgXjRkiGjCt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5UMzPJgXjRkiGjCt p{margin:0;}#mermaid-svg-5UMzPJgXjRkiGjCt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster-label text{fill:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster-label span{color:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster-label span p{background-color:transparent;}#mermaid-svg-5UMzPJgXjRkiGjCt .label text,#mermaid-svg-5UMzPJgXjRkiGjCt span{fill:#333;color:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt .node rect,#mermaid-svg-5UMzPJgXjRkiGjCt .node circle,#mermaid-svg-5UMzPJgXjRkiGjCt .node ellipse,#mermaid-svg-5UMzPJgXjRkiGjCt .node polygon,#mermaid-svg-5UMzPJgXjRkiGjCt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5UMzPJgXjRkiGjCt .rough-node .label text,#mermaid-svg-5UMzPJgXjRkiGjCt .node .label text,#mermaid-svg-5UMzPJgXjRkiGjCt .image-shape .label,#mermaid-svg-5UMzPJgXjRkiGjCt .icon-shape .label{text-anchor:middle;}#mermaid-svg-5UMzPJgXjRkiGjCt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5UMzPJgXjRkiGjCt .rough-node .label,#mermaid-svg-5UMzPJgXjRkiGjCt .node .label,#mermaid-svg-5UMzPJgXjRkiGjCt .image-shape .label,#mermaid-svg-5UMzPJgXjRkiGjCt .icon-shape .label{text-align:center;}#mermaid-svg-5UMzPJgXjRkiGjCt .node.clickable{cursor:pointer;}#mermaid-svg-5UMzPJgXjRkiGjCt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5UMzPJgXjRkiGjCt .arrowheadPath{fill:#333333;}#mermaid-svg-5UMzPJgXjRkiGjCt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5UMzPJgXjRkiGjCt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5UMzPJgXjRkiGjCt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5UMzPJgXjRkiGjCt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5UMzPJgXjRkiGjCt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5UMzPJgXjRkiGjCt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster text{fill:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster span{color:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5UMzPJgXjRkiGjCt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt rect.text{fill:none;stroke-width:0;}#mermaid-svg-5UMzPJgXjRkiGjCt .icon-shape,#mermaid-svg-5UMzPJgXjRkiGjCt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5UMzPJgXjRkiGjCt .icon-shape p,#mermaid-svg-5UMzPJgXjRkiGjCt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5UMzPJgXjRkiGjCt .icon-shape .label rect,#mermaid-svg-5UMzPJgXjRkiGjCt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5UMzPJgXjRkiGjCt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5UMzPJgXjRkiGjCt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5UMzPJgXjRkiGjCt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



起点距离设为 0
选未确定节点中距离最小者
当前节点距离最终确定
遍历当前节点所有出边
计算 新距离 = 当前距离 + 边权
新距离更短?
更新邻居距离
保持原距离
还有未确定可达节点?
结束

什么叫"松弛"?

对边:

text 复制代码
u --w--> v

如果当前已经知道:

text 复制代码
dist[u]

那么经过 uv 的候选距离是:

text 复制代码
candidate = dist[u] + w

若:

text 复制代码
candidate < dist[v]

就更新:

text 复制代码
dist[v] = candidate

这一步叫做松弛(relaxation)

朴素版为什么是 O(V² + E)?

每确定一个节点,都要扫描 distanceMap

text 复制代码
从未确定节点中找到当前距离最小者

这个查找最坏要看 O(V) 个节点,一共做 O(V) 轮,因此产生 O(V²)。

边的松弛总共 O(E)。

改写堆的三个状态

#mermaid-svg-YwYs6odnd5XQHDP4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YwYs6odnd5XQHDP4 .error-icon{fill:#552222;}#mermaid-svg-YwYs6odnd5XQHDP4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YwYs6odnd5XQHDP4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YwYs6odnd5XQHDP4 .marker.cross{stroke:#333333;}#mermaid-svg-YwYs6odnd5XQHDP4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YwYs6odnd5XQHDP4 p{margin:0;}#mermaid-svg-YwYs6odnd5XQHDP4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster-label text{fill:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster-label span{color:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster-label span p{background-color:transparent;}#mermaid-svg-YwYs6odnd5XQHDP4 .label text,#mermaid-svg-YwYs6odnd5XQHDP4 span{fill:#333;color:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 .node rect,#mermaid-svg-YwYs6odnd5XQHDP4 .node circle,#mermaid-svg-YwYs6odnd5XQHDP4 .node ellipse,#mermaid-svg-YwYs6odnd5XQHDP4 .node polygon,#mermaid-svg-YwYs6odnd5XQHDP4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YwYs6odnd5XQHDP4 .rough-node .label text,#mermaid-svg-YwYs6odnd5XQHDP4 .node .label text,#mermaid-svg-YwYs6odnd5XQHDP4 .image-shape .label,#mermaid-svg-YwYs6odnd5XQHDP4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-YwYs6odnd5XQHDP4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YwYs6odnd5XQHDP4 .rough-node .label,#mermaid-svg-YwYs6odnd5XQHDP4 .node .label,#mermaid-svg-YwYs6odnd5XQHDP4 .image-shape .label,#mermaid-svg-YwYs6odnd5XQHDP4 .icon-shape .label{text-align:center;}#mermaid-svg-YwYs6odnd5XQHDP4 .node.clickable{cursor:pointer;}#mermaid-svg-YwYs6odnd5XQHDP4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YwYs6odnd5XQHDP4 .arrowheadPath{fill:#333333;}#mermaid-svg-YwYs6odnd5XQHDP4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YwYs6odnd5XQHDP4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YwYs6odnd5XQHDP4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YwYs6odnd5XQHDP4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YwYs6odnd5XQHDP4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YwYs6odnd5XQHDP4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster text{fill:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster span{color:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YwYs6odnd5XQHDP4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-YwYs6odnd5XQHDP4 .icon-shape,#mermaid-svg-YwYs6odnd5XQHDP4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YwYs6odnd5XQHDP4 .icon-shape p,#mermaid-svg-YwYs6odnd5XQHDP4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YwYs6odnd5XQHDP4 .icon-shape .label rect,#mermaid-svg-YwYs6odnd5XQHDP4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YwYs6odnd5XQHDP4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YwYs6odnd5XQHDP4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YwYs6odnd5XQHDP4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} add
更短距离 update
pop
再次遇到 ignore
节点从未进堆
节点在堆中
最短距离已确定

为了支持 update,普通 PriorityQueue 不方便直接修改内部某个节点的优先级。

因此改写堆维护两张表:

text 复制代码
indexMap:
节点 -> 当前在堆数组中的位置
-1 表示已经弹出

distanceMap:
节点 -> 当前候选最短距离

距离变小时:

text 复制代码
更新 distanceMap
-> 从 indexMap 找到位置
-> heapInsert 向上调整

完整代码

java 复制代码
package class10;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Dijkstra 单源最短路径。
 *
 * 前提:所有可达边的权重都必须 >= 0。
 */
public class Code06_Dijkstra {

    /**
     * 朴素版:每轮在线性扫描中找到尚未确定的最短节点。
     *
     * 时间复杂度:O(V^2 + E)
     */
    public static HashMap<Node, Integer> dijkstra1(Node from) {
        HashMap<Node, Integer> distanceMap = new HashMap<>();
        if (from == null) {
            return distanceMap;
        }

        Set<Node> selected = new HashSet<>();
        distanceMap.put(from, 0);

        Node minNode =
                getMinDistanceNodeWithoutSelected(distanceMap, selected);

        while (minNode != null) {
            int baseDistance = distanceMap.get(minNode);

            for (Edge edge : minNode.edges) {
                checkNonNegative(edge);

                Node to = edge.to;
                int newDistance = safeAdd(baseDistance, edge.weight);

                // 第一次发现:写入"起点到 minNode 的距离 + 当前边权";
                // 再次发现:只保留更小的距离。
                distanceMap.merge(to, newDistance, Math::min);
            }

            // minNode 的最短距离从此确定,不再被更新。
            selected.add(minNode);
            minNode =
                    getMinDistanceNodeWithoutSelected(distanceMap, selected);
        }

        return distanceMap;
    }

    private static Node getMinDistanceNodeWithoutSelected(
            Map<Node, Integer> distanceMap,
            Set<Node> selected) {

        Node minNode = null;
        int minDistance = Integer.MAX_VALUE;

        for (Map.Entry<Node, Integer> entry : distanceMap.entrySet()) {
            if (!selected.contains(entry.getKey())
                    && entry.getValue() < minDistance) {
                minNode = entry.getKey();
                minDistance = entry.getValue();
            }
        }

        return minNode;
    }

    /**
     * 加强版:使用支持 decrease-key 的小根堆。
     *
     * 时间复杂度:O((V + E) log V)
     *
     * @param from 起点
     * @param initialCapacity 初始堆容量;容量不足时会自动扩容
     */
    public static HashMap<Node, Integer> dijkstra2(
            Node from,
            int initialCapacity) {

        HashMap<Node, Integer> result = new HashMap<>();
        if (from == null) {
            return result;
        }

        NodeHeap heap = new NodeHeap(initialCapacity);
        heap.addOrUpdateOrIgnore(from, 0);

        while (!heap.isEmpty()) {
            Record record = heap.pop();

            // 从堆中弹出的节点,其最短距离已经确定。
            result.put(record.node, record.distance);

            for (Edge edge : record.node.edges) {
                checkNonNegative(edge);

                int newDistance =
                        safeAdd(record.distance, edge.weight);
                heap.addOrUpdateOrIgnore(edge.to, newDistance);
            }
        }

        return result;
    }

    private static void checkNonNegative(Edge edge) {
        if (edge.weight < 0) {
            throw new IllegalArgumentException(
                    "Dijkstra does not allow negative edge weights.");
        }
    }

    private static int safeAdd(int a, int b) {
        long sum = (long) a + b;
        if (sum > Integer.MAX_VALUE) {
            throw new ArithmeticException(
                    "Shortest-path distance exceeds Integer.MAX_VALUE.");
        }
        return (int) sum;
    }

    /**
     * 支持三种操作的改写小根堆:
     * 1. add:从未入堆 -> 加入;
     * 2. update:仍在堆中且距离变小 -> 更新并上浮;
     * 3. ignore:已经弹出,或新距离不更优 -> 忽略。
     */
    public static class NodeHeap {
        private Node[] nodes;
        private final Map<Node, Integer> indexMap;
        private final Map<Node, Integer> distanceMap;
        private int size;

        public NodeHeap(int initialCapacity) {
            nodes = new Node[Math.max(1, initialCapacity)];
            indexMap = new HashMap<>();
            distanceMap = new HashMap<>();
        }

        public void addOrUpdateOrIgnore(Node node, int distance) {
            if (!hasEntered(node)) {
                ensureCapacity(size + 1);

                nodes[size] = node;
                indexMap.put(node, size);
                distanceMap.put(node, distance);
                heapInsert(size);
                size++;
                return;
            }

            if (inHeap(node)
                    && distance < distanceMap.get(node)) {
                distanceMap.put(node, distance);
                heapInsert(indexMap.get(node));
            }

            // 已经弹出,或新距离没有更短:忽略。
        }

        public Record pop() {
            if (isEmpty()) {
                throw new IllegalStateException("Heap is empty.");
            }

            Node minNode = nodes[0];
            int minDistance = distanceMap.get(minNode);

            swap(0, size - 1);

            Node removed = nodes[size - 1];
            indexMap.put(removed, -1);
            distanceMap.remove(removed);
            nodes[size - 1] = null;

            size--;
            if (size > 0) {
                heapify(0);
            }

            return new Record(minNode, minDistance);
        }

        private void heapInsert(int index) {
            while (index > 0) {
                int parent = (index - 1) / 2;

                if (distanceMap.get(nodes[index])
                        >= distanceMap.get(nodes[parent])) {
                    break;
                }

                swap(index, parent);
                index = parent;
            }
        }

        private void heapify(int index) {
            int left = index * 2 + 1;

            while (left < size) {
                int right = left + 1;
                int smallest =
                        right < size
                                && distanceMap.get(nodes[right])
                                < distanceMap.get(nodes[left])
                                ? right
                                : left;

                if (distanceMap.get(nodes[index])
                        <= distanceMap.get(nodes[smallest])) {
                    break;
                }

                swap(index, smallest);
                index = smallest;
                left = index * 2 + 1;
            }
        }

        private void swap(int i, int j) {
            if (i == j) {
                return;
            }

            Node tmp = nodes[i];
            nodes[i] = nodes[j];
            nodes[j] = tmp;

            indexMap.put(nodes[i], i);
            indexMap.put(nodes[j], j);
        }

        private boolean hasEntered(Node node) {
            return indexMap.containsKey(node);
        }

        private boolean inHeap(Node node) {
            return hasEntered(node) && indexMap.get(node) >= 0;
        }

        public boolean isEmpty() {
            return size == 0;
        }

        private void ensureCapacity(int required) {
            if (required <= nodes.length) {
                return;
            }

            int newCapacity =
                    Math.max(required, nodes.length * 2);
            nodes = Arrays.copyOf(nodes, newCapacity);
        }
    }

    public static class Record {
        public final Node node;
        public final int distance;

        public Record(Node node, int distance) {
            this.node = node;
            this.distance = distance;
        }
    }
}

为什么 Dijkstra 不能有负权边?

假设某个节点 A 已经以距离 5 被确定。

Dijkstra 的逻辑依赖:

text 复制代码
未来从更远节点绕回来,
只会让距离更大,不会让 5 变小。

如果允许负权边:

text 复制代码
更远节点 -> A 的边权 = -10

就可能在 A 已经"确定"以后,突然出现更短路径。

这会破坏贪心正确性的基础。

复杂度

朴素版
  • 时间:O(V² + E)
  • 空间:O(V)
改写堆版
  • 每个节点入堆 / 出堆;
  • 每次有效距离下降触发上浮;
  • 时间复杂度:O((V + E) log V)
  • 额外空间:O(V)

易错点

  • 忘记加上 dist[current],只记录当前边权;
  • 在负权图上使用 Dijkstra;
  • 把"被发现"误认为"最短距离已经确定";
  • 自定义堆更新距离后忘记上浮;
  • 节点弹出后没有标记为"已确定",导致再次进入堆;
  • int 距离相加可能溢出,因此代码中用 long 做中间计算并主动检查。

第三部分:把 6 道题串起来理解

1. "候选集合"是图算法的核心

算法 候选集合 每次取谁 如何防止错误
BFS 队列 最早发现的节点 visited
DFS 最近发现的节点 visited
拓扑排序 零入度队列 当前无依赖节点 剩余入度
Kruskal 全图候选边 全局最小边 并查集防环
Prim 当前边界候选边 边界最小边 visited 防环
Dijkstra 候选距离 当前距离最小节点 selected / 出堆确定

#mermaid-svg-yg6aFkjXOaqPGPHS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yg6aFkjXOaqPGPHS .error-icon{fill:#552222;}#mermaid-svg-yg6aFkjXOaqPGPHS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yg6aFkjXOaqPGPHS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yg6aFkjXOaqPGPHS .marker.cross{stroke:#333333;}#mermaid-svg-yg6aFkjXOaqPGPHS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yg6aFkjXOaqPGPHS p{margin:0;}#mermaid-svg-yg6aFkjXOaqPGPHS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster-label text{fill:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster-label span{color:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster-label span p{background-color:transparent;}#mermaid-svg-yg6aFkjXOaqPGPHS .label text,#mermaid-svg-yg6aFkjXOaqPGPHS span{fill:#333;color:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS .node rect,#mermaid-svg-yg6aFkjXOaqPGPHS .node circle,#mermaid-svg-yg6aFkjXOaqPGPHS .node ellipse,#mermaid-svg-yg6aFkjXOaqPGPHS .node polygon,#mermaid-svg-yg6aFkjXOaqPGPHS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yg6aFkjXOaqPGPHS .rough-node .label text,#mermaid-svg-yg6aFkjXOaqPGPHS .node .label text,#mermaid-svg-yg6aFkjXOaqPGPHS .image-shape .label,#mermaid-svg-yg6aFkjXOaqPGPHS .icon-shape .label{text-anchor:middle;}#mermaid-svg-yg6aFkjXOaqPGPHS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yg6aFkjXOaqPGPHS .rough-node .label,#mermaid-svg-yg6aFkjXOaqPGPHS .node .label,#mermaid-svg-yg6aFkjXOaqPGPHS .image-shape .label,#mermaid-svg-yg6aFkjXOaqPGPHS .icon-shape .label{text-align:center;}#mermaid-svg-yg6aFkjXOaqPGPHS .node.clickable{cursor:pointer;}#mermaid-svg-yg6aFkjXOaqPGPHS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yg6aFkjXOaqPGPHS .arrowheadPath{fill:#333333;}#mermaid-svg-yg6aFkjXOaqPGPHS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yg6aFkjXOaqPGPHS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yg6aFkjXOaqPGPHS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yg6aFkjXOaqPGPHS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yg6aFkjXOaqPGPHS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yg6aFkjXOaqPGPHS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster text{fill:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster span{color:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yg6aFkjXOaqPGPHS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS rect.text{fill:none;stroke-width:0;}#mermaid-svg-yg6aFkjXOaqPGPHS .icon-shape,#mermaid-svg-yg6aFkjXOaqPGPHS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yg6aFkjXOaqPGPHS .icon-shape p,#mermaid-svg-yg6aFkjXOaqPGPHS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yg6aFkjXOaqPGPHS .icon-shape .label rect,#mermaid-svg-yg6aFkjXOaqPGPHS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yg6aFkjXOaqPGPHS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yg6aFkjXOaqPGPHS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yg6aFkjXOaqPGPHS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} BFS DFS
控制访问顺序
拓扑排序
控制依赖消除顺序
Kruskal Prim
控制安全最小边选择
Dijkstra
控制最短距离确定顺序

2. visited 的语义并不总一样

BFS / DFS

text 复制代码
visited = 已经发现,不要重复加入容器

Prim

text 复制代码
visited = 已经进入生成树

Dijkstra

朴素版中的 selected

text 复制代码
selected = 最短距离已经最终确定

所以看到一个 Set<Node> 时,不要机械理解成"访问过"。

先问:

这个集合到底承诺了什么不变量?


3. 三种贪心为什么都能"选当前最小"?

Kruskal、Prim、Dijkstra 都在选某种"当前最小",但对象不同:

text 复制代码
Kruskal:全局剩余安全边里的最小边
Prim:当前生成树切分边界上的最小边
Dijkstra:当前未确定节点中的最短距离

它们看起来相似,但正确性依据不同。

不要把模板记成:

text 复制代码
"小根堆 = 贪心"

而应该记成:

text 复制代码
先证明"当前最小可以安全地被确定"
再决定用什么数据结构高效找到它

第四部分:统一图结构

今天使用的三个基础类可以整理为:

Node.java

java 复制代码
package class10;

import java.util.ArrayList;
import java.util.List;

/**
 * 图中的点。
 */
public class Node {
    public int value;          // 节点标识
    public int in;             // 入度
    public int out;            // 出度
    public List<Node> nexts;   // 从当前点可直接到达的节点
    public List<Edge> edges;   // 从当前点出发的边

    public Node(int value) {
        this.value = value;
        this.in = 0;
        this.out = 0;
        this.nexts = new ArrayList<>();
        this.edges = new ArrayList<>();
    }
}

Edge.java

java 复制代码
package class10;

/**
 * 图中的边。
 */
public class Edge {
    public Node from;
    public Node to;
    public int weight;

    public Edge(Node from, Node to, int weight) {
        this.from = from;
        this.to = to;
        this.weight = weight;
    }
}

Graph.java

java 复制代码
package class10;

import java.util.HashMap;
import java.util.HashSet;

/**
 * 图结构。
 */
public class Graph {
    public HashMap<Integer, Node> nodes; // 点集
    public HashSet<Edge> edges;          // 边集

    public Graph() {
        nodes = new HashMap<>();
        edges = new HashSet<>();
    }
}

结构关系图

#mermaid-svg-96QQALv0qutFQtA8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-96QQALv0qutFQtA8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-96QQALv0qutFQtA8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-96QQALv0qutFQtA8 .error-icon{fill:#552222;}#mermaid-svg-96QQALv0qutFQtA8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-96QQALv0qutFQtA8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-96QQALv0qutFQtA8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-96QQALv0qutFQtA8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-96QQALv0qutFQtA8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-96QQALv0qutFQtA8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-96QQALv0qutFQtA8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-96QQALv0qutFQtA8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-96QQALv0qutFQtA8 .marker.cross{stroke:#333333;}#mermaid-svg-96QQALv0qutFQtA8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-96QQALv0qutFQtA8 p{margin:0;}#mermaid-svg-96QQALv0qutFQtA8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-96QQALv0qutFQtA8 .cluster-label text{fill:#333;}#mermaid-svg-96QQALv0qutFQtA8 .cluster-label span{color:#333;}#mermaid-svg-96QQALv0qutFQtA8 .cluster-label span p{background-color:transparent;}#mermaid-svg-96QQALv0qutFQtA8 .label text,#mermaid-svg-96QQALv0qutFQtA8 span{fill:#333;color:#333;}#mermaid-svg-96QQALv0qutFQtA8 .node rect,#mermaid-svg-96QQALv0qutFQtA8 .node circle,#mermaid-svg-96QQALv0qutFQtA8 .node ellipse,#mermaid-svg-96QQALv0qutFQtA8 .node polygon,#mermaid-svg-96QQALv0qutFQtA8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-96QQALv0qutFQtA8 .rough-node .label text,#mermaid-svg-96QQALv0qutFQtA8 .node .label text,#mermaid-svg-96QQALv0qutFQtA8 .image-shape .label,#mermaid-svg-96QQALv0qutFQtA8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-96QQALv0qutFQtA8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-96QQALv0qutFQtA8 .rough-node .label,#mermaid-svg-96QQALv0qutFQtA8 .node .label,#mermaid-svg-96QQALv0qutFQtA8 .image-shape .label,#mermaid-svg-96QQALv0qutFQtA8 .icon-shape .label{text-align:center;}#mermaid-svg-96QQALv0qutFQtA8 .node.clickable{cursor:pointer;}#mermaid-svg-96QQALv0qutFQtA8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-96QQALv0qutFQtA8 .arrowheadPath{fill:#333333;}#mermaid-svg-96QQALv0qutFQtA8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-96QQALv0qutFQtA8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-96QQALv0qutFQtA8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-96QQALv0qutFQtA8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-96QQALv0qutFQtA8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-96QQALv0qutFQtA8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-96QQALv0qutFQtA8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-96QQALv0qutFQtA8 .cluster text{fill:#333;}#mermaid-svg-96QQALv0qutFQtA8 .cluster span{color:#333;}#mermaid-svg-96QQALv0qutFQtA8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-96QQALv0qutFQtA8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-96QQALv0qutFQtA8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-96QQALv0qutFQtA8 .icon-shape,#mermaid-svg-96QQALv0qutFQtA8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-96QQALv0qutFQtA8 .icon-shape p,#mermaid-svg-96QQALv0qutFQtA8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-96QQALv0qutFQtA8 .icon-shape .label rect,#mermaid-svg-96QQALv0qutFQtA8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-96QQALv0qutFQtA8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-96QQALv0qutFQtA8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-96QQALv0qutFQtA8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Graph
Node 集合
Edge 集合
nexts 邻接点
edges 出边
from
to
weight


第五部分:建议测试用例

图算法特别适合用"小图手推 + 边界图"测试。

BFS / DFS

text 复制代码
1. root == null
2. 单节点
3. 一条链
4. 一个节点指向多个邻居
5. 多个节点共同指向同一个节点
6. 存在自环
7. 存在大环:1 -> 2 -> 3 -> 1
8. 图不连通:确认只遍历 root 可达部分

拓扑排序

text 复制代码
1. 空图
2. 单节点
3. 一条依赖链
4. 多个零入度起点
5. 一个节点依赖多个前驱
6. DAG 有多种合法拓扑序
7. 2 节点环:1 -> 2 -> 1
8. 自环:1 -> 1

Kruskal / Prim

text 复制代码
1. 单节点
2. 两点一边
3. 三角形:检查是否跳过成环边
4. 多条相同权重边
5. 连通图
6. 不连通图:检查最小生成森林
7. 无向边使用双向 Edge 表示
8. 含负权边:MST 算法本身可以处理负权

Dijkstra

text 复制代码
1. 单节点
2. 只有一条边
3. 同一节点存在多条候选路径
4. 后发现的路径更短,需要松弛更新
5. 存在不可达节点
6. 边权为 0
7. 出现负权边:应拒绝使用 Dijkstra
8. 距离累加接近 Integer.MAX_VALUE

今日自测

1. BFS 为什么推荐在"入队时"就标记 visited?

查看答案

因为一个节点可能被多个前驱同时发现。第一次发现时立即标记,可以保证它只进入队列一次,避免重复入队和重复处理。

2. DFS 为什么经常倒序压栈邻居?

查看答案

栈是后进先出。若希望邻接表中靠前的节点先被弹出,就需要让靠后的节点先入栈,因此通常从 nexts 末尾向前压栈。

3. 拓扑排序为什么能用结果长度检测环?

查看答案

有向环中的节点无法通过删除环外节点把剩余入度全部降为 0,因此不会进入零入度队列。若最终结果节点数少于总节点数,就说明还有节点无法被消除,图中存在环。

4. Kruskal 为什么需要并查集?

查看答案

每次尝试加入最小边时,需要快速判断两个端点是否已经连通。若已连通,再加入该边必然形成环;并查集正好高效维护这种动态连通性。

5. Prim 为什么不需要并查集?

查看答案

Prim 始终维护"已经进入生成树的节点集合"。候选边若指向已访问节点就丢弃,指向新节点才接纳,因此 visited 已经足够完成防环判断。

6. Dijkstra 中"发现一个节点"和"确定一个节点"有什么区别?

查看答案

发现只表示目前存在一条到达它的路径,因此得到的是候选距离;当它成为所有未确定节点中距离最小者并被选中或从最小堆弹出后,才表示最短距离最终确定。

7. Dijkstra 第一次发现邻居时为什么不能只保存边权?

查看答案

边权只表示 current 到 next 的局部代价。起点到 next 的完整候选距离必须是 distcurrent + edge.weight。

8. Kruskal、Prim、Dijkstra 的"最小"分别是什么?

查看答案

Kruskal 选全局剩余安全边中的最小边;Prim 选当前生成树边界上的最小边;Dijkstra 选当前未确定节点中候选距离最小的节点。


加餐挑战

  1. 把 BFS 改成返回 List<Node>,不要直接打印;
  2. 为 BFS 增加 distance,返回起点到每个节点的无权最短距离;
  3. 用递归方式实现 DFS,并比较显式栈与递归栈;
  4. 拓扑排序除了返回顺序,再返回一个 hasCycle 状态;
  5. 用优先队列重写 Dijkstra,但允许重复 Record 入堆,通过"过期记录检查"避免自定义 decrease-key;
  6. 比较 O(E log E) 的 Kruskal 与堆版 Prim 在稀疏图、稠密图中的表现;
  7. 思考:如果图中存在负权边但没有负环,应该选择什么最短路算法?
  8. 尝试实现 Bellman-Ford,并与 Dijkstra 的"确定节点"思想做对比。

挑战 5 提示:允许重复候选

普通 PriorityQueue 不支持 decrease-key 时,可以直接把更短的新 Record 再压入堆。弹出时如果该 Record 的距离已经不是当前最佳距离,就把它视为过期记录并跳过。


Day 18 复盘清单

  • 我能写出 Queue + visited 的图 BFS
  • 我知道 BFS 中节点为什么只应该输出一次
  • 我能用显式栈写 DFS,并解释为什么倒序压栈
  • 我能写出 Kahn 拓扑排序,并检测有向环
  • 我知道拓扑排序为什么不应该直接修改原始入度
  • 我能解释 Kruskal 中并查集的作用
  • 我能写出路径压缩与按大小合并
  • 我能解释 Prim 的"点解锁边"过程
  • 我能说清 Kruskal 与 Prim 的候选集合差异
  • 我能写出 Dijkstra 的松弛公式
  • 我知道第一次发现邻居时必须加上当前节点距离
  • 我能解释 Dijkstra 为什么不能处理负权边
  • 我能理解改写堆的 add / update / ignore
  • 我能说清 BFS、DFS、MST、最短路中 visited 的不同语义

一句话总结: Day 18 真正要建立的是"图上的候选状态"意识:BFS 用队列控制扩散顺序,DFS 用栈控制深入顺序,拓扑排序用入度控制依赖消除,Kruskal 与 Prim 用安全的最小边扩张结构,而 Dijkstra 用最小候选距离逐步确定最短路。

相关推荐
用户8132679332538 分钟前
Python 计算盘中 VWAP:为什么 1 分钟 K 线是量化工程中的高性价比选择
后端·算法·github
高频因子挖掘机42 分钟前
Python批量获取1000只ETF 5分钟K线:从分页到吞吐量优化的QuantDash实践
后端·算法·github
额额额对了1 小时前
Linux C 多线程编程基础:从内存模型到生命周期管理
linux·开发语言·算法
wabs6662 小时前
关于二叉树【力扣144.二叉树的前序遍历的思考】
数据结构·c++·算法·leetcode·二叉树
郝学胜-神的一滴2 小时前
C++20 高级编程 004:从初始化、内存到const系列关键字
开发语言·算法·编程·软件构建·c++20
kyle~2 小时前
点云配准--- 迭代最近点 ICP 求解
线性代数·算法·机器学习
qq7422349842 小时前
Gradio 极简入门:三分钟为AI模型打造交互界面,并对比Streamlit与Dash如何选型
人工智能·算法·大模型·交互·dash
1000世界小札8 小时前
《大话数据结构》第9章精读:归并排序与快速排序完整 C++ 实现
数据结构·c++·算法
2601_9561219711 小时前
背包基础篇(01、完全、分组、多重、混合)
c++·算法·动态规划