最短路径算法

184 阅读5分钟
  • BFS
  • Dijkstra
  • Best-First
  • A*
  • Floyd
  • Bellman-Ford
  • SPFA

广度优先遍历

Edward F. Moore 在 1950 年发表,起初被用于在迷宫中寻找最短路径。在 Prim 最小生成树算法和 Dijkstra 单源最短路径算法中,都采用了与广度优先搜索类似的思想。BFS是最短路径算法里面最简单的,也是后面其他的基础。它是在所有方向上均等地探索。这是一个非常有用的算法,不仅适用于常规寻路,还适用于程序地图生成、流场寻路、距离地图和其他类型的地图分析。

原理

广度优先遍历就是你找一个点,然后匀速的向各个方向扩展

时间复杂度O(V+E),V 为顶点个数,E 为边的条数

空间复杂度O(V),V 为顶点个数,也就是我们每次遍历需要储存下一次要遍历的点

代码

一般情况下,我们使用广度优先遍历的时候都会用到队列,每次遍历一遍就把下一次可以遍历的点加入队尾

为了防止当前取出元素的时候取到下一次的元素(因为每次都往队尾加元素,如果不需要统计广度的次数就可以不理),那么我们可以在进行一次扩散时先获取当前需要遍历的元素个数,也就是当前队列里面剩下的元素个数

733. 图像渲染

class Solution {
    int[] xn = {-1, 1, 0, 0};
    int[] yn = {0, 0, -1, 1};
    public int[][] floodFill(int[][] image, int sr, int sc, int color) {
        // 队列,用于广度遍历的时候记录每一次扩展到的点
        Deque<int[]> deque = new LinkedList<>();
        deque.add(new int[]{sr, sc});
        // 记录遍历过的点
        boolean[][] visited = new boolean[image.length][image[0].length];
        visited[sr][sc] = true;
        // 起始点
        int start = image[sr][sc];
        image[sr][sc] = color;
        // 开始广度遍历
        while (!deque.isEmpty()){
            int size = deque.size();
            for (int i = 0; i < size; i++) {
                int[] poll = deque.poll();
                int x = poll[0];
                int y = poll[1];
                for (int j = 0; j < 4; j++) {
                    // 判断---如果不在图内、不等于起始值、访问过,都需要排除
                    if (x+xn[j] >= 0 && x+xn[j] < image.length && y+yn[j] >= 0 && y+yn[j] < image[0].length && image[x+xn[j]][y+yn[j]] == start && !visited[x+xn[j]][y+yn[j]]){
                        image[x+xn[j]][y+yn[j]] = color;
                        visited[x+xn[j]][y+yn[j]] = true;
                        deque.add(new int[]{x+xn[j], y+yn[j]});
                    }
                }
            }
        }
        return image;
    }
}

Dijkstra

上面的广度优先遍历有点贪心的味道,不管三七二十一就是一股脑的往外蔓延,但是如果每一条路都有价值,并且我们需要找到两个点之间最低价值的路径呢?这个时候广度似乎就没办法解决了,所以我们引入了Dijkstra算法

Dijkstra算法是由计算机科学家Edsger W. Dijkstra在1956年提出的

Dijkstra算法用来寻找图形中节点之间的最短路径

原理

在大学我们就学过这个算法,想像一下,你现在需要求出点0到各个点的最短路径,你是不是要考虑到他们的路径价值,而Dijkstra算法就是解决这种问题的 --- 单点最短路径问题

本质其实就是贪心,每次取距离0最短,并且没有遍历过的点,从0开始

  1. 每次我们取距离0最近并且没有取过的点(后面就知道,取过的点已经是求出了最短距离)
  2. 取出这个点后,我们更新和这个点相连的点的距离,比如取出的是1,0到1的距离是2,3又和1相连,并且距离是5,那么0到3的距离就是7,现在1已经被选择过了,我们假设现在满足条件的是3,他和1相连,那么0进过3到1的距离肯定比之前长,因为3是从1更新过来的,而且取1的时候他是距离0最小,并且没有被遍历过的(也就是后续取的点肯定比当前0到1的距离长,这里不考虑负数距离),这就是为什么被选中过的点一定已经确定了0到它的最短距离
  3. 重复1、2这两个过程,直到所有点都被取过,那么就完成了整个算法的过程(如果只需要判断0到某一个点的最短距离/路径,那么只要那个点被取出过后就不需要继续往下走了)

不考虑堆优化的情况下(邻接表实现)

时间复杂度为O(n^2+m)

空间复杂度为O(m)

考虑堆优化的情况下(当图为稠密图时,堆优化反而时间更差,主要是边太多,那么放进优先队列里面重复的点就越多(有时候不得不放,因为你要考虑相同的点可能更优秀,距离更短))

时间复杂度为O((v+e)*loge)

空间复杂度为O(v+e)

推荐看一下这个网站的图文介绍,介绍了整个过程,很生动形象

www.freecodecamp.org/chinese/new…

743. 网络延迟时间

代码

class Solution {
    class Node{
        int value;
        int power;

        public Node(int value, int power) {
            this.value = value;
            this.power = power;
        }
    }
    Map<Integer, List<Node>> map = new HashMap<>();
    public int networkDelayTime(int[][] times, int n, int k) {
        for (int[] time : times) {
            map.computeIfAbsent(time[0]-1, c->new ArrayList<>()).add(new Node(time[1]-1, time[2]));
        }
        int[] paths = Dijkstra(k - 1, n);
        int res = -1;
        for (int i = 0; i < n; i++) {
            res = Math.max(res, paths[i]);
        }
        return res == Integer.MAX_VALUE ? -1 : res;
    }
    public int[] Dijkstra(int source, int n){
        int[] paths = new int[n];
        Arrays.fill(paths, Integer.MAX_VALUE);
        paths[source] = 0;
        PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingInt(a -> a.power));
        queue.add(new Node(source, 0));
        boolean[] visited = new boolean[n];
        while (!queue.isEmpty()){
            Node poll = queue.poll();
            if (visited[poll.value]) continue;
            visited[poll.value] = true;
            List<Node> list = map.getOrDefault(poll.value, new ArrayList<>());
            for (Node node : list) {
                if (visited[node.value]) continue;
                if (node.power + poll.power < paths[node.value]){
                    queue.add(new Node(node.value, node.power+poll.power));
                    paths[node.value] = node.power+poll.power;
                }
            }
        }
        return paths;
    }
}

Best-First

最佳优先算法,意思就是把Dijkstra中选点的方法改成选取距离终点最近的点(曼哈顿距离)

原理

默认选择距离终点直线最近的点,这种算法也是贪心的一种。

这种说法在有障碍物的情况下是不能用的,因为他给出的答案往往不是最短距离,比如用Dijkstra和他对比

很明显图一才是最佳的路径

这里很明显只能用于无障碍物,所以

时间复杂度:O(2n)

空间复杂度:O(1)

代码

A*

A*搜索算法发明者是Nils John Nilsson博士,它是一种启发式搜索算法,可以用来找到 A 点到 B 点之间的最短路径。常用于游戏中的角色的移动,或线上游戏中BOT的移动。可以找到一条最短路径,也可以像宽度搜索算法一样,进行启发式的搜索。

原理

A*算法就是Dijkstra的扩展,选择哪一个点时是使用估计函数 f(n) = g(n)+h(n)

  1. f(n):其中f(n)越小,优先级越高
  2. g(n):g(n)是从起点走到这个点消耗了多少步
  3. h(n):h(n)是当前点到终点的预计代价,也叫做启发函数,他是当前点到终点的距离函数,可以类比为当前点到终点还需要多少步,但是由于可能会有障碍物什么的,而且我们无法准确的估计出当前点到终点还需要多少步,所以这个函数如果选择得好,在特定的情景下,算法的速度可能会有很大的提升

这是Dijkstra、Best-First、A算法的比较,我们可以发现,A算法在准确度和时间上都是最快的,这里结合了其他两个算法的优点,首先如果 h(n) 始终为0,我们发现我们就只考虑起点走到当前点的最短距离,这和Dijkstra是一样的,但是如果我们考虑h(n),也就是当前点到终点的预估距离,那么选择当前点的时候我们就可以选择更加优秀的点,比如当前 h(n) 选择的算法是曼哈顿距离,也就是两点之间的直线距离,这也是 Best-First 使用的算法,当然我们也可以选择其他的,这就要看使用者能不能根据具体情况选择最佳 h(n) 函数了

推荐一个很好玩的网站,他使用了可拖动的动画展示了上面几个算法的过程,最终总结了A*算法

www.redblobgames.com/pathfinding…

代码

773. 滑动谜题

class Solution {
    int n;
    int m;
    int[] xn = {-1, 1, 0, 0};
    int[] yn = {0, 0, -1, 1};
    int[][] sites = {{1, 2}, {0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}};
    class Node{
        String str;
        int x = 0;
        int y = 0;
        int value;

        public Node(String str, int x, int y, int value) {
            this.str = str;
            this.x = x;
            this.y = y;
            this.value = value;
        }
    }
    public int slidingPuzzle(int[][] board) {
        StringBuilder source = new StringBuilder("");
        String target = "123450";
        int tx = 0;
        int ty = 0;
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                if (board[i][j] == 0) {
                    tx = i;
                    ty = j;
                }
                source.append(board[i][j]);
            }
        }
        if (!check(source)) return -1;
        Map<String, Integer> map = new HashMap<>();
        n = board.length;
        m = board[0].length;
        PriorityQueue<Node> queue = new PriorityQueue<>((a, b) -> a.value-b.value);
        queue.add(new Node(source.toString(), tx, ty,calculate(source)));
        map.put(source.toString(), 0);
        while (!queue.isEmpty()){
            Node poll = queue.poll();
            int step = map.getOrDefault(poll.str, 0);
            if (poll.str.equals(target)) return step;
            for (int i = 0; i < 4; i++) {
                int newX = poll.x+xn[i];
                int newY = poll.y+yn[i];
                if (newX >= 0 && newX < n && newY >= 0 && newY < m){
                    StringBuilder stringBuilder = new StringBuilder(poll.str);
                    update(stringBuilder, poll.x*m+poll.y, newX*m+newY);
                    if (map.containsKey(stringBuilder.toString())) continue;
                    String s = stringBuilder.toString();
                    queue.add(new Node(s, newX, newY, calculate(stringBuilder)+step));
                    map.put(s, step+1);
                }
            }
        }
        return -1;
    }

    public int calculate(StringBuilder str){
        int sum = 0;
        for (int i = 0; i < str.length(); i++) {
            if (str.charAt(i) == '0') continue;
            sum += Math.abs(i/3-sites[str.charAt(i)-'0'][0])+Math.abs(i%3-sites[str.charAt(i)-'0'][1]);
        }
        return sum;
    }

    public void update(StringBuilder str, int x, int y){
        char c1 = str.charAt(x);
        char c2 = str.charAt(y);
        str.setCharAt(x, c2);
        str.setCharAt(y, c1);
    }

    boolean check(StringBuilder cs) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < n * m; i++) {
            if (cs.charAt(i) != '0') list.add(cs.charAt(i), - '0');
        }
        int cnt = 0;
        for (int i = 0; i < list.size(); i++) {
            for (int j = i + 1; j < list.size(); j++) {
                if (list.get(i) > list.get(j)) cnt++;
            }
        }
        return cnt % 2 == 0;
    }
}

Floyd

该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名,这个算法主要就是寻找每一个点到其他所有点的最短路径

原理

对于每个顶点v,和任一项顶点对 (i, j), i != j, v != j, 如果 A[i][j] > A[i][v] + A[v][j],则将 A[i][j] 更新为 A[i][v] + A[v][j] 的值,并且将 Path[i][j] 改为v

这句话主要就是找到中间点,然后判断两个点之间如果经过这个中间点会不会有更短的路径

只需要遵循这句话写代码即可,代码非常简单,三个for循环

  1. 第一个for循环,遍历0~n-1,得到 v(选取中间值)
  2. 第二个for循环,遍历0~n-1,得到 i,i != v
  3. 第三个for循环,遍历0~n-1,得到 j,j != v,判断是否满足 A[i][j] > A[i][v] + A[v][j],如果满足则更新 A[i][j] = A[i][v] + A[v][j]

时间复杂度为O(n3)

空间复杂度为O(n2)

参考:www.bilibili.com/video/BV1LE…

代码

743. 网络延迟时间

class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {
        int[][] distance = new int[n][n];
        int[][] path = new int[n][n];
        for (int i = 0; i < n; i++) {
            Arrays.fill(distance[i], 1000000);
            Arrays.fill(path[i], -1);
        }
        for (int i = 0; i < n; i++) {
            distance[i][i] = 0;
        }
        for (int[] time : times) {
            distance[time[0]-1][time[1]-1] = time[2];
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i == j) continue;
                for (int l = 0; l < n; l++) {
                    if (i == l) continue;
                    if (distance[j][l] > distance[j][i] + distance[i][l]){
                        distance[j][l] = distance[j][i] + distance[i][l];
                        path[j][l] = i;
                    }
                }
            }
        }
        int max = -1;
        for (int j = 0; j < n; j++) {
            if (distance[k-1][j] == 1000000){
                return -1;
            }
            max = Math.max(max, distance[k-1][j]);

        }
        return max;
    }
}

Bellman-Ford

贝尔曼-福特算法(Bellman-Ford)是由理查德·贝尔曼(Richard Bellman)莱斯特·福特创立的,求解单源最短路径问题的一种算法,该算法允许有负权的存在

原理

这个算法有一个叫做松弛操作,它是针对边而言的,本意就是 (0) --7--> (∞),那么对这条边进行松弛,则无穷会被更新为7,因为源节点到上一个节点的距离是0,这条边又需要7的距离,那么源节点到下一个节点的距离就是7,那么结果就是(0) --7-->(7)

我们需要遍历每一条边,并更新这条边中的目标点,既如果(target) > (边长)+(source),那么(target) = (边长)+(source),遍历的次数为V次,既节点个数,为什么呢?

我们可以考虑这样一种极端情况,每次我们都从最后一条边开始更新,这样就必须更新 n-1 次才可以完成所有节点的更新

上面遍历完后,我们可以考虑在单独进行一次松弛操作,如果可以进行,说明存在负环,对于这个环,我们无法找到最短路径

时间复杂度O(V*E)

空间复杂度O(V*V)

推荐一个动画演示的视频

www.bilibili.com/video/BV1j3…

代码

这个代码多进行一次松弛操作,因为这道题不存在负权,当然要进行也行,也就是把上面循环的操作在复制一份在下面进行一次即可(不需要循环执行,只需要循环一遍)

743. 网络延迟时间

class Solution {
    class Node{
        int source;
        int target;
        int value;

        public Node(int source, int target, int value) {
            this.source = source;
            this.target = target;
            this.value = value;
        }
    }
    public int networkDelayTime(int[][] times, int n, int k) {
        int[] path = new int[n];
        ArrayList<Node> list = new ArrayList<>();
        for (int[] time : times) {
            list.add(new Node(time[0]-1, time[1]-1, time[2]));
        }
        Arrays.fill(path, 1000000);
        path[k-1] = 0;
        for (int i = 0; i < n-1; i++) {
            for (int j = 0; j < list.size(); j++) {
                if (path[list.get(j).target] > path[list.get(j).source]+list.get(j).value){
                    path[list.get(j).target] = path[list.get(j).source]+list.get(j).value;
                }
            }
        }
        int max = -1;
        for (int i = 0; i < n; i++) {
            if (path[i] == 1000000){
                return -1;
            }
            max = Math.max(max, path[i]);
        }
        return max;
    }
}

SPFA

针对Bellman-Ford算法最坏的情况,我们可以发现时间复杂度很高O(V*E),主要是没办法直接找到可以松弛的最合适的点,这里我们可以使用队列优化(不需要优先队列)

原理

在Bellman-Ford算法中,我们把for循环改成队列的方式

初始时,我们把源点放入队列中,然后遍历这个队列的边,并且进行松弛操作,如果可以松弛,那么我们就把它放入到队尾,这样每次我们取到的点绝对是之前松弛过过的点,大概率它连接的边也是可以松弛其他点的

比如还是这个图,我们0先入队,然后取出来它,遍历它的边,更新0 --1-->∞为0 --1-->1,然后这个1的点入队,这样整体下来只需要遍历一次。

当然上面这个例子是最好的情况,如果存在负环,那么他可能会退变为Bellman-Ford,但是总体来说还是优于它的,毕竟Bellman-Ford无论最好还是最坏,每次都要判断全部边,因为他不知道自己是否找到了所有可更新的边

代码

743. 网络延迟时间

class Solution {
    class Node{
        int target;
        int value;

        public Node(int target, int value) {
            this.target = target;
            this.value = value;
        }
    }
    public int networkDelayTime(int[][] times, int n, int k) {
        int[] path = new int[n];
        Map<Integer, List<Node>> map = new HashMap<>();
        for (int[] time : times) {
            map.computeIfAbsent(time[0]-1,t->new ArrayList<>()).add(new Node(time[1]-1, time[2]));
        }
        Arrays.fill(path, 1000000);
        path[k-1] = 0;

        Deque<Integer> deque = new LinkedList<>();
        deque.add(k-1);
        while (!deque.isEmpty()){
            Integer poll = deque.poll();
            List<Node> list = map.getOrDefault(poll, new ArrayList<>());
            for (Node node : list) {
                if (path[node.target] > path[poll]+node.value){
                    path[node.target] = path[poll]+node.value;
                    deque.add(node.target);
                }
            }
        }

        int max = -1;
        for (int i = 0; i < n; i++) {
            if (path[i] == 1000000){
                return -1;
            }
            max = Math.max(max, path[i]);
        }
        return max;
    }
}