最短路径问题解法总结

1,存图方式

1.1 邻接矩阵

这是一种使用二维矩阵来进行存图的方法。

适用于边数较多的稠密图使用,当边数量接近点的数量的平方,即 m≈n² 时,可定义为稠密图

//定义无穷大为INF,若使用Interger.MAX_VALUE即0x7fffffff,在松弛操作时会溢出为较小的负数
int INF = 0x3f3f3f3f;

// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
int[][] w = new int[N][N];

// 加边操作
void add(int a, int b, int c) {
    w[a][b] = c;
}

// 初始化邻接矩阵数组
void init(){
  for (int i=1;i<=n;i++){
    for (int j=1;j<=n;j++){
      w[i][j] = w[j][i] = i == j ? 0 : INF;
    }
  }
}

1.2 邻接表(链式前向星)

这也是一种在图论中十分常见的存图方式,与数组存储单链表的实现一致(头插法)。

这种存图方式又叫链式前向星存图

适用于边数较少的稀疏图使用,当边数量接近点的数量,即 m≈n 时,可定义为稀疏图

int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
int idx;
int[] d = new int[N];//存储节点的深度
int[] indeg = new int[N];//存储节点的入度
boolean[] vis = new boolean[N];

void add(int a, int b, int c) {
    e[idx] = b;
    ne[idx] = he[a];
    he[a] = idx;
    w[idx] = c;
    idx++;
  
    indeg[b]++;
}

{
  // 初始化链表头
  Arrays.fill(he, -1);
}

void dfs(int u){
        vis[u] = true;
        for (int i=he[u];i!=-1;i=ne[i]){
            int v = e[i];
            if (!vis[v]){
                dfs(v);
            }
        }
    }

void bfs(int u){
  Queue queue = new ArrayDeque<>();
  queue.offer(u);
  d[u]=1;
  vis[u] = true;
  while (!queue.isEmpty()){
    int x = queue.poll();
    for (int i=he[x];i!=-1;i=ne[i]){
      int y = e[i];
      if (vis[y])
        continue;
      queue.offer(y);
      d[y] = d[x] + 1;
    }
  }
}

void topsort(){
  Queue queue = new ArrayDeque<>();
  for (int i=0;i

首先 idx 是用来对边进行编号的,然后对存图用到的几个数组作简单解释:

  • he 数组:he[i],表示以i为起点的第一条边在边集数组的位置(编号)(head)
  • e 数组:用于访问某一条边指向的节点;(end)
  • ne 数组:该数组就是用于找到下一条边;(nextEdge)
  • w 数组:用于记录某条边的权重为多少。(weight)

因此当我们想要遍历所有由 a 点发出的边时,可以使用如下方式:

for (int i = he[a]; i != -1; i = ne[i]) {
    int b = e[i], c = w[i]; // 存在由 a 指向 b 的边,权重为 c
}

2,Floyd算法+邻接矩阵

Floyd算法的基本思想如下:从任意节点A到任意节点B的最短路径不外乎2种可能,1是直接从A到B,2是从A经过若干个节点到B,所以,我们假设dist(AB)为节点A到节点B的最短路径的距离,对于每一个节点K,我们检查dist(AK) + dist(KB) < dist(AB)是否成立,如果成立,证明从A到K再到B的路径比A直接到B的路径短,我们便设置 dist(AB) = dist(AK) + dist(KB),这样一来,当我们遍历完所有节点K,dist(AB)中记录的便是A到B的最短路径的距离。

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]);
        }
      }
    }
 }
  • 时间复杂度:O(n³)
  • 空间复杂度:O(n²)

3,Dijkstra算法+邻接矩阵

同理,我们可以使用复杂度为 O(n^2)O(n2) 的「单源最短路」算法朴素 Dijkstra 算法进行求解,同时使用「邻接矩阵」来进行存图。Dijkstra算法不能处理负权边

迪杰斯特拉(Dijkstra)算法按路径长度递增次序产生最短路径。先把V分成两组:

  • S:已求出最短路径的顶点的集合

  • V-S=T:尚未确定最短路径的顶点集合

    将T中顶点按最短路径递增的次序加入到S中,依据:可以证明V0到T中顶点Vk的最短路径,或是从V0到Vk的直接路径的权值或是从V0经S中顶点到Vk的路径权值之和(反证法可证)。

        // 顶点数N,变数M
    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;

        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]);
            }
        }
    }
  • 时间复杂度:O(n²)
  • 空间复杂度:O(n²)

4,Dijkstra+邻接表

        // 顶点数N,变数M
    int N = 110,M = 6010;
        // 邻接表
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
        int idx;
    // dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
    int[] dist = new int[N];
    // 记录哪些点已经被更新过
    boolean[] vis = new boolean[N];
    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++;
    }
        {
        // 初始化链表头
        Arrays.fill(he, -1);
    }

        void dijkstra() {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        Arrays.fill(vis, false);
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[k] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
        PriorityQueue 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]});
                }
            }
        }
    }

  • 时间复杂度:O(mlogn+n)
  • 空间复杂度:O(m)

5,Bellman-Ford算法

为了能够求解边上带有负值的单源最短路径问题,Bellman(贝尔曼,动态规划提出者)和Ford(福特)提出了从源点逐次绕过其他顶点,以缩短到达终点的最短路径长度的方法。 Bellman-ford算法是求含负权图的单源最短路径算法,效率很低,但代码很容易写。即进行不停地松弛,每次松弛把每条边都更新一下,若n-1次松弛后还能更新,则说明图中有负环,无法得出结果,否则就成功完成。Bellman-ford算法有一个小优化:每次松弛先设一个flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出。

如果没有负权回路的话,应当会在 n - 1 次松弛之后结束算法。原因在于考虑对每条边进行 1 次松弛的时候,得到的实际上是至多经过 0 个点的最短路径(初始化每个点到源点的距离为无穷,这里的经过 0 个点意思为路径仅由源点和终点组成),对每条边进行两次松弛的时候得到的至多是经过 1 个点的最短路径。如果没有负权回路,那么任意两点的最短路径至多经过 n - 2 个点(不包含源点和终点这两个点),因此经过 n - 1 次松弛操作后应当可以得到最短路径。如果有负权回路,那么第 n 次松弛操作仍然会成功(即第 n 次松弛仍然存在一条边可以对其进行松弛操作),但是不存在负权回路的图应该在第 n - 1 次松弛之后,已经不存在可以松弛的边了,所以Bellman -Ford算法就利用了这个性质以达到判定负环的目的。

例787. K 站中转内最便宜的航班

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

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

解:「限制最多经过不超过 k个点」等价于「限制最多不超过 k + 1 条边」

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];
    }
}

6,SPFA算法

用一个队列来进行维护。初始时将源加入队列。每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。直到队列为空时算法结束;这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。

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 INF = 0x3f3f3f3f;
    int n, k, idx;
    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);
        }
        // 最短路
        spfa();
        // 遍历答案
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = Math.max(ans, dist[i]);
        }
        return ans > INF / 2 ? -1 : ans;
    }
    void spfa() {
        // 起始先将所有的点标记为「未入队」和「距离为正无穷」
        Arrays.fill(vis, false);
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[k] = 0;
        // 使用「双端队列」存储,存储的是点编号
        Deque d = new ArrayDeque<>();
        // 将「源点/起点」进行入队,并标记「已入队」
        d.addLast(k);
        vis[k] = true;
        while (!d.isEmpty()) {
            // 每次从「双端队列」中取出,并标记「未入队」
            int poll = d.pollFirst();
            vis[poll] = false;
            // 尝试使用该点,更新其他点的最短距离
            // 如果更新的点,本身「未入队」则加入队列中,并标记「已入队」
            for (int i = he[poll]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] > dist[poll] + w[i]) {
                    dist[j] = dist[poll] + w[i];
                    if (vis[j]) continue;
                    d.addLast(j);
                    vis[j] = true;
                }
            }
        }
    }
}

你可能感兴趣的:(最短路径问题解法总结)