左哥算法 - 图及其相关算法(二)

130 阅读22分钟

1. 拓扑排序算法

拓扑排序

适用于有向无环图(DAG),常用于确定任务的执行顺序。

流程:

  1. 统计所有节点的入度
  2. 将入度为0的节点加入队列
  3. 取出队列中的节点,将其邻居节点的入度减1
  4. 如果邻居节点入度变为0,加入队列
  5. 重复步骤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))

在这个例子中:

  1. 内衣(u) → 外套(v)

    • 必须先穿内衣,再穿外套
    • 所以内衣在序列中必须出现在外套之前
  2. 袜子(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.代码执行流程
  1. 初始化阶段
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 = []
  1. 处理入度为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]
  1. 处理入度为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]
  1. 处理入度为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]
  1. 处理入度为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.关键点解释
  1. 入度的概念
入度:指向该节点的边的数量
- 入度为0意味着没有依赖
- 适合作为序列的起始点
  1. HashMap的作用
HashMap<Node, Integer> inMap
  • 记录每个节点的实时入度
  • 方便快速更新和查询
  1. 队列的作用
Queue<Node> zeroInQueue
  • 存储所有入度为0的节点
  • 保证依赖关系的正确处理顺序
6.实际应用场景
  1. 课程安排
课程A -> 课程B(表示AB的先修课)
拓扑排序可以得到合理的学习顺序
  1. 项目管理
任务A -> 任务B(表示A必须在B之前完成)
拓扑排序可以得到合理的任务执行顺序
  1. 软件编译
模块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(...) {

就像配送系统在:

  1. 查看所有已知距离的地点
  2. 排除已送达的地点
  3. 找出最近的一个

在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;
    }
}
  1. 每个城市都知道它和哪些城市相连,以及相连的距离(通过neighbors存储)
  2. distances存储的是从已选择的节点集合到其他节点的当前最短距离
  3. 初始时:
    • 起点到自己距离为0
    • 起点到直接相连的城市距离为边的权重
    • 起点到未直接相连的城市距离为无穷大
  4. 每次选择未访问节点中距离最小的,这个距离是从已选择节点集合到该节点的最短距离

就像是这样一个过程:

  1. 你站在北京(起点)
  2. 你知道:
    • 直接到上海要1200km
    • 直接到广州要2000km
    • 到深圳还不知道怎么走(初始为无穷大)
  3. 这个方法会告诉你:"在还没去过的城市中,上海是最近的(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处理
- 没有新的节点了,结束

这就像送外卖时:

  1. 从餐厅出发
  2. 每次都选最近的地点送
  3. 送到一个地点后看看能否找到更短的路
  4. 直到所有订单都送完

代码详解

让我用图解方式详细讲解 Dijkstra(迪杰斯特拉)算法的工作原理。

Dijkstra算法的目的

计算从一个源点到其他所有点的最短路径。

示例图

假设我们有这样一个带权图:

graph LR
    A((A)) --2--> B((B))
    A --4--> C((C))
    B --2--> C
    B --3--> D((D))
    C --2--> D
算法执行流程
  1. 初始化阶段
HashMap<Node, Integer> distanceMap = new HashMap<>();
distanceMap.put(head, 0);
HashSet<Node> selectedNodes = new HashSet<>();

初始状态:

distanceMap = {A:0}     // 只知道起点A到自己的距离为0
selectedNodes = {}      // 还没有确定任何最短路径
  1. 第一轮:处理节点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}
  1. 第二轮:处理节点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}
  1. 第三轮:处理节点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}
关键代码解析
  1. 获取最小距离的未访问节点
Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
  • 从未确定最短路径的节点中选择距离最小的
  1. 更新相邻节点的距离
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[结束]
实际应用场景
  1. 导航软件
计算从当前位置到目的地的最短路径
  1. 网络路由
数据包寻找最优传输路径
  1. 社交网络
计算用户之间的最短关系链
算法特点
  1. 优点
- 能找到单源最短路径
- 适用于非负权重的图
- 实现相对简单
  1. 缺点
- 不适用于负权重
- 时间复杂度较高 O(V²)
- 需要存储所有节点信息
关键要点
  1. 贪心策略
  • 每次选择当前最短距离的节点
  • 一旦选择就确定了最短路径
  1. 松弛操作
  • 通过已知节点更新其邻居的距离
  • 始终保持最小距离值
  1. 终止条件
  • 所有可达节点都被访问
  • 或者没有更小的距离可以更新

这个算法就像是在一个城市中找路,从起点开始,每次都选择当前最近的未访问地点,然后看看通过这个地点是否能找到到其他地点的更短路径。

整个流程图总结

让我用流程图来总结 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
🔄 主要阶段说明
  1. 初始化阶段
graph TD
    A[创建数据结构] --> B[distanceMap]
    A --> C[selectedNodes]
    B --> D[起点距离设为0]
    C --> E[已访问集合为空]
    
    style A fill:#f96,stroke:#333,stroke-width:2px
  1. 节点处理阶段
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
  1. 距离更新阶段
graph TD
    A[遍历邻边] --> B{首次发现<br>邻居节点?}
    B -->|是| C[记录新距离]
    B -->|否| D[比较并取最小距离]
    C --> E[更新distanceMap]
    D --> E
    
    style A fill:#f96,stroke:#333,stroke-width:2px
📝 关键步骤说明
  1. 初始化流程
创建数据结构
↓
设置起点距离为0
↓
准备处理节点
  1. 主循环流程
获取最小距离节点
↓
检查节点是否存在
↓
处理当前节点
↓
更新邻居距离
↓
标记已访问
  1. 更新距离流程
检查邻居节点
↓
计算新距离
↓
更新最小距离
↓
继续下一个邻居
🎯 算法终止条件
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
🎯 目标

找出从餐厅到每个地点的最短送餐时间。

📝 送餐过程
  1. 初始状态
你在餐厅A,知道:
- 到商场B需要2分钟
- 到小区C需要4分钟
- 还不知道到学校D需要多久
  1. 第一步:从餐厅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

就像你在手机上看到:

✅ 商场B2分钟
✅ 小区C:4分钟
❓ 学校D:还不知道
  1. 第二步:去最近的地点(商场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分钟)
  1. 第三步:去下一个最近的地点(小区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出发:

🏪 到商场B2分钟(直接去)
🏘️ 到小区C:4分钟(直接去或经过商场B都可以)
🏫 到学校D:5分钟(经过商场B
💡 算法要点解释
  1. 就像配送系统
- distanceMap 就像你的导航软件,记录到每个地点的最短时间
- selectedNodes 就像你的配送完成记录
  1. 选择策略
- 每次都选择当前最近的未配送地点
- 就像你实际送外卖时会先送近的
  1. 更新距离
- 每到一个新地点,就看看通过这里能不能更快地到达其他地点
- 就像你送完一单后,看看顺路能不能更快地送下一单
🌟 生活中的类比
  1. 像逛商场
- 从入口开始,先去最近的店铺
- 然后看看通过这个店铺能不能更快地到达其他店铺
  1. 像地铁换乘
- 有时直达需要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

这个输出清晰地展示了:

  1. 每次迭代选择的节点
  2. 处理每条边时的距离更新
  3. 已确定节点的变化
  4. 距离表的实时状态

通过这些详细的步骤打印,你可以更好地理解 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 算法的基本思想

就像修建城市道路:

  1. 列出所有可能的道路(边)及其成本(权重)
  2. 从最便宜的道路开始修建
  3. 避免形成环路(浪费资源)

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-B4
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

执行步骤:

  1. 初始状态:每个城市都是独立的
  2. 选择B-D:成本1,最便宜的道路
  3. 选择A-C:成本2,第二便宜的
  4. 选择B-C:成本3,连接两个分离的区域

最终结果:

graph LR
    A((城市A)) --2--- C((城市C))
    B((城市B)) --1--- D((城市D))
    B --3--- C

这就像是一个精明的城市规划者:

  1. 列出所有可能的道路和成本
  2. 优先建设便宜的道路
  3. 避免建设形成环路的道路(浪费资源)
  4. 最终用最少的成本连接所有城市

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. 关键点解释

  1. 并查集的作用
- 快速判断两个顶点是否连通
- 防止形成环
- 合并连通分量
  1. 边的处理顺序
- 按权重从小到大处理
- 保证选择最小权重的边
- 使用优先队列实现排序
  1. 环的判断
- 如果两个顶点已经在同一个集合中
- 则添加这条边会形成环
- 使用并查集的find操作判断

7. 实际应用场景

  1. 网络布线
- 连接所有计算机
- 使用最少的网线
- 最小化成本
  1. 城市规划
- 连接所有区域
- 最小化道路建设成本
- 保证交通可达
  1. 电力网络
- 连接所有用电区域
- 最小化电缆成本
- 保证供电可靠性

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)
该边不会形成环,添加到结果集
合并 BC:
B 的根节点: B
C 的根节点: CB 指向 C
当前最小生成树边集: [B-C(1)]

=== 迭代 2 ===
当前处理最小权重边: A-C(2)
该边不会形成环,添加到结果集
合并 AC:
A 的根节点: A
C 的根节点: CA 指向 C
当前最小生成树边集: [B-C(1), A-C(2)]

=== 迭代 3 ===
当前处理最小权重边: B-D(3)
查找 B 的根节点:
B -> C
该边不会形成环,添加到结果集
合并 BD:
B 的根节点: C
D 的根节点: DC 指向 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

这个输出清晰地展示了:

  1. 初始化过程
  2. 边的排序
  3. 每次选择最小边的过程
  4. 并查集的操作
  5. 环的检测
  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

这个过程就像修建城市道路:

  1. 先列出所有可能的道路和成本
  2. 按照成本从低到高排序
  3. 每次选择最便宜的道路
  4. 但要确保不会形成环(已经有路可以到达的地方就不修新路)
  5. 最终用最少的成本连接所有城市

或者像组织公司团建:

A部门到B部门坐车需要2A部门到D部门坐车需要3A部门到C部门坐车需要4B部门到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:帮你做最省钱的建设