最短路径算法(Floyd,Dijkstra,Blleman Ford)

最短路算法笔记

本笔记以 Leetcode.743为例,搞清楚最短路算法的多样化。 代码均来自 公众号 : 宫水三叶的刷题日记,仅是当作笔记使用。

1、Floyd 算法

  • Floyd算法求的是从任意起点出发,到达任意起点的最短距离,时间复杂度为O(n^3),使用三个for循环,过程为枚举中转点 - 枚举起点 - 枚举终点 - 松弛操作。搭配邻接矩阵,不需要多讲解直接上代码:
class Solution {
    int N = 110, M = 6010;
    // 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
    int[][] w = new int[N][N];
    int INF = 0x3f3f3f3f;
    int n, k;
    public int networkDelayTime(int[][] ts, int _n, int _k) {
        n = _n; k = _k;
        // 初始化邻接矩阵
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                w[i][j] = w[j][i] = i == j ? 0 : INF;
            }
        }
        // 存图
        for (int[] t : ts) {
            int u = t[0], v = t[1], c = t[2];
            w[u][v] = c;
        }
        // 最短路
        floyd();
        // 遍历答案
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = Math.max(ans, w[k][i]);
        }
        return ans >= INF / 2 ? -1 : ans;
    }
    void floyd() {
        // floyd 基本流程为三层循环:
        // 枚举中转点 - 枚举起点 - 枚举终点 - 松弛操作        
        for (int p = 1; p <= n; p++) {
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= n; j++) {
                    w[i][j] = Math.min(w[i][j], w[i][p] + w[p][j]);
                }
            }
        }
    }
}

2、Dijkstra (普通)算法 + 邻接矩阵

  1. dijkstra 算法求的是单源最短路 ,时间复杂度为O(n^2),内涵是记录源点到另一个点的最短距离并不断更新,用双层for就能解决,一层用来遍历源点到其他点,一层用来更新源点到其他点的最短距离。看代码:
class Solution {
    int N = 110, M = 6010;
    // 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
    int[][] w = new int[N][N];
    // dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
    int[] dist = new int[N];
    // 记录哪些点已经被更新过
    boolean[] vis = new boolean[N];
    int INF = 0x3f3f3f3f;
    int n, k;
    public int networkDelayTime(int[][] ts, int _n, int _k) {
        n = _n; k = _k;
        // 初始化邻接矩阵
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                w[i][j] = w[j][i] = i == j ? 0 : INF;
            }
        }
        // 存图
        for (int[] t : ts) {
            int u = t[0], v = t[1], c = t[2];
            w[u][v] = c;
        }
        // 最短路
        dijkstra();
        // 遍历答案
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = Math.max(ans, dist[i]);
        }
        return ans > INF / 2 ? -1 : ans;
    }
    void dijkstra() {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        Arrays.fill(vis, false);
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[k] = 0;
        // 迭代 n 次
        for (int p = 1; p <= n; p++) {
            // 每次找到「最短距离最小」且「未被更新」的点 t
            int t = -1;
            for (int i = 1; i <= n; i++) {
                if (!vis[i] && (t == -1 || dist[i] < dist[t])) t = i;
            }
            // 标记点 t 为已更新
            vis[t] = true;
            // 用点 t 的「最小距离」更新其他点
            for (int i = 1; i <= n; i++) {
                dist[i] = Math.min(dist[i], dist[t] + w[t][i]);
            }
        }
    }
}

3、Dijkstra 算法+优先队列(堆)+邻接表

  1. 这个算法比普通的多用了一个优先队列,每次优先弹出最短距离最小的点,就少用了一个for循环
    时间复杂度为O(m*logn),顺便讲一下邻接表,与数组存储单链表一样是使用头插法,初始化代码如下:
int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
int idx;

void add(int a, int b, int c) {
    e[idx] = b; //e 可以得到哪条边对应的点
    ne[idx] = he[a];//ne作用是找到下一条边
    he[a] = idx;//he是某个节点对应的边集合的头节点
    w[idx] = c;//存储边的权值
    idx++;
}


//若要遍历从源点到所有点
for (int i = he[a]; i != -1; i = ne[i]) {
    int b = e[i], c = w[i]; // 存在由 a 指向 b 的边,权重为 c
}

可以看别人(大佬)的解释:

数组 he 的下标表示结点,值是一个索引 ind,e[ind] 表示 对应一条边,ne[ind] 表示下一个连接结点的索引,假设与 结点a 相连的结点有 b, c, 那么通过 he[a]取得一个索引 ind1 后,通过 e[ind1] = b 可以得到与 a 相连的第一个结点是 b,然后通过 ne[ind1] 可以获得下一个结点的索引 ind2 ,通过 e[ind2] = c 可以得到与 a 相连的第二个结点是 c,最后 ne[ind2] = -1 说明没有下一个结点了
add函数采用链表的头插法,假设 结点a 已经有一个相连的结点 b,那么就有 he[a]=ind, e[ind]=b ,此时再给 a 增加一个相连的结点 c,那么就要建立由b的索引到新结点c的索引 ne[new_ind] = he[a] = ind ,然后新建一条边 e[new_ind], 最后更新 he[a] = new_ind ,就完成了由 a -> b 到 a -> c -> b 的添加操作
可以理解为 he 是邻接表的表头,key是结点val是一个指向存有相邻结点的链表头指针,e是链表结点的val即相邻结点,ne是链表结点的next指针

反推一下,在a->b 中插入一个c 可得 a->c->b,所以e[c_idx] = c,ne[c_idx] = b_idx = he[a],he[a] = c_idx。还是挺形象的。
还有使用邻接表存图的时候,得判断是有向图还是无向图,有权图还是无权图若是无向图 就需要在add的时候把两个点都加进去 比如说 a和b:

for(int[]t:ts){
	add(t[0],t[1]);
	add(t[1],t[0]);
}

一般不带权的图使用普通BFS就OK,带权的才会算最短路径。
改良后dijkstra算法上代码:

class Solution {
    int N = 110, M = 6010;
    // 邻接表
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    // dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
    int[] dist = new int[N];
    // 记录哪些点已经被更新过
    boolean[] vis = new boolean[N];
    int n, k, idx;
    int INF = 0x3f3f3f3f;
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx;
        w[idx] = c;
        idx++;
    }
    public int networkDelayTime(int[][] ts, int _n, int _k) {
        n = _n; k = _k;
        // 初始化链表头
        Arrays.fill(he, -1);
        // 存图
        for (int[] t : ts) {
            int u = t[0], v = t[1], c = t[2];
            add(u, v, c);
        }
        // 最短路
        dijkstra();
        // 遍历答案
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = Math.max(ans, dist[i]);
        }
        return ans > INF / 2 ? -1 : ans;
    }
    void dijkstra() {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        Arrays.fill(vis, false);
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[k] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
        q.add(new int[]{k, 0});
        while (!q.isEmpty()) {
            // 每次从「优先队列」中弹出
            int[] poll = q.poll();
            int id = poll[0], step = poll[1];
            // 如果弹出的点被标记「已更新」,则跳过
            if (vis[id]) continue;
            // 标记该点「已更新」,并使用该点更新其他点的「最短距离」
            vis[id] = true;
            for (int i = he[id]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] > dist[id] + w[i]) {
                    dist[j] = dist[id] + w[i];
                    q.add(new int[]{j, dist[j]});
                }
            }
        }
    }
}

4、Bellman Ford 算法(比较简单)

BF算法和Floyd算法一样都是基于动态规划衍生的算法,BF可以用在负权图中求最短路,这个也是一种单源最短路算法。可以使用类和邻接表或者邻接矩阵来配合使用,BF的核心操作就是遍历所有的边,这时候就可以不需要另外开一个空间存图。BF适合用在有条件限制的求最短路问题。
这里以另一题为例 Leetcode.787
这种题就是有限制的最短路问题,也可以用动态规划来做,但BF其实可以看成由原始状态 f[i][k] 从源点到i点最多经过k条边的最短路径,以下为代码:

class Solution {
    int N = 110, INF = 0x3f3f3f3f;
    int[][] g = new int[N][N];
    int[] dist = new int[N];
    int n, m, s, t, k;
    public int findCheapestPrice(int _n, int[][] flights, int _src, int _dst, int _k) {
        n = _n; s = _src; t = _dst; k = _k + 1;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                g[i][j] = i == j ? 0 : INF;
            }
        }
        for (int[] f : flights) {
            g[f[0]][f[1]] = f[2];
        }
        int ans = bf();
        return ans > INF / 2 ? -1 : ans;
    }
    int bf() {
        Arrays.fill(dist, INF);
        dist[s] = 0;
        for (int limit = 0; limit < k; limit++) {
            int[] clone = dist.clone(); //这里很关键
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    dist[j] = Math.min(dist[j], clone[i] + g[i][j]);
                }
            }
        }
        return dist[t];
    }
}

其中有个关键的代码,就是在迭代的时候,我们如果要使用dist[a] 来更新dist[b],不能保证dist[a]是否在同一次迭代更新,这也就有点判断是否遍历过的意思,所以每次都需要备份以下dist数组以防止超出次数限制。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值