【最短路径】Dijkstra算法


Dijkstra算法

给定一个源顶点 \(s\) 从一组顶点 \(V\) 在加权有向图中,其中所有边权重 \(w(u, v)\) 均是非负的,找到最短路径权重 \(d(s, v)\) 从源头 \(s\) 对于所有顶点 \(v\) 出现在\(Graph\)中。

\(Dijkstra\) 算法是一种求解非负权图单源最短路径的算法。

算法分析

我们以下图为例,计算起点 \(A\) 到每个顶点的最短距离:

image

我们先定义一个 \(distances[5]\) 数组,用于记录起点到所有顶点的距离,这里我们假设数组中的元素序号 0 ~ 4 分别表示顶点 A ~ E ,将数组中的所有元素都初始化为无效值(或者无穷大),即:

\[distances[i] = MAX \]

初始条件

由于,我们从顶点 \(A\) 开始,所以顶点 \(A\) 的距离为 \(0\),即 \(distances[0] = 0\)

image

第一次查找

我们用\(S\)记录最短路径,从顶点A开始计算A的邻居节点B,E,由于B可以通过A直接到达,我们暂时记录 \(S_{AB} = 10\),,同理,顶点A到顶点E的距离 \(S_{AE} = 3\),即

\[distance[1] = 10, \quad distance[4] = 3 \]

image

第二次查找

我们考虑,从 \(\{S_{AB},\ S_{AE}\}\) 中选择权重最小的边 \(S_{AE}\)作为路径,继续查找:

image

第三次查找

image

第四次查找

image

第五次查找

image

第六次查找

image


代码模板

这里我们直接给出代码模板:

import heapq
from typing import List, Tuple

class NodeState(object):
    def __init__(self, node_id: int, distance: int = 0):
        # 节点id
        self._id = node_id
        # 从起点到当前节点的距离
        self._distance = distance

    def get_id(self):
        return self._id

    def get_distance(self):
        return self._distance

    def __lt__(self, other):
        """ 小的节点在堆顶 """
        return self.get_distance() - other.get_distance()


def dijkstra(start: int, graph: List[List[Tuple[int]]]):
    """ 单源最短路径:计算从起点到每个节点的距离
    :param start: 起点
    :param graph: 邻接表
    :return:
    """
    distances = [float("INF")] * len(graph)
    distances[start] = 0
    # 使用优先级队列保存所有的节点,保证堆顶的元素最小
    queue = list()
    heapq.heappush(queue, NodeState(start, 0))
    while len(queue) != 0:
        current_stat = heapq.heappop(queue)
        if current_stat.get_distance() > distances[current_stat.get_id()]:
            continue

        for neighbor in graph[current_stat.get_id()]:
            # 当前节点的序号
            _id = neighbor[0]
            # 起点到当前节点的距离
            distance = distances[current_stat.get_id()] + neighbor[1]
            if distance < distances[_id]:
                distances[_id] = distance
                heapq.heappush(queue, NodeState(_id, distance))

    return distances

应用

应用 1:Leetcode.743

题目

743. 网络延迟时间

有 n 个网络节点,标记为 1 到 n。
给你一个列表 times,表示信号经过 有向 边的传递时间。 \(times[i] = (u_i, v_i, w_i)\),其中 \(u_i\) 是源节点,\(v_i\) 是目标节点, \(w_i\) 是一个信号从源节点传递到目标节点的时间。
现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

示例 1:

image

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

题目分析

这是一道典型的求最短路径的算法:加权有向图,没有负权重边,求最短路径。

我们将传播时间看成边\(V\)的权重,我们用邻接表来表示有向加权图,最后直接利用\(Dijkstra\)算法,计算出从起点到终点的每一个节点的最短传播路径即可。

这里我们直接给出 \(Dijkstra\) 算法的模板,套用模板即可。

需要注意:题目中的节点序号是从 \(1\) 开始的,所以我们生成邻接矩阵的时候,将所有的节点序号都减一,保证节点序号从 \(0\) 开始。

代码实现

【JAVA实现】:

class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {
        List<List<Node>> graph = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            graph.add(new ArrayList<>());
        }

        for (int[] time : times) {
            graph.get(time[0] - 1).add(new Node(time[1] - 1, time[2]));
        }

        int[] distances = dijkstra(k - 1, graph);
        int result = Integer.MIN_VALUE;
        for (int distance : distances) {
            result = Math.max(result, distance);
        }

        return result == Integer.MAX_VALUE ? -1 : result;
    }

    private int[] dijkstra(int start, List<List<Node>> graph) {
        int[] distances = new int[graph.size()];
        Arrays.fill(distances, Integer.MAX_VALUE);

        distances[start] = 0;
        PriorityQueue<Node> queue = new PriorityQueue<>((a, b) -> b.getDistance() - a.getDistance());
        queue.add(new Node(start, 0));
        while (!queue.isEmpty()) {
            Node currentNode = queue.poll();
            int u = currentNode.getId();
            if (currentNode.getDistance() > distances[u]) {
                continue;
            }

            for (Node neighbor : graph.get(u)) {
                int v = neighbor.getId();
                int distance = distances[u] + neighbor.getDistance();
                if (distance < distances[v]) {
                    distances[v] = distance;
                    queue.offer(new Node(v, distance));
                }
            }
        }
        return distances;
    }

    private static class Node {
        private final int id;
        private final int distance;

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

        public int getId() {
            return id;
        }

        public int getDistance() {
            return distance;
        }
    }
}

【Python实现】:

import heapq
from typing import List

class Solution:
    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        graph = [list() for _ in range(n)]
        for _time in times:
            graph[_time[0] - 1].append((_time[2], _time[1] - 1))

        distances = self.dijkstra(graph, k - 1)
        result = max(distances) if max(distances) < float("INF") else -1
        return int(result)

    def dijkstra(self, graph, start):
        distances = [float("INF")] * len(graph)
        distances[start] = 0
        queue = list()
        heapq.heappush(queue, (0, start))
        while queue:
            _node = heapq.heappop(queue)
            if distances[_node[1]] < _node[0]:
                continue

            for neighbor in graph[_node[1]]:
                distance = _node[0] + neighbor[0]
                if distance < distances[neighbor[1]]:
                    distances[neighbor[1]] = distance
                    heapq.heappush(queue, (distance, neighbor[1]))
        return distances

应用 2:Leetcode.1514

题目

1514. 概率最大的路径

给你一个由 n 个节点(下标从 0 开始)组成的无向加权图,该图由一个描述边的列表组成,其中 edges[i] = [a, b] 表示连接节点 a 和 b 的一条无向边,且该边遍历成功的概率为 succProb[i] 。
指定两个节点分别作为起点 start 和终点 end ,请你找出从起点到终点成功概率最大的路径,并返回其成功概率。如果不存在从 start 到 end 的路径,请 返回 0 。只要答案与标准答案的误差不超过 1e-5 ,就会被视作正确答案。

示例 1:

image

输入:n = 3, edges = [[0,1],[1,2],[0,2]], succProb = [0.5,0.5,0.2], start = 0, end = 2
输出:0.25000
解释:从起点到终点有两条路径,其中一条的成功概率为 0.2 ,而另一条为 0.5 * 0.5 = 0.25

解题思路

这道题的特点:边有非负的权重,求单源最优路径

可以转换为 \(Dijkstra\) 算法求解,\(Dijkstra\) 算法是为了求解单源最短路径,其实只要是图的路径具有非负权重,求单源最优路径,都可以通过 \(Dijsktra\) 算法求解,只是将路径累加换成概率相乘即可。

题目中的图是无向图,我们可以将无向图的边可以看成是双向的边即可。

首先,我们首先将图中的边转换为邻接表 \(graph\) ,主要保存每个顶点的邻近节点及其概率:

\[graph[i] = (v,\ probability) \]

我们用一个优先级队列 \(queue\) 保存每个节点的概率及其节点序号:

\[queue[j] = (probability, v) \]

然后,从起点开始更新邻近节点的概率,最后返回终点的概率即可。

代码实现

class Solution {
    public double maxProbability(int n, int[][] edges, double[] successProb, int start_node, int end_node) {
        List<List<Node>> graph = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            graph.add(new ArrayList<>());
        }

        // 使用邻接矩阵构造无向图
        for (int i = 0; i < successProb.length; i++) {
            graph.get(edges[i][0]).add(new Node(edges[i][1], successProb[i]));
            graph.get(edges[i][1]).add(new Node(edges[i][0], successProb[i]));
        }
        double[] distances = dijkstra(start_node, graph, n);
        return distances[end_node] == Integer.MAX_VALUE ? 0 : distances[end_node];
    }

    private double[] dijkstra(int start, List<List<Node>> graph, int n) {
        // 初始每个节点的概率为零
        double[] distances = new double[n];
        // 大顶堆
        PriorityQueue<Node> q = new PriorityQueue<>((a, b) -> Double.compare(b.getDistance(), a.getDistance()));
        // 起点的概率设置为 1
        distances[start] = 1;
        q.offer(new Node(start, 1));
        while (!q.isEmpty()) {
            Node candidate = q.poll();
            int u = candidate.getId();
            // 如果堆顶的元素的权重更小,就跳过
            if (candidate.getDistance() < distances[u]) {
                continue;
            }
            // 遍历邻近节点
            for (Node neighbor : graph.get(u)) {
                int v = neighbor.getId();
                // 起点到当前节点的权重
                double distance = distances[u] * neighbor.getDistance();
                if (distances[v] < distance) {
                    distances[v] = distance;
                    q.offer(new Node(v, distance));
                }
            }
        }
        return distances;
    }

    private static class Node {
        private final int id;
        private final double distance;

        public Node(int id, double distance) {
            this.id = id;
            this.distance = distance;
        }

        public int getId() {
            return id;
        }

        public double getDistance() {
            return distance;
        }
    }
}

注意:

  • Python:这道题直接使用模板,会有用例超时,这里将顶点及其概率,直接通过元祖保存到优先级队列里面,避免创建对象引用的开销,避免用例超时。
  • Java:使用邻接矩阵会超时,需要使用邻接表才能通过。

应用 3:Leetcode.1631

题目

1631. 最小体力消耗路径

你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ,其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格子 (0, 0) ,且你希望去最右下角的格子 (rows-1, columns-1) (注意下标从 0 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。
一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。
请你返回从左上角走到右下角的最小 体力消耗值 。

示例 1:

image
输入:heights = [[1,2,2],[3,8,2],[5,3,5]]
输出:2
解释:路径 [1,3,5,3,5] 连续格子的差值绝对值最大为 2 。
这条路径比路径 [1,2,2,2,5] 更优,因为另一条路径差值最大值为 3 。

解题思路

这里的矩阵,我们可以类似的看成一个有向无环图,因为矩阵中的每一个点,都可以向四周邻近的点延伸。同时,需要注意,矩阵中已经搜索过的节点,不能再次重复搜索,每次搜索的时候,我们更新到达每个点所需要的权重(即题目中的体力值)。

显然,这是一个在有向加权图中,并且权重都是非负值,求源顶点 \(S\) 到目的顶点 \(V\) 的最小权重的路径的过程,因此,我们可以考虑使用 \(Dijkstra\) 算法求解。

通常,我们使用 \(Dijkstra\) 算法时,都会给定一个使用邻接表或者邻接矩阵表示的有向图。

这里,我们可以直接使用给定的矩阵 \(heights\),和一个辅助的二维矩阵 \(visit\),来遍历题目中的有向图,即对于矩阵中的每一个点,都可以向四周扩散,同时不能重复访问已经经过的节点。对于每一个节点,如果它的权重更小,就更新它的权重,并且将其重新加入优先级队列。

更新完所有节点的权重之后,就可以得到源顶点 \(S\) 到目的顶点 \(V\) 的最小权重的路径了。

代码实现

class Solution {
    private static int[][] DIRECTIONS = new int[][]{{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

    public int minimumEffortPath(int[][] heights) {
        int m = heights.length, n = heights[0].length;
        int[][] distances = dijkstra(heights);
        return distances[m - 1][n - 1];
    }

    private int[][] dijkstra(int[][] heights) {
        int m = heights.length, n = heights[0].length;
        // 记录起点到每一个位置的距离
        int[][] distances = new int[m][n];
        for (int i = 0; i < m; i++) {
            Arrays.fill(distances[i], Integer.MAX_VALUE);
        }
        // 维护一个小根堆
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> a[2] - b[2]);
        // 初始化起点
        distances[0][0] = 0;
        q.offer(new int[]{0, 0, 0});
        // 维护一个visit数组,避免走回头路
        boolean[][] visit = new boolean[m][n];
        while (!q.isEmpty()) {
            int[] candidate = q.poll();
            int u1 = candidate[0], u2 = candidate[1];
            // 已经访问过,就跳过
            if (visit[u1][u2]) {
                continue;
            }
            // 到达终点,则退出
            if (u1 == m - 1 && u2 == n - 1) {
                break;
            }
            visit[u1][u2] = true;
            // 遍历邻近的节点
            for (int[] direction : DIRECTIONS) {
                int v1 = u1 + direction[0];
                int v2 = u2 + direction[1];
                if (v1 < 0 || v1 >= m || v2 < 0 || v2 >= n) {
                    continue;
                }
                // 如果到达位置(v1,v2) 需要消耗的能量更小,就更新到达这个位置的路径,并将其入队
                int distance = Math.max(candidate[2], Math.abs(heights[v1][v2] - heights[u1][u2]));
                if (distance < distances[v1][v2]) {
                    distances[v1][v2] = Math.max(candidate[2], distance);
                    q.offer(new int[]{v1, v2, distance});
                }
            }
        }
        return distances;
    }
}

总结

\(Dijkstra\) 算法:可以求解 非负权重图 上的 单源 最优路径的问题。


posted @ 2022-11-28 23:58  LARRY1024  阅读(499)  评论(0编辑  收藏  举报