1. 拓扑排序算法
拓扑排序
适用于有向无环图(DAG),常用于确定任务的执行顺序。
流程:
- 统计所有节点的入度
- 将入度为0的节点加入队列
- 取出队列中的节点,将其邻居节点的入度减1
- 如果邻居节点入度变为0,加入队列
- 重复步骤3-4直到队列为空
// 图的结构
public class Graph {
public HashMap<Integer, Node> nodes; // 点集
public HashSet<Edge> edges; // 边集
public Graph() {
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
// 图中的节点结构
public class Node {
public int value; // 节点值
public int in; // 入度
public int out; // 出度
public ArrayList<Node> nexts; // 邻接点
public ArrayList<Edge> edges; // 相连的边
public Node(int value) {
this.value = value;
in = 0;
out = 0;
nexts = new ArrayList<>();
edges = new ArrayList<>();
}
}
// 边的结构
public class Edge {
public int weight; // 权重
public Node from; // 起点
public Node to; // 终点
public Edge(int weight, Node from, Node to) {
this.weight = weight;
this.from = from;
this.to = to;
}
}
public List<Node> topologicalSort(Graph graph) {
HashMap<Node, Integer> inMap = new HashMap<>(); // 存储节点的入度
Queue<Node> zeroInQueue = new LinkedList<>(); // 入度为0的节点队列
List<Node> result = new ArrayList<>();
// 统计入度
for (Node node : graph.nodes.values()) {
inMap.put(node, node.in);
if (node.in == 0) {
zeroInQueue.offer(node);
}
}
while (!zeroInQueue.isEmpty()) {
Node cur = zeroInQueue.poll();
result.add(cur);
// 遍历当前节点的邻居,减少入度
for (Node next : cur.nexts) {
inMap.put(next, inMap.get(next) - 1);
if (inMap.get(next) == 0) {
zeroInQueue.offer(next);
}
}
}
return result;
}
/**
* 图结构定义
*/
public class Graph {
public HashMap<Integer, Node> nodes; // 存储所有节点
public HashSet<Edge> edges; // 存储所有边
public Graph() {
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
/**
* 节点定义
*/
public class Node {
public int value; // 节点值
public int in; // 入度
public int out; // 出度
public ArrayList<Node> nexts; // 邻接节点
public ArrayList<Edge> edges; // 相关边
public Node(int value) {
this.value = value;
in = 0;
out = 0;
nexts = new ArrayList<>();
edges = new ArrayList<>();
}
}
/**
* 边的定义
*/
public class Edge {
public int weight; // 权重
public Node from; // 起始节点
public Node to; // 目标节点
public Edge(int weight, Node from, Node to) {
this.weight = weight;
this.from = from;
this.to = to;
}
}
代码详细讲解
让我用图解方式详细讲解拓扑排序的工作原理。
1.拓扑排序的概念
拓扑排序是将有向无环图(DAG)的所有节点排成一个线性序列,使得图中任意一对顶点u和v,若存在一条从u到v的路径,则u在序列中出现在v之前。
让我用具体的例子来解释 u 和 v 的含义。
简单解释
- u:表示起始节点(前置节点)
- v:表示终止节点(后置节点)
具体例子
假设我们有一个表示"穿衣服"的流程:
graph LR
内衣((内衣/u)) --> 外套((外套/v))
内衣 --> 裤子((裤子))
袜子((袜子/u)) --> 鞋子((鞋子/v))
在这个例子中:
-
内衣(u) → 外套(v)
- 必须先穿内衣,再穿外套
- 所以内衣在序列中必须出现在外套之前
-
袜子(u) → 鞋子(v)
- 必须先穿袜子,再穿鞋子
- 所以袜子在序列中必须出现在鞋子之前
再举个例子:课程依赖
graph LR
高数1((高数1/u)) --> 高数2((高数2/v))
高数2((高数2/u)) --> 高数3((高数3/v))
在这个例子中:
- 高数1(u) → 高数2(v):必须先学高数1,才能学高数2
- 高数2(u) → 高数3(v):必须先学高数2,才能学高数3
所以最终的学习顺序必须是:
高数1 → 高数2 → 高数3
生活中的例子
想象做一道菜:
graph LR
洗菜((洗菜/u)) --> 切菜((切菜/v))
切菜((切菜/u)) --> 炒菜((炒菜/v))
- 洗菜(u) → 切菜(v):必须先洗菜,才能切菜
- 切菜(u) → 炒菜(v):必须先切菜,才能炒菜
所以正确的顺序必须是:
洗菜 → 切菜 → 炒菜
总结
- u 和 v 就是用来表示两个有依赖关系的事物
- u 必须在 v 之前完成
- 在拓扑排序的结果中,u 一定出现在 v 的前面
- 这就像是生活中的"必须先做什么,才能做什么"的关系
所以当我们说"u在序列中出现在v之前",就是在说:
- 如果事物u是事物v的前提条件
- 那么在最终的排序结果中
- u必须排在v的前面
2.示例图
graph LR
A((A/0)) --> B((B/1))
A --> C((C/1))
B --> D((D/2))
C --> D
style A fill:#98FB98,stroke:#333,stroke-width:4px
括##号中的数字表示入度(指向该节点的边的数量)
3.代码执行流程
- 初始化阶段
HashMap<Node, Integer> inMap = new HashMap<>(); // 存储每个节点的入度
Queue<Node> zeroInQueue = new LinkedList<>(); // 存储入度为0的节点
List<Node> result = new ArrayList<>(); // 存储排序结果
初始状态:
inMap = {A:0, B:1, C:1, D:2}
zeroInQueue = [A]
result = []
- 处理入度为0的节点A
graph LR
A((A/0)) --> B((B/1))
A --> C((C/1))
B --> D((D/2))
C --> D
style A fill:#FF9999,stroke:#333,stroke-width:4px
style B fill:#98FB98,stroke:#333,stroke-width:4px
style C fill:#98FB98,stroke:#333,stroke-width:4px
result = [A]
// 删除A的出边后,更新B和C的入度
inMap = {A:0, B:0, C:0, D:2}
zeroInQueue = [B, C]
- 处理入度为0的节点B
graph LR
B((B/0)) --> D((D/1))
C((C/0)) --> D
style B fill:#FF9999,stroke:#333,stroke-width:4px
style C fill:#98FB98,stroke:#333,stroke-width:4px
result = [A, B]
// 删除B的出边后,更新D的入度
inMap = {A:0, B:0, C:0, D:1}
zeroInQueue = [C]
- 处理入度为0的节点C
graph LR
D((D/0))
style D fill:#98FB98,stroke:#333,stroke-width:4px
result = [A, B, C]
// 删除C的出边后,更新D的入度
inMap = {A:0, B:0, C:0, D:0}
zeroInQueue = [D]
- 处理入度为0的节点D
result = [A, B, C, D]
inMap = {A:0, B:0, C:0, D:0}
zeroInQueue = [] // 队列为空,算法结束
4.流程图
graph TD
A[开始] --> B[初始化入度Map和队列]
B --> C[统计所有节点入度]
C --> D[将入度为0的节点加入队列]
D --> E{队列是否为空?}
E -->|否| F[取出队首节点加入结果]
F --> G[更新其邻居节点的入度]
G --> H[将新的入度为0的节点加入队列]
H --> E
E -->|是| I[返回结果]
I --> J[结束]
5.关键点解释
- 入度的概念
入度:指向该节点的边的数量
- 入度为0意味着没有依赖
- 适合作为序列的起始点
- HashMap的作用
HashMap<Node, Integer> inMap
- 记录每个节点的实时入度
- 方便快速更新和查询
- 队列的作用
Queue<Node> zeroInQueue
- 存储所有入度为0的节点
- 保证依赖关系的正确处理顺序
6.实际应用场景
- 课程安排
课程A -> 课程B(表示A是B的先修课)
拓扑排序可以得到合理的学习顺序
- 项目管理
任务A -> 任务B(表示A必须在B之前完成)
拓扑排序可以得到合理的任务执行顺序
- 软件编译
模块A -> 模块B(表示B依赖A)
拓扑排序可以得到正确的编译顺序
这个算法的核心思想就是:不断找出没有依赖(入度为0)的节点,把它加入结果序列,然后删除它的出边,重复这个过程直到所有节点都被处理完。
2. 最短路径算法
Dijkstra算法
用于计算一个节点到其他所有节点的最短路径(要求边的权重为非负数)。
distanceMap:存储从起点到每个节点的当前最短距离 selectedNodes:存储已经确定了最短路径的节点 getMinDistanceAndUnselectedNode:在未确定节点中找距离最小的
一个点被"确认"意味着:
我们已经找到了从起点到这个点的最短路径
public HashMap<Node, Integer> dijkstra(Node head) {
// 第1步:创建记录最短距离的表
HashMap<Node, Integer> distanceMap = new HashMap<>();
// 起点到自己的距离为0
distanceMap.put(head, 0);
// 第2步:创建已确定最短路径的节点集合
HashSet<Node> selectedNodes = new HashSet<>();
// 第3步:选择一个未确定的最小距离节点
Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
// 第4步:主循环,直到没有新的最小距离节点
while (minNode != null) {
// 获取当前节点的距离
int distance = distanceMap.get(minNode);
// 第5步:检查当前节点的所有邻边
for (Edge edge : minNode.edges) {
Node toNode = edge.to; // 邻居节点
// 第6步:更新邻居的距离
if (!distanceMap.containsKey(toNode)) {
// 第一次发现这个邻居
distanceMap.put(toNode, distance + edge.weight);
} else {
// 已经有一条到达该邻居的路径,比较取最小值
distanceMap.put(toNode,
Math.min(distanceMap.get(toNode), distance + edge.weight));
}
}
// 第7步:标记当前节点为已确定
selectedNodes.add(minNode);
// 第8步:选择下一个最小距离节点
minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
}
return distanceMap; // 返回所有节点的最短距离
}
🔍 辅助方法的实现:
private Node getMinDistanceAndUnselectedNode(
HashMap<Node, Integer> distanceMap,
HashSet<Node> selectedNodes) {
Node minNode = null;
int minDistance = Integer.MAX_VALUE;
// 遍历所有已知距离的节点
for (Entry<Node, Integer> entry : distanceMap.entrySet()) {
Node node = entry.getKey();
int distance = entry.getValue();
// 如果节点未被选中,且距离更小
if (!selectedNodes.contains(node) && distance < minDistance) {
minNode = node;
minDistance = distance;
}
}
return minNode;
}
📝 详细步骤解释
1. 初始化阶段
HashMap<Node, Integer> distanceMap = new HashMap<>();
distanceMap.put(head, 0);
就像:
创建一个配送距离表
记录:从餐厅到餐厅自己的距离是0分钟
2. 创建已访问集合
HashSet<Node> selectedNodes = new HashSet<>();
就像:
创建一个已送达地点的清单
开始时是空的
3. 获取最小距离节点
Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
就像:
在未送达的地点中
找出距离最近的那个
4. 主循环
while (minNode != null) {
就像:
只要还有未送达的地点
就继续送外卖
5. 处理当前节点
int distance = distanceMap.get(minNode);
for (Edge edge : minNode.edges) {
就像:
到达当前地点后
看看从这里到其他地点的路线
6. 更新邻居距离
if (!distanceMap.containsKey(toNode)) {
distanceMap.put(toNode, distance + edge.weight);
} else {
distanceMap.put(toNode,
Math.min(distanceMap.get(toNode), distance + edge.weight));
}
就像:
如果是第一次知道这个地点:
记录当前的总路程
否则:
比较已知路线和新路线,保留更短的
7. 标记已访问
selectedNodes.add(minNode);
就像:
在送达清单上打勾
表示这个地点已经确定了最短路径
8. 选择下一个节点
minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
就像:
继续找下一个最近的未送达地点
🎯 辅助方法的工作原理
private Node getMinDistanceAndUnselectedNode(...) {
就像配送系统在:
- 查看所有已知距离的地点
- 排除已送达的地点
- 找出最近的一个
在Dijkstra最短路径或者Prim最小生成树算法中,
distances实际上存储的是从当前已选择的节点集合到其他节点的最短距离。
让我用更准确的例子说明:
class GraphExample {
static class City {
String name;
Map<City, Integer> neighbors; // 邻接表,存储相邻城市和距离
public City(String name) {
this.name = name;
this.neighbors = new HashMap<>();
}
}
public static void main(String[] args) {
// 1. 创建城市
City beijing = new City("北京");
City shanghai = new City("上海");
City guangzhou = new City("广州");
City shenzhen = new City("深圳");
// 2. 添加边(无向图)
beijing.neighbors.put(shanghai, 1200); // 北京-上海 1200km
beijing.neighbors.put(guangzhou, 2000); // 北京-广州 2000km
shanghai.neighbors.put(guangzhou, 1300); // 上海-广州 1300km
guangzhou.neighbors.put(shenzhen, 140); // 广州-深圳 140km
// 3. 初始化距离表(从起点北京开始)
HashMap<City, Integer> distances = new HashMap<>();
distances.put(beijing, 0); // 起点到自己距离为0
distances.put(shanghai, 1200); // 起点到上海初始距离
distances.put(guangzhou, 2000); // 起点到广州初始距离
distances.put(shenzhen, Integer.MAX_VALUE); // 起点到深圳初始距离为无穷大
// 4. 已访问节点集合
HashSet<City> visited = new HashSet<>();
visited.add(beijing); // 将起点加入已访问集合
// 5. 查找下一个最近的未访问城市
City nearest = getMinDistanceAndUnselectedNode(distances, visited);
System.out.println("下一个最近的城市是:" + nearest.name);
// 输出:下一个最近的城市是:上海
}
private static City getMinDistanceAndUnselectedNode(
HashMap<City, Integer> distanceMap,
HashSet<City> selectedNodes) {
City minNode = null;
int minDistance = Integer.MAX_VALUE;
for (Entry<City, Integer> entry : distanceMap.entrySet()) {
City city = entry.getKey();
int distance = entry.getValue();
if (!selectedNodes.contains(city) && distance < minDistance) {
minNode = city;
minDistance = distance;
}
}
return minNode;
}
}
- 每个城市都知道它和哪些城市相连,以及相连的距离(通过
neighbors存储) distances存储的是从已选择的节点集合到其他节点的当前最短距离- 初始时:
- 起点到自己距离为0
- 起点到直接相连的城市距离为边的权重
- 起点到未直接相连的城市距离为无穷大
- 每次选择未访问节点中距离最小的,这个距离是从已选择节点集合到该节点的最短距离
就像是这样一个过程:
- 你站在北京(起点)
- 你知道:
- 直接到上海要1200km
- 直接到广州要2000km
- 到深圳还不知道怎么走(初始为无穷大)
- 这个方法会告诉你:"在还没去过的城市中,上海是最近的(1200km)"
这样解释是不是更清楚了?
📊 示例执行过程
第1轮:
- distanceMap = {A:0}
- 选择A处理
- 更新B和C的距离
第2轮:
- distanceMap = {A:0, B:2, C:4}
- 选择B处理
- 更新D的距离
第3轮:
- distanceMap = {A:0, B:2, C:4, D:5}
- 选择C处理
- 检查是否有更短路径
第4轮:
- 选择D处理
- 没有新的节点了,结束
这就像送外卖时:
- 从餐厅出发
- 每次都选最近的地点送
- 送到一个地点后看看能否找到更短的路
- 直到所有订单都送完
代码详解
让我用图解方式详细讲解 Dijkstra(迪杰斯特拉)算法的工作原理。
Dijkstra算法的目的
计算从一个源点到其他所有点的最短路径。
示例图
假设我们有这样一个带权图:
graph LR
A((A)) --2--> B((B))
A --4--> C((C))
B --2--> C
B --3--> D((D))
C --2--> D
算法执行流程
- 初始化阶段
HashMap<Node, Integer> distanceMap = new HashMap<>();
distanceMap.put(head, 0);
HashSet<Node> selectedNodes = new HashSet<>();
初始状态:
distanceMap = {A:0} // 只知道起点A到自己的距离为0
selectedNodes = {} // 还没有确定任何最短路径
- 第一轮:处理节点A
graph LR
A((A/0)) --2--> B((B/2))
A --4--> C((C/4))
B --2--> C
B --3--> D((D/5))
C --2--> D
style A fill:#FF9999,stroke:#333,stroke-width:4px
distanceMap = {A:0, B:2, C:4}
selectedNodes = {A}
- 第二轮:处理节点B
graph LR
A((A/0)) --2--> B((B/2))
A --4--> C((C/4))
B --2--> C
B --3--> D((D/5))
C --2--> D
style A fill:#FF9999,stroke:#333,stroke-width:4px
style B fill:#FF9999,stroke:#333,stroke-width:4px
distanceMap = {A:0, B:2, C:4, D:5}
// 通过B更新到C的距离:min(4, 2+2)=4
// 通过B更新到D的距离:2+3=5
selectedNodes = {A, B}
- 第三轮:处理节点C
graph LR
A((A/0)) --2--> B((B/2))
A --4--> C((C/4))
B --2--> C
B --3--> D((D/5))
C --2--> D
style A fill:#FF9999,stroke:#333,stroke-width:4px
style B fill:#FF9999,stroke:#333,stroke-width:4px
style C fill:#FF9999,stroke:#333,stroke-width:4px
distanceMap = {A:0, B:2, C:4, D:5}
// 通过C更新到D的距离:min(5, 4+2)=5
selectedNodes = {A, B, C}
关键代码解析
- 获取最小距离的未访问节点
Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
- 从未确定最短路径的节点中选择距离最小的
- 更新相邻节点的距离
if (!distanceMap.containsKey(toNode)) {
distanceMap.put(toNode, distance + edge.weight);
} else {
distanceMap.put(toNode,
Math.min(distanceMap.get(toNode), distance + edge.weight));
}
- 如果是第一次发现这个节点,直接更新距离
- 如果已经有距离,取最小值
算法流程图
graph TD
A[开始] --> B[初始化距离Map和已选择集合]
B --> C[将起点距离设为0]
C --> D[获取未访问的最小距离节点]
D --> E{节点是否存在?}
E -->|是| F[更新该节点的邻居距离]
F --> G[将节点加入已选择集合]
G --> D
E -->|否| H[返回距离Map]
H --> I[结束]
实际应用场景
- 导航软件
计算从当前位置到目的地的最短路径
- 网络路由
数据包寻找最优传输路径
- 社交网络
计算用户之间的最短关系链
算法特点
- 优点
- 能找到单源最短路径
- 适用于非负权重的图
- 实现相对简单
- 缺点
- 不适用于负权重
- 时间复杂度较高 O(V²)
- 需要存储所有节点信息
关键要点
- 贪心策略
- 每次选择当前最短距离的节点
- 一旦选择就确定了最短路径
- 松弛操作
- 通过已知节点更新其邻居的距离
- 始终保持最小距离值
- 终止条件
- 所有可达节点都被访问
- 或者没有更小的距离可以更新
这个算法就像是在一个城市中找路,从起点开始,每次都选择当前最近的未访问地点,然后看看通过这个地点是否能找到到其他地点的更短路径。
整个流程图总结
让我用流程图来总结 Dijkstra 算法的完整过程:
graph TD
A[开始] --> B[初始化]
B --> C[创建distanceMap和selectedNodes]
C --> D[将起点距离设为0]
D --> E[获取未访问的最小距离节点]
E --> F{节点存在?}
F -->|是| G[获取当前节点距离]
F -->|否| H[结束]
G --> I[遍历当前节点的所有邻边]
I --> J{邻居节点是否<br>首次发现?}
J -->|是| K[记录到邻居的<br>新距离]
J -->|否| L[比较并更新<br>最小距离]
K --> M[标记当前节点<br>为已访问]
L --> M
M --> E
style A fill:#f96,stroke:#333,stroke-width:2px
style H fill:#f96,stroke:#333,stroke-width:2px
🔄 主要阶段说明
- 初始化阶段
graph TD
A[创建数据结构] --> B[distanceMap]
A --> C[selectedNodes]
B --> D[起点距离设为0]
C --> E[已访问集合为空]
style A fill:#f96,stroke:#333,stroke-width:2px
- 节点处理阶段
graph TD
A[获取最小距离节点] --> B{是否存在<br>未访问节点?}
B -->|是| C[处理当前节点]
B -->|否| D[算法结束]
C --> E[更新邻居距离]
E --> F[标记为已访问]
F --> A
style A fill:#f96,stroke:#333,stroke-width:2px
style D fill:#f96,stroke:#333,stroke-width:2px
- 距离更新阶段
graph TD
A[遍历邻边] --> B{首次发现<br>邻居节点?}
B -->|是| C[记录新距离]
B -->|否| D[比较并取最小距离]
C --> E[更新distanceMap]
D --> E
style A fill:#f96,stroke:#333,stroke-width:2px
📝 关键步骤说明
- 初始化流程
创建数据结构
↓
设置起点距离为0
↓
准备处理节点
- 主循环流程
获取最小距离节点
↓
检查节点是否存在
↓
处理当前节点
↓
更新邻居距离
↓
标记已访问
- 更新距离流程
检查邻居节点
↓
计算新距离
↓
更新最小距离
↓
继续下一个邻居
🎯 算法终止条件
graph TD
A{终止条件} --> B[所有节点已访问]
A --> C[没有可达节点]
B --> D[返回最短距离表]
C --> D
style A fill:#f96,stroke:#333,stroke-width:2px
style D fill:#f96,stroke:#333,stroke-width:2px
💡 实际应用流程
就像送外卖系统:
graph TD
A[接收订单] --> B[计算初始距离]
B --> C[选择最近地点]
C --> D[送达并更新路线]
D --> E{还有未送订单?}
E -->|是| C
E -->|否| F[完成配送]
style A fill:#f96,stroke:#333,stroke-width:2px
style F fill:#f96,stroke:#333,stroke-width:2px
这个流程图展示了算法的完整执行过程,从初始化到结束的每个步骤都清晰可见。每个阶段都有明确的任务和转换条件,帮助我们更好地理解算法的工作原理。
再生动形象的解释一遍
好的,让我用一个生活中的例子来解释 Dijkstra 算法。
🚗 送外卖场景
想象你是一个送外卖的骑手,需要从餐厅(起点A)送外卖到不同的地点。
graph LR
A((餐厅A)) --2分钟--> B((商场B))
A --4分钟--> C((小区C))
B --2分钟--> C
B --3分钟--> D((学校D))
C --2分钟--> D
style A fill:#f96,stroke:#333,stroke-width:4px
🎯 目标
找出从餐厅到每个地点的最短送餐时间。
📝 送餐过程
- 初始状态
你在餐厅A,知道:
- 到商场B需要2分钟
- 到小区C需要4分钟
- 还不知道到学校D需要多久
- 第一步:从餐厅A出发
graph LR
A((餐厅A/0分钟)) --2分钟--> B((商场B/2分钟))
A --4分钟--> C((小区C/4分钟))
B --2分钟--> C
B --3分钟--> D((学校D/???))
C --2分钟--> D
style A fill:#f96,stroke:#333,stroke-width:4px
就像你在手机上看到:
✅ 商场B:2分钟
✅ 小区C:4分钟
❓ 学校D:还不知道
- 第二步:去最近的地点(商场B)
graph LR
A((餐厅A/0分钟)) --2分钟--> B((商场B/2分钟))
A --4分钟--> C((小区C/4分钟))
B --2分钟--> C
B --3分钟--> D((学校D/5分钟))
C --2分钟--> D
style A fill:#f96,stroke:#333,stroke-width:4px
style B fill:#f96,stroke:#333,stroke-width:4px
到了商场B后,你发现:
- 从B到C只需2分钟(总共4分钟,和直接从A去C一样快)
- 从B到D需要3分钟(总共5分钟)
- 第三步:去下一个最近的地点(小区C)
graph LR
A((餐厅A/0分钟)) --2分钟--> B((商场B/2分钟))
A --4分钟--> C((小区C/4分钟))
B --2分钟--> C
B --3分钟--> D((学校D/5分钟))
C --2分钟--> D
style A fill:#f96,stroke:#333,stroke-width:4px
style B fill:#f96,stroke:#333,stroke-width:4px
style C fill:#f96,stroke:#333,stroke-width:4px
到了小区C后,你计算:
从C到D需要2分钟(总共6分钟)
但是之前通过B去D只需5分钟,所以不用更新
🎉 最终结果
从餐厅A出发:
🏪 到商场B:2分钟(直接去)
🏘️ 到小区C:4分钟(直接去或经过商场B都可以)
🏫 到学校D:5分钟(经过商场B)
💡 算法要点解释
- 就像配送系统
- distanceMap 就像你的导航软件,记录到每个地点的最短时间
- selectedNodes 就像你的配送完成记录
- 选择策略
- 每次都选择当前最近的未配送地点
- 就像你实际送外卖时会先送近的
- 更新距离
- 每到一个新地点,就看看通过这里能不能更快地到达其他地点
- 就像你送完一单后,看看顺路能不能更快地送下一单
🌟 生活中的类比
- 像逛商场
- 从入口开始,先去最近的店铺
- 然后看看通过这个店铺能不能更快地到达其他店铺
- 像地铁换乘
- 有时直达需要4站
- 但换乘可能只需要3站
- 需要不断比较哪条路线更快
这就是 Dijkstra 算法的核心思想:像送外卖一样,每次都选择当前最近的地点,然后看看通过这个地点能不能找到去其他地方的更短路径!
详细步骤打印
class DijkstraAlgorithm {
data class Node(
val id: String,
val edges: MutableList<Edge> = mutableListOf()
)
data class Edge(
val from: Node,
val to: Node,
val weight: Int
)
fun dijkstra(head: Node): Map<Node, Int> {
println("\n=== 开始执行Dijkstra算法 ===")
println("起点: ${head.id}")
// 第1步:创建距离表
val distanceMap = mutableMapOf<Node, Int>()
distanceMap[head] = 0
println("\n1. 初始化距离表:")
println(" ${head.id} -> 0")
// 第2步:创建已确定节点集合
val selectedNodes = mutableSetOf<Node>()
println("\n2. 创建已确定节点集合(初始为空)")
// 第3步:获取首个最小距离节点
var minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes)
println("\n3. 选择首个最小距离节点: ${minNode?.id}")
// 第4步:主循环
var iteration = 1
while (minNode != null) {
println("\n=== 迭代 $iteration ===")
val distance = distanceMap[minNode]!!
println("当前处理节点: ${minNode.id}, 距离: $distance")
// 第5步:检查邻边
println("\n5. 检查节点 ${minNode.id} 的所有邻边:")
for (edge in minNode.edges) {
val toNode = edge.to
println("\n 检查边: ${minNode.id} -> ${toNode.id}, 权重: ${edge.weight}")
// 第6步:更新邻居距离
val newDistance = distance + edge.weight
if (!distanceMap.containsKey(toNode)) {
distanceMap[toNode] = newDistance
println(" 首次发现节点 ${toNode.id}, 设置距离: $newDistance")
} else {
val oldDistance = distanceMap[toNode]!!
if (newDistance < oldDistance) {
distanceMap[toNode] = newDistance
println(" 更新节点 ${toNode.id} 的距离: $oldDistance -> $newDistance")
} else {
println(" 保持节点 ${toNode.id} 的原距离: $oldDistance")
}
}
}
// 第7步:标记节点为已确定
selectedNodes.add(minNode)
println("\n6. 标记节点 ${minNode.id} 为已确定")
println(" 已确定节点集合: ${selectedNodes.map { it.id }}")
// 打印当前距离表状态
println("\n当前距离表:")
distanceMap.forEach { (node, dist) ->
println(" ${node.id} -> $dist ${if (node in selectedNodes) "(已确定)" else ""}")
}
// 第8步:选择下一个节点
minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes)
println("\n7. 选择下一个最小距离节点: ${minNode?.id}")
iteration++
}
println("\n=== Dijkstra算法完成 ===")
println("最终距离表:")
distanceMap.forEach { (node, distance) ->
println("${node.id} -> $distance")
}
return distanceMap
}
private fun getMinDistanceAndUnselectedNode(
distanceMap: Map<Node, Int>,
selectedNodes: Set<Node>
): Node? {
var minNode: Node? = null
var minDistance = Int.MAX_VALUE
for ((node, distance) in distanceMap) {
if (node !in selectedNodes && distance < minDistance) {
minNode = node
minDistance = distance
}
}
return minNode
}
}
fun main() {
// 创建测试图
val nodeA = DijkstraAlgorithm.Node("A")
val nodeB = DijkstraAlgorithm.Node("B")
val nodeC = DijkstraAlgorithm.Node("C")
val nodeD = DijkstraAlgorithm.Node("D")
// 添加边
nodeA.edges.add(DijkstraAlgorithm.Edge(nodeA, nodeB, 5))
nodeA.edges.add(DijkstraAlgorithm.Edge(nodeA, nodeC, 2))
nodeB.edges.add(DijkstraAlgorithm.Edge(nodeB, nodeD, 4))
nodeC.edges.add(DijkstraAlgorithm.Edge(nodeC, nodeB, 1))
nodeC.edges.add(DijkstraAlgorithm.Edge(nodeC, nodeD, 3))
// 执行算法
val algorithm = DijkstraAlgorithm()
algorithm.dijkstra(nodeA)
}
运行结果:
=== 开始执行Dijkstra算法 ===
起点: A
1. 初始化距离表:
A -> 0
2. 创建已确定节点集合(初始为空)
3. 选择首个最小距离节点: A
=== 迭代 1 ===
当前处理节点: A, 距离: 0
5. 检查节点 A 的所有邻边:
检查边: A -> B, 权重: 5
首次发现节点 B, 设置距离: 5
检查边: A -> C, 权重: 2
首次发现节点 C, 设置距离: 2
6. 标记节点 A 为已确定
已确定节点集合: [A]
当前距离表:
A -> 0 (已确定)
B -> 5
C -> 2
7. 选择下一个最小距离节点: C
=== 迭代 2 ===
当前处理节点: C, 距离: 2
5. 检查节点 C 的所有邻边:
检查边: C -> B, 权重: 1
更新节点 B 的距离: 5 -> 3
检查边: C -> D, 权重: 3
首次发现节点 D, 设置距离: 5
6. 标记节点 C 为已确定
已确定节点集合: [A, C]
当前距离表:
A -> 0 (已确定)
B -> 3
C -> 2 (已确定)
D -> 5
7. 选择下一个最小距离节点: B
=== 迭代 3 ===
当前处理节点: B, 距离: 3
5. 检查节点 B 的所有邻边:
检查边: B -> D, 权重: 4
更新节点 D 的距离: 5 -> 7
6. 标记节点 B 为已确定
已确定节点集合: [A, C, B]
当前距离表:
A -> 0 (已确定)
B -> 3 (已确定)
C -> 2 (已确定)
D -> 7
7. 选择下一个最小距离节点: D
=== 迭代 4 ===
当前处理节点: D, 距离: 7
5. 检查节点 D 的所有邻边:
6. 标记节点 D 为已确定
已确定节点集合: [A, C, B, D]
当前距离表:
A -> 0 (已确定)
B -> 3 (已确定)
C -> 2 (已确定)
D -> 7 (已确定)
7. 选择下一个最小距离节点: null
=== Dijkstra算法完成 ===
最终距离表:
A -> 0
B -> 3
C -> 2
D -> 7
这个输出清晰地展示了:
- 每次迭代选择的节点
- 处理每条边时的距离更新
- 已确定节点的变化
- 距离表的实时状态
通过这些详细的步骤打印,你可以更好地理解 Dijkstra 算法的工作原理。
3. 最小生成树算法(Kruskal)
Kruskal算法
让我用图解方式详细讲解 Kruskal(克鲁斯卡尔)最小生成树算法。
1. 什么是最小生成树?
graph LR
A((A)) --- B((B))
A --- C((C))
B --- C
B --- D((D))
C --- D
变成:
graph LR
A((A)) --- B((B))
A --- C((C))
C --- D((D))
目标:在保持图连通的前提下,选择总权重最小的边集合。
2. Kruskal 算法的基本思想
就像修建城市道路:
- 列出所有可能的道路(边)及其成本(权重)
- 从最便宜的道路开始修建
- 避免形成环路(浪费资源)
3. 算法步骤演示
假设有这样一个带权图:
graph LR
A((A)) --4--- B((B))
A --2--- C((C))
B --3--- C
B --1--- D((D))
C --5--- D
style A fill:#f96,stroke:#333,stroke-width:2px
第1步:排序所有边
边列表(按权重排序):
1. B-D:1
2. A-C:2
3. B-C:3
4. A-B:4
5. C-D:5
第2步:选择最小边 B-D
graph LR
A((A))
B((B)) --1--- D((D))
C((C))
style B fill:#f96,stroke:#333,stroke-width:2px
style D fill:#f96,stroke:#333,stroke-width:2px
第3步:选择次小边 A-C
graph LR
A((A)) --2--- C((C))
B((B)) --1--- D((D))
style A fill:#f96,stroke:#333,stroke-width:2px
style B fill:#f96,stroke:#333,stroke-width:2px
style C fill:#f96,stroke:#333,stroke-width:2px
style D fill:#f96,stroke:#333,stroke-width:2px
第4步:选择 B-C(连接两个分离的部分)
graph LR
A((A)) --2--- C((C))
B((B)) --1--- D((D))
B --3--- C
style A fill:#f96,stroke:#333,stroke-width:2px
style B fill:#f96,stroke:#333,stroke-width:2px
style C fill:#f96,stroke:#333,stroke-width:2px
style D fill:#f96,stroke:#333,stroke-width:2px
4. 代码实现
public class Kruskal {
// 边的定义
class Edge implements Comparable<Edge> {
Node from;
Node to;
int weight;
public Edge(Node f, Node t, int w) {
from = f;
to = t;
weight = w;
}
@Override
public int compareTo(Edge e) {
return this.weight - e.weight;
}
}
// 并查集结构
class UnionFind {
private HashMap<Node, Node> parent;
public UnionFind(Collection<Node> nodes) {
parent = new HashMap<>();
for (Node node : nodes) {
parent.put(node, node);
}
}
public Node find(Node node) {
Node father = parent.get(node);
if (father != node) {
father = find(father);
}
parent.put(node, father);
return father;
}
public void union(Node a, Node b) {
parent.put(find(a), find(b));
}
}
// Kruskal算法主体
public Set<Edge> kruskalMST(Graph graph) {
// 创建并查集
UnionFind unionFind = new UnionFind(graph.nodes.values());
// 边按权重排序
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>();
for (Edge edge : graph.edges) {
priorityQueue.add(edge);
}
Set<Edge> result = new HashSet<>();
while (!priorityQueue.isEmpty()) {
Edge edge = priorityQueue.poll(); // 取出最小边
if (unionFind.find(edge.from) != unionFind.find(edge.to)) {
// 如果不会形成环,则加入结果集
result.add(edge);
unionFind.union(edge.from, edge.to);
}
}
return result;
}
}
代码详细讲解
让我详细拆解这段代码,用生动的比喻来解释。
1. Edge类(边的定义)
class Edge implements Comparable<Edge> {
Node from; // 起点
Node to; // 终点
int weight; // 权重(路径长度/成本)
}
就像规划城市道路:
from: 起始城市
to: 目标城市
weight: 修建这条道路的成本
比较方法:
public int compareTo(Edge e) {
return this.weight - e.weight;
}
就像比较两条道路的造价,便宜的排在前面。
2. 并查集(UnionFind)
graph TD
A[并查集结构] --> B[城市分组管理]
B --> C[初始时每个城市独立]
B --> D[连接后城市分到同一组]
B --> E[防止形成环路]
class UnionFind {
private HashMap<Node, Node> parent; // 记录每个节点的父节点
}
就像城市的行政区划:
- 每个城市最初都是独立的
- 连接后的城市属于同一个区域
- parent记录每个城市属于哪个区域
初始化:
public UnionFind(Collection<Node> nodes) {
parent = new HashMap<>();
for (Node node : nodes) {
parent.put(node, node);
}
}
就像:
一开始每个城市都是独立的行政区
每个城市都是自己的管理者
查找操作:
public Node find(Node node) {
Node father = parent.get(node);
if (father != node) {
father = find(father);
}
parent.put(node, father);
return father;
}
就像:
查找一个城市属于哪个行政区
如果这个城市不是区长
就继续往上找,直到找到区长
同时更新管理关系,提高以后查找效率
合并操作:
public void union(Node a, Node b) {
parent.put(find(a), find(b));
}
就像:
把两个城市划分到同一个行政区
让其中一个区长管理另一个区
3. Kruskal算法主体
graph TD
A[开始] --> B[创建城市管理系统]
B --> C[对所有道路按成本排序]
C --> D[选择最便宜的道路]
D --> E{是否形成环?}
E -->|否| F[建设这条道路]
E -->|是| G[放弃这条道路]
F --> H[更新城市分组]
H --> I{还有道路可选?}
G --> I
I -->|是| D
I -->|否| J[完成]
代码实现:
public Set<Edge> kruskalMST(Graph graph) {
// 1. 创建城市管理系统
UnionFind unionFind = new UnionFind(graph.nodes.values());
// 2. 将所有道路按成本排序
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>();
for (Edge edge : graph.edges) {
priorityQueue.add(edge);
}
// 3. 开始建设道路
Set<Edge> result = new HashSet<>();
while (!priorityQueue.isEmpty()) {
// 4. 选择最便宜的道路
Edge edge = priorityQueue.poll();
// 5. 检查是否会形成环
if (unionFind.find(edge.from) != unionFind.find(edge.to)) {
// 6. 建设这条道路
result.add(edge);
// 7. 更新城市分组
unionFind.union(edge.from, edge.to);
}
}
return result;
}
实际执行过程示例:
假设有这样的道路规划:
graph LR
A((城市A)) --4--- B((城市B))
A --2--- C((城市C))
B --3--- C
B --1--- D((城市D))
C --5--- D
执行步骤:
- 初始状态:每个城市都是独立的
- 选择B-D:成本1,最便宜的道路
- 选择A-C:成本2,第二便宜的
- 选择B-C:成本3,连接两个分离的区域
最终结果:
graph LR
A((城市A)) --2--- C((城市C))
B((城市B)) --1--- D((城市D))
B --3--- C
这就像是一个精明的城市规划者:
- 列出所有可能的道路和成本
- 优先建设便宜的道路
- 避免建设形成环路的道路(浪费资源)
- 最终用最少的成本连接所有城市
5. 算法流程图
graph TD
A[开始] --> B[创建并查集]
B --> C[将所有边按权重排序]
C --> D[取出最小权重的边]
D --> E{是否形成环?}
E -->|否| F[加入结果集]
E -->|是| G[丢弃该边]
F --> H[合并顶点]
H --> I{还有边吗?}
G --> I
I -->|是| D
I -->|否| J[结束]
style A fill:#f96,stroke:#333,stroke-width:2px
style J fill:#f96,stroke:#333,stroke-width:2px
6. 关键点解释
- 并查集的作用
- 快速判断两个顶点是否连通
- 防止形成环
- 合并连通分量
- 边的处理顺序
- 按权重从小到大处理
- 保证选择最小权重的边
- 使用优先队列实现排序
- 环的判断
- 如果两个顶点已经在同一个集合中
- 则添加这条边会形成环
- 使用并查集的find操作判断
7. 实际应用场景
- 网络布线
- 连接所有计算机
- 使用最少的网线
- 最小化成本
- 城市规划
- 连接所有区域
- 最小化道路建设成本
- 保证交通可达
- 电力网络
- 连接所有用电区域
- 最小化电缆成本
- 保证供电可靠性
8. 算法复杂度
时间复杂度:O(ElogE)
- E是边的数量
- 主要来自边的排序
空间复杂度:O(V)
- V是顶点数量
- 主要来自并查集
Kruskal算法就像是一个精明的建设者,总是优先选择成本最低的道路来连接各个区域,同时避免不必要的重复建设(环路)。
详细步骤打印
class Kruskal {
data class Node(val id: String)
data class Edge(
val from: Node,
val to: Node,
val weight: Int
) : Comparable<Edge> {
override fun compareTo(other: Edge) = this.weight - other.weight
override fun toString() = "${from.id}-${to.id}(${weight})"
}
class UnionFind(nodes: Collection<Node>) {
private val parent = mutableMapOf<Node, Node>()
init {
println("\n初始化并查集:")
nodes.forEach { node ->
parent[node] = node
println("节点 ${node.id} 的父节点为自己")
}
}
fun find(node: Node): Node {
var current = node
var father = parent[current]!!
if (father != current) {
println("查找 ${node.id} 的根节点:")
println("${current.id} -> ${father.id}")
father = find(father)
parent[current] = father
println("路径压缩: ${current.id} 直接指向根节点 ${father.id}")
}
return father
}
fun union(a: Node, b: Node) {
val rootA = find(a)
val rootB = find(b)
println("合并 ${a.id} 和 ${b.id}:")
println("${a.id} 的根节点: ${rootA.id}")
println("${b.id} 的根节点: ${rootB.id}")
if (rootA != rootB) {
parent[rootA] = rootB
println("将 ${rootA.id} 指向 ${rootB.id}")
}
}
}
fun kruskalMST(nodes: Set<Node>, edges: Set<Edge>): Set<Edge> {
println("=== 开始 Kruskal 算法 ===")
println("节点集合: ${nodes.map { it.id }}")
println("边集合: $edges")
// 1. 创建并查集
val unionFind = UnionFind(nodes)
// 2. 边按权重排序
val priorityQueue = PriorityQueue<Edge>()
println("\n将所有边加入优先队列:")
edges.forEach { edge ->
priorityQueue.add(edge)
println("添加边: $edge")
}
// 3. 主循环
val result = mutableSetOf<Edge>()
var iteration = 1
println("\n开始处理边:")
while (priorityQueue.isNotEmpty()) {
println("\n=== 迭代 $iteration ===")
// 取出最小边
val edge = priorityQueue.poll()
println("当前处理最小权重边: $edge")
// 检查是否形成环
val fromRoot = unionFind.find(edge.from)
val toRoot = unionFind.find(edge.to)
if (fromRoot != toRoot) {
println("该边不会形成环,添加到结果集")
result.add(edge)
unionFind.union(edge.from, edge.to)
println("当前最小生成树边集: $result")
} else {
println("该边会形成环,跳过")
}
iteration++
}
println("\n=== Kruskal 算法完成 ===")
println("最小生成树边集: $result")
println("总权重: ${result.sumOf { it.weight }}")
return result
}
}
fun main() {
// 创建测试图
val nodeA = Kruskal.Node("A")
val nodeB = Kruskal.Node("B")
val nodeC = Kruskal.Node("C")
val nodeD = Kruskal.Node("D")
val edges = setOf(
Kruskal.Edge(nodeA, nodeB, 4),
Kruskal.Edge(nodeA, nodeC, 2),
Kruskal.Edge(nodeB, nodeC, 1),
Kruskal.Edge(nodeB, nodeD, 3),
Kruskal.Edge(nodeC, nodeD, 5)
)
val nodes = setOf(nodeA, nodeB, nodeC, nodeD)
// 执行算法
val kruskal = Kruskal()
kruskal.kruskalMST(nodes, edges)
}
运行结果:
=== 开始 Kruskal 算法 ===
节点集合: [A, B, C, D]
边集合: [A-B(4), A-C(2), B-C(1), B-D(3), C-D(5)]
初始化并查集:
节点 A 的父节点为自己
节点 B 的父节点为自己
节点 C 的父节点为自己
节点 D 的父节点为自己
将所有边加入优先队列:
添加边: A-B(4)
添加边: A-C(2)
添加边: B-C(1)
添加边: B-D(3)
添加边: C-D(5)
开始处理边:
=== 迭代 1 ===
当前处理最小权重边: B-C(1)
该边不会形成环,添加到结果集
合并 B 和 C:
B 的根节点: B
C 的根节点: C
将 B 指向 C
当前最小生成树边集: [B-C(1)]
=== 迭代 2 ===
当前处理最小权重边: A-C(2)
该边不会形成环,添加到结果集
合并 A 和 C:
A 的根节点: A
C 的根节点: C
将 A 指向 C
当前最小生成树边集: [B-C(1), A-C(2)]
=== 迭代 3 ===
当前处理最小权重边: B-D(3)
查找 B 的根节点:
B -> C
该边不会形成环,添加到结果集
合并 B 和 D:
B 的根节点: C
D 的根节点: D
将 C 指向 D
当前最小生成树边集: [B-C(1), A-C(2), B-D(3)]
=== 迭代 4 ===
当前处理最小权重边: A-B(4)
查找 A 的根节点:
A -> C
C -> D
路径压缩: A 直接指向根节点 D
查找 B 的根节点:
B -> C
C -> D
路径压缩: B 直接指向根节点 D
该边会形成环,跳过
=== 迭代 5 ===
当前处理最小权重边: C-D(5)
查找 C 的根节点:
C -> D
查找 D 的根节点:
该边会形成环,跳过
=== Kruskal 算法完成 ===
最小生成树边集: [B-C(1), A-C(2), B-D(3)]
总权重: 6
这个输出清晰地展示了:
- 初始化过程
- 边的排序
- 每次选择最小边的过程
- 并查集的操作
- 环的检测
- 最终结果的构建
通过这些详细的步骤打印,你可以更好地理解 Kruskal 算法的工作原理。每一步都显示了:
- 当前处理的边
- 并查集的状态变化
- 是否形成环的判断
- 最小生成树的逐步构建过程
更详细的打印过程
假设我们有这样一个图:
2
A ---- B
|\ |
| \ |
4 3 5
| \ |
| \ |
C ---- D
6
让我们一步步执行:
第1步:初始化
-------------------------------
创建并查集:
parent = {
A -> A,
B -> B,
C -> C,
D -> D
}
所有边按权重排序放入优先队列:
priorityQueue = [
(A-B, 2), // 权重为2的边
(A-D, 3), // 权重为3的边
(A-C, 4), // 权重为4的边
(B-D, 5), // 权重为5的边
(C-D, 6) // 权重为6的边
]
result = [] // 结果集为空
第2步:处理权重最小的边(A-B, 2)
-------------------------------
取出边:A-B (权重2)
检查:find(A) = A, find(B) = B
A和B不在同一集合,可以连接
union(A, B) // 将B连到A
parent = {
A -> A,
B -> A, // B的父节点改为A
C -> C,
D -> D
}
result = [(A-B, 2)]
第3步:处理第二小的边(A-D, 3)
-------------------------------
取出边:A-D (权重3)
检查:find(A) = A, find(D) = D
A和D不在同一集合,可以连接
union(A, D) // 将D连到A
parent = {
A -> A,
B -> A,
C -> C,
D -> A // D的父节点改为A
}
result = [(A-B, 2), (A-D, 3)]
第4步:处理第三小的边(A-C, 4)
-------------------------------
取出边:A-C (权重4)
检查:find(A) = A, find(C) = C
A和C不在同一集合,可以连接
union(A, C) // 将C连到A
parent = {
A -> A,
B -> A,
C -> A, // C的父节点改为A
D -> A
}
result = [(A-B, 2), (A-D, 3), (A-C, 4)]
第5步:处理第四小的边(B-D, 5)
-------------------------------
取出边:B-D (权重5)
检查:find(B) = A, find(D) = A
B和D已经在同一集合中,跳过这条边
result不变
第6步:处理最后一条边(C-D, 6)
-------------------------------
取出边:C-D (权重6)
检查:find(C) = A, find(D) = A
C和D已经在同一集合中,跳过这条边
result不变
最终结果:
最小生成树的边集合:
1. A-B (权重2)
2. A-D (权重3)
3. A-C (权重4)
形成的树结构:
2
A ---- B
|
|
4 3
|
|
C D
总权重:2 + 3 + 4 = 9
这个过程就像修建城市道路:
- 先列出所有可能的道路和成本
- 按照成本从低到高排序
- 每次选择最便宜的道路
- 但要确保不会形成环(已经有路可以到达的地方就不修新路)
- 最终用最少的成本连接所有城市
或者像组织公司团建:
A部门到B部门坐车需要2元
A部门到D部门坐车需要3元
A部门到C部门坐车需要4元
B部门到D部门坐车需要5元
C部门到D部门坐车需要6元
选择路线:
1. 先选最便宜的A-B线路(2元)
2. 再选次便宜的A-D线路(3元)
3. 再选A-C线路(4元)
4. B-D和C-D线路不需要了,因为已经都能互相到达
这样就用最少的成本把所有部门都连接起来了!
最小生成树与最短路径算法的区别
1. 最短路径算法 (Dijkstra) vs 最小生成树 (Kruskal)
Dijkstra(找最短路径):
graph LR
A((A)) --4--- B((B))
A --2--- C((C))
B --1--- D((D))
C --5--- D
style A fill:#f96,stroke:#333,stroke-width:4px
style D fill:#aaf,stroke:#333,stroke-width:4px
目标:找到从A到D的最短路径
最短路径: A → C → B → D
总距离: 2 + 3 + 1 = 6
Kruskal(最小生成树):
graph LR
A((A)) --2--- C((C))
B((B)) --1--- D((D))
B --3--- C
style A fill:#f96,stroke:#333,stroke-width:2px
style B fill:#f96,stroke:#333,stroke-width:2px
style C fill:#f96,stroke:#333,stroke-width:2px
style D fill:#f96,stroke:#333,stroke-width:2px
目标:用最少的总成本连接所有节点
选择的边:
1. B-D (权重1)
2. A-C (权重2)
3. B-C (权重3)
总成本: 1 + 2 + 3 = 6
2. 两者的区别
Dijkstra(最短路径)
目标:找到从一个点到另一个点的最短距离
就像:导航软件找最快的开车路线
特点:
- 关注起点到终点
- 可能不会用到所有边
- 寻找单源最短路径
Kruskal(最小生成树)
目标:用最小的总成本连接所有点
就像:铺设城市电缆,要连接所有建筑
特点:
- 关注整体成本最小
- 必须连接所有节点
- 形成一棵树(无环)
3. 生活中的例子
Dijkstra(最短路径):
graph LR
家((家)) --10分钟--> 商场((商场))
家 --5分钟--> 公园((公园))
商场 --3分钟--> 医院((医院))
公园 --8分钟--> 医院
问题:如何最快从家到医院?
答案:家 → 公园 → 医院 (13分钟)
Kruskal(最小生成树):
graph LR
A((小区A)) --100万--> B((小区B))
A --50万--> C((小区C))
B --30万--> D((小区D))
C --80万--> D
问题:如何用最少的钱铺设电缆连接所有小区?
答案:
1. B-D (30万)
2. A-C (50万)
3. B-C (70万)
总成本:150万
4. 应用场景
Dijkstra适用于:
1. 导航系统
2. 网络路由
3. 社交网络最短关系链
Kruskal适用于:
1. 布置电力网络
2. 设计通信网络
3. 铺设自来水管道
graph TB
subgraph 最短路径-Dijkstra
A1[起点] --> B1[找最短距离]
B1 --> C1[到达终点]
style A1 fill:#f96
style C1 fill:#aaf
end
subgraph 最小生成树-Kruskal
A2[所有点] --> B2[最小总成本]
B2 --> C2[连接所有点]
style A2 fill:#f96
style B2 fill:#f96
style C2 fill:#f96
end
形象比喻:
- Dijkstra 像是导航软件,找到从A到B的最快路线
- Kruskal 像是城市规划,用最少的成本修路连接所有社区
这两个算法就像是两个不同的工具:
- 🚗 Dijkstra:帮你规划最短路线
- 🏗️ Kruskal:帮你做最省钱的建设