数据结构与算法 - 图

一、概念

图是有顶点(vertex)和边(edge)组成的数据结构,例如

该图有4个顶点:A、B、C、D以及四条有向边,有向图中,边是单向的。

1. 有向图 VS 无向图

如果是无向图,那么边是双向的,下面是一个无向图的例子

2. 度

度是指与该顶点相邻的边的数量

例如,上图中

  • A、B、C、E、F这几个顶点的度数为2
  • D顶点度数为4

有向图中,细分为入度和出度,参见下图

  • A(2out / 0 in)
  • B、C、E(1 out / 1 in)
  • D(2 out / 2 in)
  • F(0 out / 2 in)

3. 权

边可以有权重,代表从源顶点到目标顶点的距离、费用、时间或其他度量。

4. 路径

路径被定义为从一个顶点到另一个顶点的另一个顶点的一系列连续边,例如上图中【北京】到【上海有多条路径

  • 北京 - 上海
  • 北京 - 武汉 - 上海

路径长度

  • 不考虑权重,长度就是边的数量
  • 考虑权重,一般就是权重累加

5. 环

在有向图中,从一个顶点开始,可以通过若干条有向边返回到该顶点,那么就形成了一个环

6. 图的连通性

如果两个顶点之间存在路径,则这两个顶点是连通的,所有顶点都连通,则该图被称为连通图。若子图连通,则称为连通分量。

二、图的表示

比如说,下面的无向图

用邻接矩阵可以表示为:

  A B C D
A 0 1 1 0
B 1 0 0 1 
C 1 0 0 1
D 0 1 1 0

用邻接表可以表示为:

A -> B -> C
B -> A -> D
C -> A -> D
D -> B -> C

有向图的例子

  A B C D
A 0 1 1 0
B 0 0 0 1
C 0 0 0 1
D 0 0 0 0
A - B - C
B - D
C - D
D - empty

三、Java表示

顶点

package com.itheima.datastructure.graph;

import java.util.List;
import java.util.Objects;

/**
 * 顶点
 */
public class Vertex {
    String name;
    public List<Edge> edges;
    boolean visited;  // 是否被访问过,用在BFS和DFS
    int inDegree;  // 入度,用在拓扑排序
    int status;  // 状态 0-未访问 1-访问中 2-访问过,用在拓扑排序
    static final Integer INF = Integer.MAX_VALUE;
    int dist = INF;  // 距离
    Vertex prev = null;  // 记录从何而来
    
    public Vertex(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        String n = name;
        if (visited) {
            n = "\u001B[31m" + name + "\u001B[0m";
        }
        return n + '(' + (dist == Integer.MAX_VALUE ? "∞" : String.valueOf(dist)) + ") <- " + (prev == null ? "null" : prev.name);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Vertex vertex = (Vertex) o;

        return Objects.equals(name, vertex.name);
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }
}

package com.itheima.datastructure.graph;

public class Edge {
    public Vertex linked;
    public int weight;

    public Edge(Vertex linked) {
        this(linked, 1);
    }

    public Edge(Vertex linked, int weight) {
        this.linked = linked;
        this.weight = weight;
    }
}

四、DFS(深度优先搜索)

package com.itheima.datastructure.graph;

import java.util.LinkedList;
import java.util.List;

public class Dfs {
    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");

        v1.edges = List.of(new Edge(v3), new Edge(v2), new Edge(v6));
        v2.edges = List.of(new Edge(v4));
        v3.edges = List.of(new Edge(v4), new Edge(v6));
        v4.edges = List.of(new Edge(v5));
        v5.edges = List.of();
        v6.edges = List.of(new Edge(v5));

        dfs2(v1);
    }

    /**
     * 递归方式实现深度优先遍历
     * @param v
     */
    private static void dfs(Vertex v) {
        v.visited = true;
        System.out.print(v.name + " ");
        for (Edge edge : v.edges) {
            if(!edge.linked.visited) {
                dfs(edge.linked);
            }
        }
    }

    /**
     * 非递归方式深度优先遍历
     * @param v
     */
    public static void dfs2(Vertex v) {
        LinkedList<Vertex> stack = new LinkedList<>();
        stack.push(v);

        while (!stack.isEmpty()) {
            Vertex pop = stack.pop();
            pop.visited = true;
            System.out.println(pop.name);
            for (Edge edge : pop.edges) {
                if(!edge.linked.visited) {
                    stack.push(edge.linked);
                }
            }
        }
    }

}

五、BFS(广度优先搜索)

package com.itheima.datastructure.graph;

import java.util.LinkedList;
import java.util.List;

public class Bfs {

    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");

        v1.edges = List.of(new Edge(v3), new Edge(v2), new Edge(v6));
        v2.edges = List.of(new Edge(v4));
        v3.edges = List.of(new Edge(v4), new Edge(v6));
        v4.edges = List.of(new Edge(v5));
        v5.edges = List.of();
        v6.edges = List.of(new Edge(v5));

        bfs(v1);
    }

    /**
     * 广度优先遍历 - 队列
     * @param v
     */
    private static void bfs(Vertex v) {
        LinkedList<Vertex> queue = new LinkedList<>();
        v.visited = true;
        queue.offer(v);
        while(!queue.isEmpty()) {
            Vertex poll = queue.poll();
            System.out.println(poll.name);
            for (Edge edge : poll.edges) {
                if(!edge.linked.visited) {
                    edge.linked.visited = true;
                    queue.offer(edge.linked);
                }
            }
        }
    }
}

六、拓扑排序

1. Kahn

package com.itheima.datastructure.graph;

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

public class TopologicalSort {

    public static void main(String[] args) {
        Vertex v1 = new Vertex("网页基础");
        Vertex v2 = new Vertex("Java基础");
        Vertex v3 = new Vertex("JavaWeb");
        Vertex v4 = new Vertex("Spring框架");
        Vertex v5 = new Vertex("微服务框架");
        Vertex v6 = new Vertex("数据库");
        Vertex v7 = new Vertex("实战项目");

        v1.edges = List.of(new Edge(v3)); // +1
        v2.edges = List.of(new Edge(v3)); // +1
        v3.edges = List.of(new Edge(v4));
        v6.edges = List.of(new Edge(v4));
        v4.edges = List.of(new Edge(v5));
        v5.edges = List.of(new Edge(v7));
        v7.edges = List.of();

        List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7);
        topologicalSort(graph);
    }

    private static void topologicalSort(List<Vertex> graph) {
        // 1. 统计每个顶点的入度
        for (Vertex v : graph) {  // 顶点集
            for (Edge edge : v.edges) {  // 边集
                edge.linked.inDegree++;
            }
        }
        // 2. 将入度为0的顶点加入队列
        LinkedList<Vertex> queue = new LinkedList<>();
        for (Vertex v : graph) {
            if(v.inDegree == 0) {
                queue.offer(v);
            }
        }
        // 3. 队列中不断移除顶点,每移除一个顶点,把他相邻顶点入度减1,若减到0则入队
        List<String> result = new ArrayList<>();
        while(!queue.isEmpty()) {
            Vertex poll = queue.poll();
            result.add(poll.name);
            for (Edge edge : poll.edges) {
                edge.linked.inDegree--;
                if(edge.linked.inDegree == 0) {
                    queue.offer(edge.linked);
                }
            }
        }
        if(result.size() != graph.size()) {
            System.out.println("图中出现环");
        } else {
            for (String key : result) {
                System.out.println(key);
            }
        }
    }
}

2. DFS

package com.itheima.datastructure.graph;

import java.util.LinkedList;
import java.util.List;

public class TopologicalSortDFS {
    public static void main(String[] args) {
        Vertex v1 = new Vertex("网页基础");
        Vertex v2 = new Vertex("Java基础");
        Vertex v3 = new Vertex("JavaWeb");
        Vertex v4 = new Vertex("Spring框架");
        Vertex v5 = new Vertex("微服务框架");
        Vertex v6 = new Vertex("数据库");
        Vertex v7 = new Vertex("实战项目");

        v1.edges = List.of(new Edge(v3));
        v2.edges = List.of(new Edge(v3));
        v3.edges = List.of(new Edge(v4));
        v6.edges = List.of(new Edge(v4));
        v4.edges = List.of(new Edge(v5));
        v5.edges = List.of(new Edge(v7));
        v7.edges = List.of();

        List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7);
        LinkedList<String> result = new LinkedList<>();
        for (Vertex v : graph) {
            if(v.status==0) {
                dfs(v, result);
            }
        }
        System.out.println(result);
    }

    private static void dfs(Vertex v, LinkedList<String> result) {
        if(v.status == 2) {
            // 已经访问过了
            return;
        }
        if(v.status == 1) {
            // 访问中
            throw new RuntimeException("发现环");
        }
        v.status = 1;
        // 遍历相邻节点
        for (Edge edge : v.edges) {
            dfs(edge.linked, result);
        }
        v.status = 2;
        // 向回走再压栈
        result.push(v.name);
    }
}

七、最短路径

1. Dijkstra

算法描述:

1. 将所有顶点标记为未访问,创建一个未访问顶点的集合

2. 为每个顶点分配一个临时距离值

  • 对于我们的初始顶点,将其设置为零
  • 对于所有其他顶点,将其设置为无穷大

3. 每次选择最小临时距离的未访问顶点,作为新的当前顶点

4. 对于当前顶点,遍历其所有未访问的邻居,并更新他们的临时距离为更小

  • 例如,1->6的距离为14,而1->3->6的距离是11.这时将距离更新为11
  • 否则,将保留上次距离值

5. 当前顶点的邻居处理完成后,把它从未访问集合中删除

代码:

package com.itheima.datastructure.graph;

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

public class Dijkstra {
    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");

        v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));
        v2.edges = List.of(new Edge(v4, 15));
        v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));
        v4.edges = List.of(new Edge(v5, 6));
        v5.edges = List.of();
        v6.edges = List.of(new Edge(v5, 9));

        List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6);

        dijkstra(graph, v1);
    }

    private static void dijkstra(List<Vertex> graph, Vertex source) {
        // 1. 将所有顶点标记为未访问,创建一个未访问顶点的集合
        ArrayList<Vertex> list = new ArrayList<>(graph);
        // 2. 初始顶点距离设置为0
        source.dist = 0;

        while(!list.isEmpty()) {
            // 3. 选择最小临时距离的未访问节点,作为新的当前顶点
            Vertex curr = chooseMinDistVertex(list);
            // 4. 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小
            updateNeighbourDist(curr, list);
            // 5. 当前顶点的邻居处理完成后,把它从未访问集合中移除
            list.remove(curr);
        }

        for (Vertex v : graph) {
            System.out.println(v.name + " " + v.dist);
        }
    }

    /**
     * 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小
     * @param curr
     * @param list
     */
    private static void updateNeighbourDist(Vertex curr, ArrayList<Vertex> list) {
        for (Edge edge : curr.edges) {
            Vertex n = edge.linked;
            if(list.contains(n)) {
                int dist = curr.dist + edge.weight;
                if(dist < n.dist) {
                    n.dist = dist;
                }
            }
        }
    }

    /**
     * 选取当前顶点 - 找到距离值最小的顶点
     * @param list
     * @return
     */
    private static Vertex chooseMinDistVertex(ArrayList<Vertex> list) {
        Vertex min = list.get(0);
        for (int i = 1; i < list.size(); i++) {
            if(list.get(i).dist < min.dist) {
                min = list.get(i);
            }
        }
        return min;
    }
}

改进:优先级队列

1. 创建一个优先级队列,放入所有顶点(队列大小会达到边的数量)

2. 为每个顶点分配一个临时距离值

  • 对于初始顶点,将其设置为零
  • 对于其他所有顶点,将其设置为无穷大

3. 每次选择最小临时距离的未访问顶点,作为新的当前顶点

4. 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小,若距离更新需要加入队列

  • 例如,1->6的距离是14,而1->3->6的距离是11。这时将距离更新为11
  • 否则,将保留上次距离值

5. 当前顶点的邻居处理完成后,把它从队列中删除

package com.itheima.datastructure.graph;

import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;

public class DijkstraPriorityQueue {

    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");

        v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));
        v2.edges = List.of(new Edge(v4, 15));
        v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));
        v4.edges = List.of(new Edge(v5, 6));
        v5.edges = List.of();
        v6.edges = List.of(new Edge(v5, 9));

        List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6);

        dijkstra(graph, v1);
    }

    private static void dijkstra(List<Vertex> graph, Vertex source) {
        // 1. 创建一个优先级队列,放入所有顶点(小根堆)
        PriorityQueue<Vertex> queue = new PriorityQueue<>(Comparator.comparing(v -> v.dist));
        // 2. 对于初始顶点,将其临时距离值设置为0
        source.dist = 0;
        
        for (Vertex v : graph) {
            queue.offer(v);
        }
        
        while(!queue.isEmpty()) {
            System.out.println(queue);
            // 3. 选取当前顶点(距离值最小的顶点)
            Vertex curr = queue.peek();
            // 4. 更新当前顶点邻居距离
            if(!curr.visited) {
                updateNeighbourDist(curr, queue);
                curr.visited = true;
            }
            // 5. 移除当前节点
            queue.poll();
        }

        for (Vertex v : graph) {
            System.out.println(v.name + " " + v.dist + " " + (v.prev != null ? v.prev.name : "null"));
        }
    }

    private static void updateNeighbourDist(Vertex curr, PriorityQueue<Vertex> queue) {
        for (Edge edge : curr.edges) {
            Vertex n = edge.linked;
            if(!n.visited) {
                int dist = curr.dist + edge.weight;
                if(dist < n.dist) {
                    n.dist = dist;
                    n.prev = curr;
                    queue.offer(n);
                }
            }
        }
    }
}

2. Bellman-Ford

问题

按照Dijkstra算法,得出

  • v1 -> v2最短距离2
  • v1 -> v3最短距离1
  • v1 -> v4最短距离2

事实应当是

  • v1 -> v2最短距离2
  • v1 -> v3最短距离0
  • v1 -> v4最短距离1
package com.itheima.datastructure.graph;

import java.util.List;

/**
 * Bellman-Ford算法,可以处理负边
 */
public class BellmanFord {

    public static void main(String[] args) {
        // 正常情况
        /*Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");

        v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));
        v2.edges = List.of(new Edge(v4, 15));
        v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));
        v4.edges = List.of(new Edge(v5, 6));
        v5.edges = List.of();
        v6.edges = List.of(new Edge(v5, 9));

        List<Vertex> graph = List.of(v4, v5, v6, v1, v2, v3);*/

        // 负边情况
        /*Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");

        v1.edges = List.of(new Edge(v2, 2), new Edge(v3, 1));
        v2.edges = List.of(new Edge(v3, -2));
        v3.edges = List.of(new Edge(v4, 1));
        v4.edges = List.of();
        List<Vertex> graph = List.of(v1, v2, v3, v4);*/

        // 负环情况
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");

        v1.edges = List.of(new Edge(v2, 2));
        v2.edges = List.of(new Edge(v3, -4));
        v3.edges = List.of(new Edge(v4, 1), new Edge(v1, 1));
        v4.edges = List.of();
        List<Vertex> graph = List.of(v1, v2, v3, v4);

        bellmanFord(graph, v1);
    }

    private static void bellmanFord(List<Vertex> graph, Vertex source) {
        source.dist = 0;
        int size = graph.size();
        for (int i = 0; i < size - 1; i++) {
            for (Vertex s : graph) {
                for (Edge edge : s.edges) {
                    Vertex e = edge.linked;
                    if(s.dist != Integer.MAX_VALUE && s.dist + edge.weight < e.dist) {
                        e.dist = s.dist + edge.weight;
                        e.prev = s;
                    }
                }
            }
        }
        for (Vertex v : graph) {
            System.out.println(v);
        }
    }
}

负环

如果在【顶点-1】轮处理完成后,还能继续找到更短距离,表示发现了负环。比如,在上面这张图中经过了3轮处理后,还能继续找到更短距离,直到无穷小。

3. Floy-Warshall

package com.itheima.datastructure.graph;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Floyd - Warshall多源最短路径算法
 */
public class FloydWarshall {

    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");

        v1.edges = List.of(new Edge(v3, -2));
        v2.edges = List.of(new Edge(v1, 4), new Edge(v3, 3));
        v3.edges = List.of(new Edge(v4, 2));
        v4.edges = List.of(new Edge(v2, -1));
        List<Vertex> graph = List.of(v1, v2, v3, v4);

        // 负环情况
        /*Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");

        v1.edges = List.of(new Edge(v2, 2));
        v2.edges = List.of(new Edge(v3, -4));
        v3.edges = List.of(new Edge(v4, 1), new Edge(v1, 1));
        v4.edges = List.of();
        List<Vertex> graph = List.of(v1, v2, v3, v4);*/

        floydWarshall(graph);
    }

    private static void floydWarshall(List<Vertex> graph) {
        int size = graph.size();
        int[][] dist = new int[size][size];
        Vertex[][] prev = new Vertex[size][size];

        // 1. 初始化
        for (int i = 0; i < size; i++) {
            Vertex v = graph.get(i);
            Map<Vertex, Integer> map = v.edges.stream().collect(Collectors.toMap(e -> e.linked, e -> e.weight));
            for (int j = 0; j < size; j++) {
                Vertex u = graph.get(j);
                if (v == u) {
                    dist[i][j] = 0;
                } else {
                    dist[i][j] = map.getOrDefault(u, Integer.MAX_VALUE);
                    prev[i][j] = map.get(u) != null ? v : null;
                }
            }
        }
        // print(dist);
        print(prev);

        // 2. 看能否借路到达其他顶点
        /**
         * v2 -> v1
         * dist[1][0]
         */
        for (int k = 0; k < size; k++) {  // 借助第k个顶点到达其他顶点
            for (int i = 0; i < size; i++) {  // 第i行
                for (int j = 0; j < size; j++) {  // 第j列
                    // 第i行的顶点,借助第k个顶点,到达第j列顶点
                    // 可达且距离更小
                    if (dist[i][k] != Integer.MAX_VALUE &&
                            dist[k][j] != Integer.MAX_VALUE &&
                            dist[i][k] + dist[k][j] < dist[i][j]) {
                        dist[i][j] = dist[i][k] + dist[k][j];
                        prev[i][j] = prev[k][j];
                    }
                }
            }
            // 如果对角线上有一个值小于0,则出现了负环
            // print(dist);
        }
        print(prev);
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                path(prev, graph, i, j);
            }
        }
    }

    private static void print(int[][] dist) {
        System.out.println("---------------------");
        for (int[] row : dist) {
            System.out.println(Arrays.stream(row).boxed()
                    .map(x -> x == Integer.MAX_VALUE ? "∞" : String.valueOf(x))
                    .map(s -> String.format("%2s", s))
                    .collect(Collectors.joining(",", "[", "]")));
        }
    }

    private static void print(Vertex[][] prev) {
        System.out.println("---------------------");
        for (Vertex[] row : prev) {
            System.out.println(Arrays.stream(row).map(v -> v == null ? "null" : v.name)
                    .map(s -> String.format("%5s", s))
                    .collect(Collectors.joining(",", "[", "]")));
        }
    }

    private static void path(Vertex[][] prev, List<Vertex> graph, int i, int j) {
        LinkedList<String> stack = new LinkedList<>();
        System.out.println("[" + graph.get(i).name + "," + graph.get(j).name + "]");
        stack.push(graph.get(j).name);
        while(i != j) {
            Vertex p = prev[i][j];
            stack.push(p.name);
            j = graph.indexOf(p);
        }
        System.out.println(stack);
    }
}

八、最小生成树

1. Prim

Prim算法是一个用于寻找带权图的最小生成树的贪心算法,其基本思路如下:

1. 初始化:选择一个初始顶点,将其加入到生成树中,设置一个优先队列(或最小堆)来存储边,边的权重作为优先级。

2. 扩展树:

  • 从已加入生成树的顶点出发,找到与生成树中顶点相连的所有边,并选择其中权重最小的边
  • 将这条边的另一个顶点加入生成树,同时更新优先队列中与新加入顶点相连的边

3. 重复步骤:重复以上过程,直到所有顶点都被加入到生成树中。

时间复杂度:O(E log V),其中E是边的数量,V是顶点的数量。

package com.itheima.datastructure.graph;

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

/**
 * 最小生成树
 */
public class Prim {
    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");
        Vertex v7 = new Vertex("v7");

        v1.edges = List.of(new Edge(v2, 2), new Edge(v3, 4), new Edge(v4, 1));
        v2.edges = List.of(new Edge(v1, 2), new Edge(v4, 3), new Edge(v5, 10));
        v3.edges = List.of(new Edge(v1, 4), new Edge(v4, 2), new Edge(v6, 5));
        v4.edges = List.of(new Edge(v1, 1), new Edge(v2, 3), new Edge(v3, 2),
                new Edge(v5, 7), new Edge(v6, 8), new Edge(v7, 4));
        v5.edges = List.of(new Edge(v2, 10), new Edge(v4, 7), new Edge(v7, 6));
        v6.edges = List.of(new Edge(v3, 5), new Edge(v4, 8), new Edge(v7, 1));
        v7.edges = List.of(new Edge(v4, 4), new Edge(v5, 6), new Edge(v6, 1));

        List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7);

        prim(graph, v1);
    }

    private static void prim(List<Vertex> graph, Vertex source) {
        // 1. 创建一个未访问顶点的集合
        ArrayList<Vertex> list = new ArrayList<>(graph);
        // 2. 为初始顶点设置临时距离值为0
        source.dist = 0;

        while(!list.isEmpty()) {
            // 3. 选择最小临时距离的未访问顶点,作为新的当前顶点
            Vertex min = chooseMinDistVertex(list);
            // 4. 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小
            updateNeighboursDist(min);
            // 5. 当前顶点的邻居处理完成后,把它从未访问集合中删除
            list.remove(min);
            min.visited = true;
            System.out.println("------------------");
            for (Vertex v : graph) {
                System.out.println(v);
            }
        }
    }

    private static void updateNeighboursDist(Vertex curr) {
        for (Edge edge : curr.edges) {
            Vertex n = edge.linked;
            if(!n.visited) {
                int dist = edge.weight;
                if(dist < n.dist) {
                    n.dist = dist;
                    n.prev = curr;
                }
            }
        }
    }

    private static Vertex chooseMinDistVertex(ArrayList<Vertex> list) {
        Vertex min = list.get(0);
        for (int i = 1; i < list.size(); i++) {
            if(list.get(i).dist < min.dist) {
                min = list.get(i);
            }
        }
        return min;
    }
}

2. Kruskal

package com.itheima.datastructure.graph;

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

public class Kruskal {

    static class Edge implements Comparable<Edge> {
        List<Vertex> vertices;
        int start;
        int end;
        int weight;

        public Edge(List<Vertex> vertices, int start, int end, int weight) {
            this.vertices = vertices;
            this.start = start;
            this.end = end;
            this.weight = weight;
        }

        public Edge(int start, int end, int weight) {
            this.start = start;
            this.end = end;
            this.weight = weight;
        }

        @Override
        public int compareTo(Edge o) {
            return Integer.compare(this.weight, o.weight);
        }

        @Override
        public String toString() {
            return vertices.get(start).name + "<->" + vertices.get(end).name + "(" + weight + ")";
        }
    }

    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");
        Vertex v7 = new Vertex("v7");

        List<Vertex> vertices = List.of(v1, v2, v3, v4, v5, v6, v7);
        // 优先队列(小根堆) -> 根据边的权重比较
        PriorityQueue<Edge> queue = new PriorityQueue<>(List.of(
                new Edge(vertices,0, 1, 2),
                new Edge(vertices,0, 2, 4),
                new Edge(vertices,0, 3, 1),
                new Edge(vertices,1, 3, 3),
                new Edge(vertices,1, 4, 10),
                new Edge(vertices,2, 3, 2),
                new Edge(vertices,2, 5, 5),
                new Edge(vertices,3, 4, 7),
                new Edge(vertices,3, 5, 8),
                new Edge(vertices,3, 6, 4),
                new Edge(vertices,4, 6, 6),
                new Edge(vertices,5, 6, 1)
        ));

        kruskal(vertices.size(), queue);
    }

    private static void kruskal(int size, PriorityQueue<Edge> queue) {
        List<Edge> result = new ArrayList<>();
        DisjointSet set = new DisjointSet(size);

        // 小于顶点的个数减1 -> 还没找完
        while(result.size() < size - 1) {
            // 获取权重最小的边进行处理
            Edge poll = queue.poll();
            int start = set.find(poll.start);
            int end = set.find(poll.end);
            // 如果结果不相等,说明这两个顶点不相交 -> 不连通
            if(start != end) {
                result.add(poll);
                // 相交 -> 连通
                set.union(start, end);
            }
        }

        for (Edge edge : result) {
            System.out.println(edge);
        }
    }
}

九、不相交集合(并查集合)

使用并查集(Union-Find)来管理连通分量,从而有效地检查边的连通性。

package com.itheima.datastructure.graph;

import java.util.Arrays;

/**
 * 不相交集合(并查集合)
 */
public class DisjointSet {
    int[] s;  // 老大
    int[] size;  // 顶点个数

    public DisjointSet(int size) {
        s = new int[size];
        this.size = new int[size];
        /**
         * 索引对应顶点
         * 元素是用来表示与之有关系的顶点
         * 索引  0  1  2  3  4  5  6
         * 元素 [0, 1, 2, 3, 4, 5, 6] 表示一开始顶点直接没有联系(只与自己有联系)
         */
        for (int i = 0; i < size; i++) {
            s[i] = i;
            this.size[i] = 1;
        }
    }

    // find是找到集合中的老大 - 优化1:路径压缩
    public int find(int x) {
        // 索引和值相等的就是老大
        if(x == s[x]) {
            return x;
        }
        // 路径压缩
        return s[x] = find(s[x]);
    }

    // union是让两个集合“相交”,即选出新老大,x、y是原老大索引
    // 把顶点个数少的集合连接到顶点个数多的集合
    public void union(int x, int y) {
        if(size[x] < size[y]) {
            // y是新老大,x新小弟
            s[x] = y;
            // 更新老大的元素个数
            size[y] = size[y] + size[x];
        } else {
            // x是新老大,y新小弟
            s[y] = x;
            // 更新老大的元素个数
            size[x] = size[x] + size[y];
        }

        // 可以优化代码为:
        /*if(size[x] < size[y]) {
            int t = x;
            x = y;
            y = t;
        }
        s[y] = x;
        size[x] = size[x] + size[y];*/
    }

    @Override
    public String toString() {
        return "内容:" +  Arrays.toString(s) + "\n大小:" + Arrays.toString(size);
    }

    public static void main(String[] args) {
        DisjointSet set = new DisjointSet(7);
        System.out.println(set);

        int x = set.find(0);
        int y = set.find(3);
        System.out.println("老大分别是:" + x + " " + y);
        if(x != y) {
            set.union(x, y);
            System.out.println(set);
        }
    }
}

十、习题

1. 省份数量

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

示例 1:

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2

示例 2:

输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3

提示:

  • 1 <= n <= 200
  • n == isConnected.length
  • n == isConnected[i].length
  • isConnected[i][j] 为 1 或 0
  • isConnected[i][i] == 1
  • isConnected[i][j] == isConnected[j][i]

解法一:深度优先搜索DFS。时间复杂度O(n^2),与输入矩阵大小成正比,适合n的最大值为200

class Solution {

    public int findCircleNum(int[][] isConnected) {
        int n = isConnected.length;
        boolean[] visited = new boolean[n];
        int provinceCount = 0;

        for (int i = 0; i < n; i++) {
            if(!visited[i]) {
                // 在未访问的城市上进行DFS
                dfs(isConnected, visited, i);
                // 每遍历完一个省份,增加计数
                provinceCount++;
            }
        }
        return provinceCount;
    }

    private void dfs(int[][] isConnected, boolean[] visited, int city) {
        // 标记城市为已访问
        visited[city] = true;
        for (int j = 0; j < isConnected.length; j++) {
            // 如果有连接且问访问的城市,则继续DFS。结束时会访问完一个省份的所有城市
            if(isConnected[city][j] == 1 && !visited[j]) {
                dfs(isConnected, visited, j);
            }
        }
    }
}

解法二:广度优先搜索BFS。时间复杂度O(n^2),与输入矩阵大小成正比,适合n的最大值为200

class Solution {

    public int findCircleNum(int[][] isConnected) {
        int n = isConnected.length;
        boolean[] visited = new boolean[n];
        int provinceCount = 0;

        for (int i = 0; i < n; i++) {
            if (!visited[i]) {
                // 在未访问的城市上进行BFS
                bfs(isConnected, visited, i);
                // 每遍历完一个省份,增加计数
                provinceCount++;
            }
        }
        return provinceCount;
    }

    private void bfs(int[][] isConnected, boolean[] visited, int city) {
        LinkedList<Integer> queue = new LinkedList<>();
        queue.offer(city);
        // 标记城市为已访问
        visited[city] = true;

        while (!queue.isEmpty()) {
            // 取出队列中的城市
            int currentCity = queue.poll();
            for (int j = 0; j < isConnected.length; j++) {
                // 如果有连接且未访问,则加入队列
                if (isConnected[currentCity][j] == 1 && !visited[j]) {
                    visited[j] = true;
                    queue.offer(j);
                }
            }
        }
    }
}

解法三:并查集

class Solution {
    static class DisjionSet {
        private int[] parent;
        private int count;

        public DisjionSet(int n) {
            parent = new int[n];
            for (int i = 0; i < n; i++) {
                // 初始化每个城市的父节点为自身
                parent[i] = i;
            }
            count = n; // 初始时省份数量为城市数
        }

        public int find(int city) {
            if (parent[city] != city) {
                // 路径压缩
                parent[city] = find(parent[city]);
            }
            return parent[city];
        }

        public void union(int city1, int city2) {
            int root1 = find(city1);
            int root2 = find(city2);

            if (root1 != root2) {
                // 合并两个省份
                parent[root1] = root2;
                // 合并后省份数量减少
                count--;
            }
        }

        public int getCount() {
            return count;
        }
    }

    public int findCircleNum(int[][] isConnected) {
        int n = isConnected.length;
        DisjionSet set = new DisjionSet(n);

        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                // 如果两个城市连接,则合并
                if (isConnected[i][j] == 1) {
                    set.union(i, j);
                }
            }
        }

        return set.getCount();
    }
}

2. 所有可能路径

给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序

 graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)。

示例 1:

输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3

示例 2:

输入:graph = [[4,3,1],[3,2,4],[3],[4],[]]
输出:[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]

提示:

  • n == graph.length
  • 2 <= n <= 15
  • 0 <= graph[i][j] < n
  • graph[i][j] != i(即不存在自环)
  • graph[i] 中的所有元素 互不相同
  • 保证输入为 有向无环图(DAG)

解法一:深度优先搜索DFS

class Solution {
    public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        path.add(0);
        // 从节点0开始深度优先搜索
        dfs(graph, 0, path, result);

        return result;
    }

    private void dfs(int[][] graph, int node, List<Integer> path, List<List<Integer>> result) {
        // 到了第n - 1个节点
        if (node == graph.length - 1) {
            result.add(new ArrayList<>(path));
            return;
        }

        for (int neighbor : graph[node]) {
            path.add(neighbor);
            // 从相邻节点继续递归
            dfs(graph, neighbor, path, result);
            path.remove(path.size() - 1); // 回溯
        }
    }
}

解法二:广度优先搜索BFS

class Solution {
    public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
        List<List<Integer>> result = new ArrayList<>();
        Queue<List<Integer>> queue = new LinkedList<>();

        List<Integer> startPath = new ArrayList<>();
        startPath.add(0);
        queue.offer(startPath);

        while (!queue.isEmpty()) {
            List<Integer> currentPath = queue.poll();
            // 获取某条路径的最后一个节点
            int lastNode = currentPath.get(currentPath.size() - 1);

            if (lastNode == graph.length - 1) {
                result.add(new ArrayList<>(currentPath));
            }

            for (int neighbor : graph[lastNode]) {
                // 在当前路径基础上添加新的节点
                List<Integer> newPath = new ArrayList<>(currentPath);
                newPath.add(neighbor);
                queue.offer(newPath);
            }
        }

        return result;
    }
}

3. 连接所有点的最小费用

给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。

连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。

请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。

示例 1:

输入:points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
输出:20
解释:

我们可以按照上图所示连接所有点得到最小总费用,总费用为 20 。
注意到任意两个点之间只有唯一一条路径互相到达。

示例 2:

输入:points = [[3,12],[-2,5],[-4,1]]
输出:18

示例 3:

输入:points = [[0,0],[1,1],[1,0],[-1,1]]
输出:4

示例 4:

输入:points = [[-1000000,-1000000],[1000000,1000000]]
输出:4000000

示例 5:

输入:points = [[0,0]]
输出:0

提示:

  • 1 <= points.length <= 1000
  • -10^6 <= xi, yi <= 10^6
  • 所有点 (xi, yi) 两两不同。

解法一:最小生成树Prim

class Solution {
    public int minCostConnectPoints(int[][] points) {
        int n = points.length;
        if (n == 1) {
            return 0;
        }
        boolean[] inMST = new boolean[n]; // 是否在最小生成树中
        int[] minWeight = new int[n];
        Arrays.fill(minWeight, Integer.MAX_VALUE);

        // 从第一个节点开始
        minWeight[0] = 0; // 初始化第一个点的权重为0
        int totalCost = 0;

        for (int i = 0; i < n; i++) {
            // 找到下一个未包含在MST中 且 权重最小的点
            int u = -1;
            for (int j = 0; j < n; j++) {
                if (!inMST[j] && (u == -1 || minWeight[j] < minWeight[u])) {
                    u = j;
                }
            }

            // 将该点加入到MST中
            inMST[u] = true;
            totalCost += minWeight[u];

            // 更新其他点的最小权重
            for (int v = 0; v < n; v++) {
                if (!inMST[v]) {
                    // u到v的权重
                    int weight = manhattanDistance(points[u], points[v]);
                    // 更新点v的权重
                    minWeight[v] = Math.min(minWeight[v], weight);
                }
            }
        }

        return totalCost;
    }

    /**
     * 计算曼哈顿距离
     * 
     * @param p1 连接点1
     * @param p2 连接点2
     * @return
     */
    private int manhattanDistance(int[] p1, int[] p2) {
        return Math.abs(p1[0] - p2[0]) + Math.abs(p1[1] - p2[1]);
    }
}

解法二:Kruskal算法

class Solution {
    // 边的类
    static class Edge implements Comparable<Edge> {
        private int src, dest, weight;

        public Edge(int src, int dest, int weight) {
            this.src = src;
            this.dest = dest;
            this.weight = weight;
        }

        @Override
        public int compareTo(Edge o) {
            return Integer.compare(this.weight, o.weight);
        }
    }

    static class DisjointSet {
        int[] parent, rank;

        public DisjointSet(int n) {
            parent = new int[n];
            rank = new int[n];
            for (int i = 0; i < n; i++) {
                parent[i] = i;
                rank[i] = 0;
            }
        }

        public int find(int u) {
            if (parent[u] != u) {
                parent[u] = find(parent[u]);
            }
            return parent[u];
        }

        public void union(int u, int v) {
            int rootU = find(u);
            int rootV = find(v);
            if (rootU != rootV) {
                if (rank[rootU] > rank[rootV]) {
                    parent[rootV] = rootU;
                } else if (rank[rootU] < rank[rootV]) {
                    parent[rootU] = rootV;
                } else {
                    parent[rootV] = rootU;
                    rank[rootU]++;
                }
            }
        }
    }

    public int minCostConnectPoints(int[][] points) {
        int n = points.length;
        List<Edge> edges = new ArrayList<>();

        // 创建所有边
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                int weight = manhattanDistance(points[i], points[j]);
                edges.add(new Edge(i, j, weight));
            }
        }
        // 按边权重排序
        Collections.sort(edges);

        DisjointSet set = new DisjointSet(n);
        int totalCost = 0;
        for (Edge edge : edges) {
            if (set.find(edge.src) != set.find(edge.dest)) {
                set.union(edge.src, edge.dest);
                totalCost += edge.weight;
            }
        }

        return totalCost;
    }

    /**
     * 计算曼哈顿距离
     * 
     * @param p1 连接点1
     * @param p2 连接点2
     * @return
     */
    private int manhattanDistance(int[] p1, int[] p2) {
        return Math.abs(p1[0] - p2[0]) + Math.abs(p1[1] - p2[1]);
    }
}

4. 网络延迟时间

有 n 个网络节点,标记为 1 到 n

给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。

现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 

示例 1:

输入:times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
输出:2

示例 2:

输入:times = [[1,2,1]], n = 2, k = 1
输出:1

示例 3:

输入:times = [[1,2,1]], n = 2, k = 2
输出:-1

提示:

  • 1 <= k <= n <= 100
  • 1 <= times.length <= 6000
  • times[i].length == 3
  • 1 <= ui, vi <= n
  • ui != vi
  • 0 <= wi <= 100
  • 所有 (ui, vi) 对都 互不相同(即,不含重复边)

解法一:最短路径 - Dijkstra

class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {
        // 创建一个邻接表来表示图
        // 每个节点映射到它所有邻接节点及其相应的传递时间
        Map<Integer, List<int[]>> graph = new HashMap<>();

        for (int[] time : times) {
            // 先加入key -> ui
            graph.putIfAbsent(time[0], new ArrayList<>());
            // 后加入value -> vi wi
            graph.get(time[0]).add(new int[] { time[1], time[2] });
        }

        // 小根堆优先队列 跟踪当前最短路径的节点及其到达时间
        PriorityQueue<int[]> minHead = new PriorityQueue<>(Comparator.comparing(a -> a[1]));
        minHead.offer(new int[] { k, 0 }); // {节点, 时间}

        // 跟踪到达每个节点的最短时间
        Map<Integer, Integer> minTime = new HashMap<>();
        minTime.put(k, 0); // 节点,时间

        // Dijkstra算法
        // 从节点k开始,逐步扩展到邻接节点,更新每个节点的最短传递时间
        while (!minHead.isEmpty()) {
            int[] current = minHead.poll();
            int node = current[0];
            int time = current[1];

            // 遍历当前节点的所有邻接节点
            if (graph.containsKey(node)) {
                for (int[] neighbor : graph.get(node)) {
                    int nextNode = neighbor[0];
                    int travelTime = neighbor[1];

                    // 计算到达邻接节点的时间
                    int newTime = time + travelTime;

                    // 如果通过当前节点到达某个邻接节点的时间比已知的时间更短,则更新时间并将该邻接节点加入优先级队列
                    if (newTime < minTime.getOrDefault(nextNode, Integer.MAX_VALUE)) {
                        minTime.put(nextNode, newTime);
                        minHead.offer(new int[] { nextNode, newTime });
                    }
                }
            }
        }

        // 找到所有节点的最大时间
        int maxTime = 0;
        for (int i = 1; i <= n; i++) {
            // 如果存在未到达的节点,则返回-1
            if (!minTime.containsKey(i)) {
                return -1;
            }
            maxTime = Math.max(maxTime, minTime.get(i));
        }

        return maxTime;
    }
}

5. k站中转内最便宜的航班

有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi

现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1

示例 1:

输入: 
n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1
输出: 700 
解释: 城市航班图如上
从城市 0 到城市 3 经过最多 1 站的最佳路径用红色标记,费用为 100 + 600 = 700。
请注意,通过城市 [0, 1, 2, 3] 的路径更便宜,但无效,因为它经过了 2 站。

示例 2:

输入: 
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1
输出: 200
解释: 
城市航班图如上
从城市 0 到城市 2 经过最多 1 站的最佳路径标记为红色,费用为 100 + 100 = 200。

示例 3:

输入:n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 0
输出:500
解释:
城市航班图如上
从城市 0 到城市 2 不经过站点的最佳路径标记为红色,费用为 500。

提示:

  • 1 <= n <= 100
  • 0 <= flights.length <= (n * (n - 1) / 2)
  • flights[i].length == 3
  • 0 <= fromi, toi < n
  • fromi != toi
  • 1 <= pricei <= 10^4
  • 航班没有重复,且不存在自环
  • 0 <= src, dst, k < n
  • src != dst

解法一:最短路径Dijkstra。执行耗时13ms

class Solution {
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        // 使用邻接表表示图
        List<List<int[]>> graph = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            graph.add(new ArrayList<>());
        }

        // 填充图
        for (int[] flight : flights) {
            graph.get(flight[0]).add(new int[] { flight[1], flight[2] });
        }

        // 小根堆,元素为 {当前花费,当前城市,当前站数}
        PriorityQueue<int[]> minHeap = new PriorityQueue<>(Comparator.comparing(a -> a[0]));
        minHeap.offer(new int[] { 0, src, 0 });

        // 记录到达每个城市的最小费用,并且要注意站数的限制
        int[][] minCost = new int[n][k + 2]; // k + 2以考虑k站和出发城市

        // 初始化为最大值
        for (int i = 0; i < n; i++) {
            Arrays.fill(minCost[i], Integer.MAX_VALUE);
        }
        minCost[src][0] = 0; // 从src开始,费用为0

        // Dijkstra核心逻辑
        while (!minHeap.isEmpty()) {
            int[] current = minHeap.poll();
            int cost = current[0]; // 当前花费
            int city = current[1]; // 当前城市
            int stops = current[2]; // 当前站数

            // 如果当前城市是目的地且经过站数不超过k
            if (city == dst) {
                return cost;
            }

            // 如果经过的站数已经达到k+1,跳过
            if (stops > k) {
                continue;
            }

            // 遍历当前城市的所有邻接城市
            for (int[] neighbor : graph.get(city)) {
                int nextCity = neighbor[0]; // 目标城市
                int price = neighbor[1]; // 价格
                int newCost = cost + price; // 新的总费用

                // 如果新费用低于当前记录,更新并添加到优先队列
                if (newCost < minCost[nextCity][stops + 1]) {
                    minCost[nextCity][stops + 1] = newCost;
                    minHeap.offer(new int[] { newCost, nextCity, stops + 1 });
                }
            }
        }
        // 如果没有找到有效路径,返回-1
        return -1;
    }
}

解法二:最短路径 - Bellman-Ford。执行耗时5ms

    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        // 初始化源城市的最小费用为0,其他城市为无穷大
        int[] prices = new int[n];
        Arrays.fill(prices, Integer.MAX_VALUE);
        prices[src] = 0;

        // 最多迭代k+1次
        for (int i = 0; i <= k; i++) {
            int[] tempPrices = Arrays.copyOf(prices,n);
            // 遍历所有的边
            for (int[] flight : flights) {
                int u = flight[0], v = flight[1], cost = flight[2];
                if(prices[u] == Integer.MAX_VALUE) {
                    // 无法到达城市u
                    continue;
                }
                // 更新到达每个城市的最小费用
                tempPrices[v] = Math.min(tempPrices[v], prices[u] + cost);
            }
            prices = tempPrices;
        }
        
        return prices[dst] == Integer.MAX_VALUE ? -1 : prices[dst];
    }

6. 课程表

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程  bi 。

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites[i] 中的所有课程对 互不相同

解法一:拓扑排序

使用入度和队列来实现拓扑排序。每次从队列中取出一个课程,并将其对应的后置课程的入度减一,如果某个课程的入度减至0,说明可以上这门课程。

class Solution {
    // 等同于在判断有向图是否存在环 -> 拓扑排序
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        int[] inDegree = new int[numCourses];
        List<List<Integer>> graph = new ArrayList<>();

        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }

        // 处理节点的先后关系
        for (int[] pre : prerequisites) {
            // 要想修读pre[0],先修pre[1] pre[1] -> pre[0]
            graph.get(pre[1]).add(pre[0]);
            // 后置课程的入度加1
            inDegree[pre[0]]++;
        }

        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        int courseCompleted = 0;
        // 节点出队
        while (!queue.isEmpty()) {
            int course = queue.poll();
            courseCompleted++;
            for (Integer neighbor : graph.get(course)) {
                // 节点出队后,后置课程的入度减1
                inDegree[neighbor]--;
                if (inDegree[neighbor] == 0) {
                    queue.offer(neighbor);
                }
            }
        }

        return numCourses == courseCompleted;
    }
}

解法二:深度优先搜索DFS + 递归标记

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }
        // 处理图中节点的先后关系
        for (int[] pre : prerequisites) {
            graph.get(pre[1]).add(pre[0]);
        }

        // 维护一个状态数组跟踪每个课程的访问状态 0-未访问 1-正在访问 2-已访问
        int[] visited = new int[numCourses];
        
        for (int i = 0; i < numCourses; i++) {
            // 判断是否存在环,如果存在环,则说明无法完成所有可能(不存在拓扑排序)
            if(hasCycle(graph, visited, i)) {
                return false;
            }
        }

        return true;
    }

    // 如果在DFS遍历的过程中再次访问到正在访问的节点,则说明存在环,无法完成所有的课程
    private boolean hasCycle(List<List<Integer>> graph, int[] visited, int node) {
        if(visited[node] == 1) return true;  // 当前路径中存在环
        if(visited[node] == 2) return false; // 已经访问过了,无需再遍历

        visited[node] = 1;  // 正在访问
        for (Integer neighbor : graph.get(node)) {
            // 递归
            if(hasCycle(graph, visited, neighbor)) {
                return true;
            }
        }
        visited[node] = 2;  // 标记为已访问
        return false;
    }

7. 课程表Ⅱ

现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。

  • 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。

返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

示例 2:

输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

示例 3:

输入:numCourses = 1, prerequisites = []
输出:[0]

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= numCourses * (numCourses - 1)
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • ai != bi
  • 所有[ai, bi] 互不相同

解法一:拓扑排序(入度+队列)。执行耗时7ms

class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        List<Integer> order = new ArrayList<>();
        int[] inDegree = new int[numCourses];
        List<List<Integer>> graph = new ArrayList<>();

        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }

        // 处理节点的先后关系
        for (int[] pre : prerequisites) {
            // 要想修读pre[0],先修pre[1] pre[1] -> pre[0]
            graph.get(pre[1]).add(pre[0]);
            // 后置课程的入度加1
            inDegree[pre[0]]++;
        }

        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        // 节点出队
        while (!queue.isEmpty()) {
            int course = queue.poll();
            order.add(course);
            for (Integer neighbor : graph.get(course)) {
                // 节点出队后,后置课程的入度减1
                inDegree[neighbor]--;
                if (inDegree[neighbor] == 0) {
                    queue.offer(neighbor);
                }
            }
        }
        return order.size() == numCourses ? order.stream().mapToInt(i -> i).toArray() : new int[0];
    }
}

解法二:深度优先搜索DFS。执行耗时5ms

class Solution {
    private List<List<Integer>> graph;
    private int[] visited;
    private List<Integer> order;

    public int[] findOrder(int numCourses, int[][] prerequisites) {
        // 初始化图和状态数组
        graph = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }
        visited = new int[numCourses];
        order = new ArrayList<>();

        // 构建图
        for (int[] pre : prerequisites) {
            graph.get(pre[1]).add(pre[0]);
        }

        // DFS 遍历每门课程
        for (int i = 0; i < numCourses; i++) {
            if (visited[i] == 0) {
                if (!dfs(i)) {
                    return new int[0]; // 返回空数组表示无法完成所有课程
                }
            }
        }

        // 结果反转
        Collections.reverse(order);
        return order.stream().mapToInt(i -> i).toArray();
    }

    private boolean dfs(int course) {
        if (visited[course] == 1) { // 发现环
            return false;
        }
        if (visited[course] == 2) { // 已访问
            return true;
        }

        // 标记为正在访问
        visited[course] = 1;

        for (int neighbor : graph.get(course)) {
            if (!dfs(neighbor)) {
                return false;
            }
        }

        // 标记为已访问,并加入结果
        visited[course] = 2;
        order.add(course);
        return true;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值