图算法合集

Evan Zheng | Jun 3, 2024 min read

Prim算法

Prim算法是一种用于构造最小生成树(Minimum Spanning Tree,MST)的贪心算法。在给定一个加权无向连通图的情况下,最小生成树是图的一个子图,它连接了所有的顶点而不形成任何循环,并且具有可能的最小的边权重总和。

Prim算法的工作原理是从图中的任一顶点开始,逐渐长大覆盖整个图,每一步都添加最小权重的边,直到所有的顶点都被覆盖。

例子:

1168. 水资源分配优化

村里面一共有 n 栋房子。我们希望通过建造水井和铺设管道来为所有房子供水。 对于每个房子 i,我们有两种可选的供水方案:一种是直接在房子内建造水井,成本为 wells[i - 1] (注意 -1 ,因为 索引从0开始 );另一种是从另一口井铺设管道引水,数组 pipes 给出了在房子间铺设管道的成本,其中每个 pipes[j] = [house1j, house2j, costj] 代表用管道将 house1j 和 house2j连接在一起的成本。连接是双向的。 请返回 为所有房子都供水的最低总成本。

Prim 算法 在加权无向图中找到最小生成树 从一个任意顶点开始,Prim 算法通过每次添加一个顶点到树中增长最小生成树。 顶点的选择基于贪婪策略,即添加新的顶点会产生最小的成本。

image-20240603121425557

本题与建立最小生成树的差别:房子可以自己打水井。

通过添加节点0为虚拟水井,可以将房子自己打水井转换成和节点0之间建立连边:

image-20240603121910886

需要用到:PriorityQueue 最小堆List<List<Pair<Integer, Integer»> 邻接表表示图, HashSet记录已加入生成树的节点。

class Solution {
    public int minCostToSupplyWater(int n, int[] wells, int[][] pipes) {
        PriorityQueue<Pair<Integer, Integer>> minHeap = new PriorityQueue<>(n, (a,b) -> (a.getKey() - b.getKey()));
        // key: 连接到树的成本 value: 房子的编号
        List<List<Pair<Integer, Integer>>> graph = new ArrayList<>();
        for (int i = 0; i < n + 1; i++) {
            graph.add(new ArrayList<Pair<Integer, Integer>>());
        }
        // 0 号索引 虚拟水井,将 house 本身挖水井转换成和 0 号水井建立连接的边
        for (int i = 0; i < n; i++) {
            graph.get(0).add(new Pair<>(wells[i], i + 1));
            minHeap.add(new Pair<>(wells[i], i + 1));
        }
        for (int i = 0; i < pipes.length; i++) {
            int house1 = pipes[i][0];
            int house2 = pipes[i][1];
            int cost = pipes[i][2];
            graph.get(house1).add(new Pair<Integer, Integer>(cost, house2));
            graph.get(house2).add(new Pair<Integer, Integer>(cost, house1));
        }

        Set<Integer> connected = new HashSet<>();
        connected.add(0);

        int totalCost = 0;
        while (connected.size() < n + 1) {
            Pair<Integer, Integer> edge = minHeap.poll();
            int cost = edge.getKey();
            int house = edge.getValue();
            if (connected.contains(house)) continue;
            connected.add(house);
            totalCost += cost;
            for (Pair<Integer, Integer> p : graph.get(house)) {
                if (!connected.contains(p.getValue()))
                    minHeap.add(p);
            }
        }
        return totalCost;
    }
}

复杂度分析:

时间复杂度: \(O((N+M)⋅log⁡(N+M))\)

构建图,我们迭代了输入的房屋和管道,这需要 \(O(N + M)\) 的时间。 当构建 MST 时,在最坏的情况下,我们可能需要迭代图中的所有边,总数为 \(N + M\) 。 对于每个边,它最多进入和退出堆数据结构一次。把边放入堆中 (push 操作) 需要 \(log(N+M)\) 的时间,而退出边 ( pop 操作) 需要一个常数时间。 因此,MST 构建过程的时间复杂度是 \(O((N+M)⋅log⁡(N+M))\)。 总的来说,算法的总体时间复杂度是 $$O((N+M)⋅log⁡(N+M)).$$

参考:力扣(LeetCode)

Kruskal算法

Kruskal算法是一种用于在加权图中找到最小生成树(Minimum Spanning Tree, MST)的算法。它适用于边的权重各不相同的连通图。Kruskal算法是基于贪心策略实现的,其主要思想是按照边的权重顺序(从小到大)处理每条边,并在不形成环的前提下逐步添加到最小生成树中。

Kruskal算法的步骤:

  1. 排序所有边: 将图中所有的边按照权重从小到大排序。可以使用任何有效的排序算法来完成这一步,如归并排序、快速排序等。
  2. 初始化并查集: 使用并查集数据结构来维护和查询顶点间的连通性。初始时,每个顶点都处于不同的集合中。
  3. 选择最小边: 从排序后的边列表中选择最小的边。检查这条边连接的两个顶点是否已经在同一个集合中(即检查是否会形成环):
    • 如果不会形成环(即两个顶点不在同一个集合中),则将这条边添加到最小生成树中,并使用并查集合并这两个顶点所在的集合。
    • 如果会形成环,则丢弃这条边,不将其加入最小生成树。
  4. 重复选择过程: 重复第3步,直到添加了 V−1 条边到最小生成树中,其中 V 是图中顶点的数量。在连通图中,最小生成树恰好包含 V-1 条边。
  5. 完成构建: 当添加了足够的边(V-1 条)后,算法完成,此时构建的就是整个图的最小生成树。
class Solution {
    private static final int MAX_N = 10005;

    private int[] f = new int[MAX_N];

    private void init() {
        for (int i = 0; i < MAX_N; ++i) {
            f[i] = i;
        }
    }

    private int find(int x) {
        return x == f[x] ? x : (f[x] = find(f[x]));
    }

    private void merge(int u, int v) {
        f[find(u)] = find(v);
    }

    public int minCostToSupplyWater(int n, int[] wells, int[][] pipes) {
        init();
        List<int[]> edges = new ArrayList<>();
        for (int i = 1; i <= n; ++i) {
            edges.add(new int[] { 0, i, wells[i - 1] });
        }
        for (int[] pipe : pipes) {
            edges.add(pipe);
        }
        edges.sort(Comparator.comparingInt(a -> a[2]));
        int ret = 0;
        for (int[] edge : edges) {
            if (find(edge[0]) != find(edge[1])) {
                merge(edge[0], edge[1]);
                ret += edge[2];
            }
        }
        return ret;
    }
}

Dijkstra算法

用于解决图中的单源最短路径问题。这个算法可以找到一个顶点到图中其他所有顶点的最短路径。它适用于包含非负权重边的图。

初始化

  • 选择图中的一个源点。
  • 初始化一个距离数组,其中源点到自己的距离设为0,到其他所有点的距离设为无穷大(或一个非常大的数)。
  • 使用一个优先队列(最小堆)来存储所有顶点的距离,优先队列按距离排序。

处理每个顶点

  • 从优先队列中取出当前距离最短的顶点(初始时是源点)。
  • 对于这个顶点的每个邻接顶点,检查通过当前顶点到邻接顶点的路径是否比已知的路径更短。如果更短,则更新邻接顶点的最短路径值,并在优先队列中更新这个顶点的位置。

重复执行

  • 重复步骤2直到优先队列为空,即所有可达顶点的最短路径都已找到。

完成

  • 算法完成后,距离数组中存储的就是源点到图中每个顶点的最短距离

如果使用数组,时间复杂度是 O(V^2),其中 V 是顶点数。

如果使用二叉堆作为优先队列,时间复杂度可以优化到 O((V+E) \log V),其中 E 是边的数量。

例题:

// leetcode743. 网络延迟时间:
//    ┌─┐  1 ┌─┐
//    │2├───►│3│
//    └┬┘    └┬┘
//     │      │ 
// 1   │      │ 1
//     ▼      ▼
//    ┌─┐    ┌─┐
//    │1│    │4│
//    └─┘    └─┘
// 从2传递到所有节点,需要的时间:2
输入:times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
输出:2
class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {
        // build the graph
        List<int[]>[] graph = new List[n + 1];
        for (int i = 0; i <= n; i++) {
            graph[i] = new ArrayList<int[]>();
        }
        int[] delays = new int[n + 1];
        Arrays.fill(delays, Integer.MAX_VALUE);
        delays[k] = 0;
        // 注意数据初始化
        for (int[] time : times) {
            int start = time[0];
            int end = time[1];
            int latency = time[2];
            graph[start].add(new int[]{end, latency});
        }

        //利用pq就不用维护visited数组了,直接bfs
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a,b) -> {return a[1] - b[1];});
        pq.offer(new int[]{k, 0});
        while (!pq.isEmpty()) {
            int[] cur = pq.poll();
            int cur_id = cur[0];
            int cur_dist = cur[1];
            if (cur_dist > delays[cur_id]) continue;
            for (int[] neighbor : graph[cur_id]) {
                int delay_to_next = neighbor[1] + delays[cur_id];
                if (delay_to_next < delays[neighbor[0]]) {
                    delays[neighbor[0]] = delay_to_next;
                    pq.offer(new int[]{neighbor[0], delay_to_next});
                }
            }
        }
        int temp = Integer.MIN_VALUE;
        for (int i = 1; i <= n; i++) {
            if (delays[i] == Integer.MAX_VALUE) {
                return -1;
            }
            temp = Math.max(temp, delays[i]);
        }
        return temp;
    }
}