算法学习-最短路算法与各种存图方式,链式前向星,配图加深理解

本文参考:
链式前向星——最完美图解
【宫水三叶】涵盖所有的「存图方式」与「最短路算法(详尽注释)」
堆优化版Dijkstra算法
深入理解Bellman-Ford(SPFA)算法
图论最短路:Bellman-Ford与其优化SPFA算法的一点理解

本文写于我的另一篇文章算法学习-拓扑排序期间,在进行拓扑算法学习的过程中,涉及到了一系列存图的过程,在看三叶姐的题解中,时常被这些存图的数组搞得晕头转向,由此可见,对于数组这种简单数据结构的熟练运用也是算法高超的体现啊。因此,本文对相关的存图方式以及数据结构中最普遍的最短路问题的算法,进行专题学习。

基础知识

存图方式

一、邻接矩阵存图

用最朴素的二维数组方式来存图,对于w[i][j],i、j需要根据题目中节点的编号灵活标记。

// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
    int[][] w = new int[N][N];
    int INF = 0x3f3f3f3f;
    
// 初始化邻接矩阵,不能互通的两点间距离为INF,对角线的两点距离为0
// i,j为节点数值,不一定从0开始
   for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            w[i][j] = w[j][i] = i == j ? 0 : INF;
        }
    }

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

二、邻接表存图

一般输入都会以roads[i] = [ai, bi, distancei]的数组形式给出两个节点间的连接关系,这一步存图就是要对这些输入进行处理,汇总一个节点的所有相邻节点。

1. Collection容器存储相邻节点

图中一般「没有存储图的边代价」(如果想存入边的代价只需要用HashSet<int[]>存储就好)

着重于「邻点信息和入度信息的统计」,常用于「拓扑排序」,同时也可以用于一些与边代价无关的DFS、BFS搜索。

  1. HashSet数组实现如下:
//建立邻接表和入度数组
HashSet<Integer>[] adj=new HashSet[numCourses];
for(int i=0;i<numCourses;i++){
    adj[i]=new HashSet<>();
}
int[] in=new int[numCourses];
//存储其他节点并统计入度信息
for(int[]p:prerequisites){
    adj[p[1]].add(p[0]);
    in[p[0]]+=1;
}
  1. ArrayList数组实现如下:
//建立邻接表和入度数组
ArrayList<Integer>[] adj=new ArrayList[numCourses];
for(int i=0;i<numCourses;i++){
    adj[i]=new ArrayList<>();
}
int[] in=new int[numCourses];
//存储其他节点并统计入度信息
for(int[]p:prerequisites){
    adj[p[1]].add(p[0]);
    in[p[0]]+=1;
}

有些题目很坏,如6163.给定条件下构造矩阵,只能用ArrayList实现,因为给定条件中会给重边,如果在统计入度信息的时候重复计算,那么在统计节点出队列信息的时候,也应该重复减去入度。

  1. HashMap实现如下,但要注意在接下来取一个节点的相邻节点的时候,可能会出现对应节点的邻接节点set为空的情况,要先进行判断:
//建立邻接表和入度数组
HashMap<Integer,HashSet<Integer>> adj=new HashMap<>();
int[] in=new int[numCourses];
for(int[]p:prerequisites){
    HashSet<Integer> set=adj.getOrDefault(p[1],new HashSet<>()); //对应节点的set为空,之前没初始化
    set.add(p[0]); // 建立的是有向图
    adj.put(p[1],set);
    in[p[0]]+=1;
}
  1. 以上建图方式都是通过题目已经给定的数组进行邻接表的建立,在有些题目中(比如树),我们则需要通过**「递归」的方式**进行无向图的建立。

    在先序遍历的过程中,对根节点,左右叶子节点分别建立邻接表,无向图存图实现如下:

HashMap<Integer,HashSet<Integer>> map;
map=new HashMap<>();
buildGraph(root);
 public void buildGraph(TreeNode root){
        if(root==null) return;
        if(root.left!=null){
            HashSet<Integer> set1=map.getOrDefault(root.val,new HashSet<Integer>());
            HashSet<Integer> set2=map.getOrDefault(root.left.val,new HashSet<Integer>());
            set1.add(root.left.val);
            set2.add(root.val);
            map.put(root.val,set1);
            map.put(root.left.val,set2);
        }
        if(root.right!=null){
            HashSet<Integer> set1=map.getOrDefault(root.val,new HashSet<Integer>());
            HashSet<Integer> set2=map.getOrDefault(root.right.val,new HashSet<Integer>());
            set1.add(root.right.val);
            set2.add(root.val);
            map.put(root.val,set1);
            map.put(root.right.val,set2);
        }
        buildGraph(root.left);
        buildGraph(root.right);
    }
2. 结构体实现链式前向星

struct node中包括两种数据结构:
边集数组edge[ ],edge[i]表示第i条边,在构造时通过cnt不断自增进行编号,以此对每条边进行区分。
头结点数组int head[ ],head[i]存以节点数字 i 为起点的第一条边的下标(在edge[]中的下标)。该数组首先都被初始化为-1,在头插法建图的过程中不断改变。

struct node{
    int to,next,w;
}edge[maxe];//边集数组,边数一般要设置比maxn*maxn大的数,如果题目有要求除外

int head[maxn];//头结点数组

举一个简单的例子如下图所示:
在这里插入图片描述

  • 输入1 2 5;头插法中可以将head[i]视作头,edge[0].next=head[1](-1); head[1]=cnt(0)++;
    在这里插入图片描述

  • 输入2 4 12;edge[1].next=head[2](-1); head[2]=cnt(1)++;
    在这里插入图片描述

  • 输入1 4 3;edge[2].next=head[1](0); head[1]=cnt(2)++;
    在这里插入图片描述

添加一条边u v w的代码如下:
如果是有向图,执行一次add(u,v,w)就行,如果是无向图,需要执行两次add(u,v,w);add(v,u,w)

void add(int u,int v,int w){//添加一条边
    edge[cnt].to=v;
    edge[cnt].w=w;
    edge[cnt].next=head[u];
    head[u]=cnt++;
}

访问一个节点的所有邻接点代码如下:

for(int i=head[u];i!=-1;i=edge[i].next){
    int v=edge[i].to; //u的邻接点
    int w=edge[i].w; //u—v的权值}
3. 数组实现链式前向星

同2中的需要边、头节点两种重要的数据结构,每条边通过idx进行标记,head仍然是存储每个节点的对应的第一条边。
相关的邻接表数据结构定义如下:

	// 邻接表
    // 存储与 结点v 直接相连的边编号idx,head下标索引为节点的序号v
    private int[] head = new int[N];

    // 表示idx边所指向的节点,edgeNode下标索引为边编号idx
    private int[] edgeNode = new int[M];

    // 存储边编号为idx的边连接的下一条边idx,nextEdge下标索引为边编号idx
    private int[] nextEdge = new int[M];

    // 记录某条边idx的权重,weight下标索引为边编号idx
    private int[] weight = new int[M];

邻接表加边操作

/**
     * 邻接表 加边操作
     * @param srcV: 源点
     * @param desV: 目标点
     * @param weight: 边权重
     */
    private void add(int srcV, int desV, int weight) {
        // 表示边编号为idx, 指向节点的序号为desV
        this.edge[idx] = desV;

        // 头插法在头数组上加边
        this.nextEdge[idx] = this.head[srcV];
        this.head[srcV] = idx;

        // 将编号为idx的边的权值 赋为 weight
        this.weight[idx] = weight;
        idx++;
    }

最短路算法

一、Floyd(邻接矩阵)

「多源汇最短路」Floyd 算法进行求解时,使用「邻接矩阵」来进行存图。跑一遍 Floyd,可以得到「从任意起点出发,到达任意起点的最短距离」。

适用于存在重边和自环,边权可能为负数的加权图。

算法要点为:以每个点为「中转站」,刷新所有边的距离。

如果要求解某一出发点出发的最短路径,固定出发点k,从所有 w[k][x] 中取 max 即是「从 k点出发,到其他点 x 的最短距离的最大值」。

Floyd 也是基于动态规划,其原始的三维状态定义为 f[i][j][k] 代表从点 i 到点 j,且经过的所有点编号不会超过 k(即可使用点编号范围为[1,k])的最短路径。这样的状态定义引导我们能够使用 Floyd 求最小环或者求“重心点”(即删除该点后,最短路值会变大)。

// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
// 通常根据数据范围N,开辟得较大
    int[][] w = new int[N][N];
    int INF = 0x3f3f3f3f;
    
// 初始化邻接矩阵,根据已有的节点数n初始化
   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:times){
        int u=t[0],v=t[1],w=t[2];
        w[u][v]=w;
    }    
void floyd() {
    // floyd 基本流程为三层循环:
    // 枚举中转点 - 枚举起点 - 枚举终点 - 松弛操作,遍历已有的节点数为n的网络      
    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^3)
空间复杂度:O(N^2)

二、朴素 Dijkstra(邻接矩阵)

「单源最短路」算法朴素 Dijkstra 算法进行求解时,使用「邻接矩阵」来进行存图。跑一遍 Dijkstra 我们可以得到从源点 k 到其他点 x 的最短距离,再从所有最短路中取 max 即是「从 k 点出发,到其他点 x 的最短距离的最大值」。

可以用于加权图,没有负权边,可以有环。

算法要点为:每次从 「未求出最短路径的点」中 取出 距离距离起点 最小路径的点,以这个点为桥梁, 刷新「未求出最短路径的点」的距离。

代码实现思路:

  1. 初始化距离
    用一维数组int dist[]初始化确定的起点到其他点的距离,这个数据结构在前面存图的w[][]以后建立。只有起点确定dist[1] = 0, dist[i] = +∞

  2. 迭代更新
    整个过程可以看为两个不相交的集合,
    集合S:当前已经确定从起点出发到其他点的最短距离,即已经选出的点,通过vis数组标记已取出
    集合TS的补集,即还未选出的点。
    for(int i = 1; i <= n; i ++) for循环n次,每次选出一个最短距离点t,将此点标记为已访问,并用此点更新ST中的所有点的最短距离。对于t,若dist[i]>dist[t]+w[t][i],则可以用新选出来的点进行距离更新。
    在这里插入图片描述

// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
    int[][] w = new int[N][N];
    int INF = 0x3f3f3f3f;
    
// 初始化邻接矩阵
   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:times){
        int u=t[0],v=t[1],w=t[2];
        w[u][v]=w;
    }    
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 的「最小距离」更新其他点,这个过程如果w[i][j]有边连接,就能更新
       // dist[k]刚开始为0,必然可以更新与其相近的其他点
       for (int i = 1; i <= n; i++) {
           dist[i] = Math.min(dist[i], dist[t] + w[t][i]);
       }
   }
}

时间复杂度:O(N^2)
空间复杂度:O(N^2)

三、堆优化 Dijkstra(邻接表)

堆优化 Dijkstra 算法与朴素 Dijkstra 都是「单源最短路」算法。朴素Dijkstra 的方法是遍历所有的点通过比较找出最近的点,在这个地方可以使用 优先队列 来进行优化,通过 优先队列 优化后 朴素Dijkstra算法 就叫做 堆优化版的Dijkstra算法

首先将优先队列定义成小根堆,将与出发点的距离越小的节点放在队头。首先将出发点初始化为(点编号, 到起点的距离)new int[]{k, 0}加入到优先队列中que.add(new int[]{k, 0}),然后从这个点开始扩展。先将队头元素出队,标记为已挑选为最短边节点,同时注意「重边」问题(下面第二段有解释),然后遍历这个点的所有出边所到达的点 je[i]所存的指向点),更新所有点距离源点更近的距离dist[j]

如果源点直接到 j 点的距离比源点先到最短点id 点再从 id 点j 点的距离大,那么就更新 dist[j],使 dist[j] 到源点的距离最短,并将该点的编号以及该点到源点的距离作为一个 new int[]{j, dist[j]} 加入到优先队列中,然后将其标记,表示该点已经确定最短距离。因为是小根堆,所以会根据距离进行排序,距离最短的点总是位于队头。一直扩展下去,直到队列为空。

因为有 「重边」 的缘故,所以从该点拓展出来的边可能会有冗余数据,即如果在扩展idx=1的时候,第一次遍历到的点是 2 号点,距离 源点 的距离为 10,此时 dist[2] = 0x3f3f3f3f > dist[1] + distance[1 -> 2] = 0 + 10 = 10 所以 dist[2] 会被更新为 10,此时会将 {2, 10} 入队。但是很不巧从 源点 到 2 号点有一个距离为 6 的重边,当遍历到这个重边时,由于 dist[2] = 10 > dist[1] + distance[1 -> 2] = 0 + 6 = 6,所以 {2, 6} 也入队了,入队之后由于是小根堆按照距离源点的距离由小到大排序,所以 {2, 6} 会排在 {2, 10} 前面,所以 {2, 6}距离短的会先出队,出队之后2节点会被标记。所以当下一次再遇到已经被标记的 2 号点时,直接 continue 忽略掉继续扩展下一个点即可。

class Solution {
    int INF=0x3f3f3f3f;
    int N=110;
    int M=6010;
    int[]head=new int[N];
    int[]edge=new int[M];
    int[]nextEdge=new int[M];
    int[]w=new int[M];
    int[]dis=new int[N];
    boolean[]vis=new boolean[N];
    int idx=0;
    int n,k;
    public void add(int a,int b,int c){
        edge[idx]=b;
        nextEdge[idx]=head[a];
        head[a]=idx;
        w[idx]=c;
        idx++;
    }
    public int networkDelayTime(int[][] times, int _n, int _k) {
        n=_n;
        k=_k;
        Arrays.fill(head,-1);
        for(int[]t:times){
            int u=t[0],v=t[1],c=t[2];
            add(u,v,c);
        }
        pqDijkstra();
        int ans=0;
        for(int i=1;i<=n;i++){
            ans=Math.max(ans,dis[i]);
        }
        return ans>INF/2?-1:ans;
    }
    public void pqDijkstra(){
        Arrays.fill(vis,false);
        Arrays.fill(dis,INF);
        dis[k]=0;
        PriorityQueue<int[]> que=new PriorityQueue<>((a,b)->a[1]-b[1]);
        que.offer(new int[]{k,0});
        while(!que.isEmpty()){
            int[]top=que.poll();
            int id=top[0];

            //进行访问标记与忽略
            if(vis[id]) continue;
            vis[id]=true;
            for(int i=head[id];i!=-1;i=nextEdge[i]){
                //和最短边相连的所有边,判断距离是否进行更新
                int j=edge[i];
                if(dis[j]>dis[id]+w[i]){
                    dis[j]=dis[id]+w[i];
                    //将原先为INF,现在已经被更新的点加入到que中
                    que.offer(new int[]{j,dis[j]});
                }
            }
        }
    }
}

时间复杂度:总共需要遍历 m 条边,插入数据修改小根堆的时间复杂度为O(logN),所以时间复杂度为O(mlogn)。因为对于 「稀疏图」来说边数与点数很接近,所以可以看做为O(nlogn)。但是对于「稠密图」来说边数接近点数的平方个,如果「稠密图」使用堆优化版的Dijkstra算法,那么时间复杂度将会是O(n^2logn),显然不如直接使用朴素Dijkstra算法。所以堆优化版的Dijkstra算法更适用于「稀疏图」 ,而朴素Dijkstra算法更适用于「稠密图」。

四、Bellman Ford(邻接表&邻接矩阵)

Bellman Ford可以用于在「负权重图中求最短路」,该算法也是「单源最短路」算法。

Bellman Ford基于动态规划,其原始的状态定义为 f[i][k] 代表从起点到 i 点,且经过最多 k 条边的最短路径。这样的状态定义引导我们能够使用 Bellman Ford 来解决有边数限制的最短路问题。

其中涉及「迭代操作」,其定义为,每次都遍历图中的所有边,对每条边(的两个端点)都进行松弛操作。

涉及到「松弛操作」,d[to] = min(d[to], d[from] + w[from][to])或者w[i][j] = Math.min(w[i][j], w[i][p] + w[p][j]),我们可以理解其目的是为了发现距离尽可能小的路径。「每一次成功的松弛操作,都意味着我们发现了一条新的最短路」。

别人给出的总结值得在题目中慢慢体会:

  1. 只有上一次迭代中松弛过的点才有可能参与下一次迭代的松弛操作,这里的参与指的是让临近节点的dist[i]改变。这个定理很容易理解,上一次迭代改变了某些点的dist[i]才会让它周围的点在下一轮更新距离中收到影响。
  2. 迭代的实际意义:每次迭代k中,我们找到了经历了k条边的最短路。这个可以从源点处开始的前几轮迭代中理解,参考深入理解Bellman-Ford(SPFA)算法中的作图这点需要重点理解,是我们解题循环体设置的前提。
  3. “没有点能够被松弛”时,迭代结束。这个松弛次数也可以通过题目的约束条件设置,如787.K站中转内最便宜的航班中根据中转次数定下迭代边数。

数组实现邻接表存图:

class Solution {
    int INF=0x3f3f3f3f;
    int N=110;
    int M=6010;
    int[]head=new int[N];
    int[]edge=new int[M];
    int[]nextEdge=new int[M];
    int[]w=new int[M];
    int[]dis=new int[N];
    int idx=0;
    int n,k;
    public void add(int a,int b,int c){
        edge[idx]=b;
        nextEdge[idx]=head[a];
        head[a]=idx;
        w[idx]=c;
        idx++;
    }
    public int networkDelayTime(int[][] times, int _n, int _k) {
        n=_n;
        k=_k;
        Arrays.fill(head,-1);
        for(int[]t:times){
            int u=t[0],v=t[1],c=t[2];
            add(u,v,c);
        }
        bf();
        int ans=0;
        for(int i=1;i<=n;i++){
            ans=Math.max(ans,dis[i]);
        }
        return ans>INF/2?-1:ans;
    }
    public void bf(){
        Arrays.fill(dis,INF);
        dis[k]=0;
        //迭代n次,每次都使用上一次的结果进行松弛操作
        for(int p=1;p<=n;p++){
            int[]prev=dis.clone();
            //遍历所有节点的所有边,边索引为j
            for(int i=1;i<=n;i++){
                for(int j=head[i];j!=-1;j=nextEdge[j]){
                    int b=edge[j];
                    dis[b]=Math.min(dis[b],prev[i]+w[j]);
                }
            }
        }
    }
}

时间复杂度:O(n*m)相当于是迭代n次,每次遍历所有的边进行松弛操作,更新dis[]

邻接矩阵存图:

class Solution {
    int N=110;
    int[][]g=new int[N][N];
    int[]dis=new int[N];
    int INF=0x3f3f3f3f;
    int n,k,src,dst;
    public int findCheapestPrice(int _n, int[][] flights, int _src, int _dst, int _k) {
        n=_n;
        k=_k;
        src=_src;
        dst=_dst;
        //题目中的序号从0开始
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                g[i][j]=g[j][j]=i==j?0:INF;
        for(int[]f:flights){
            int u=f[0],v=f[1],c=f[2];
            g[u][v]=c;
        }
        int ans=df();
        return ans>INF/2?-1:ans;
    }
    public int df(){
        Arrays.fill(dis,INF);
        dis[src]=0;
        //松弛k+1次,对应k+1条边
        for(int p=1;p<=k+1;p++){
            int[]prev=dis.clone();
            for(int i=0;i<n;i++){
                for(int j=0;j<n;j++){
                    dis[j]=Math.min(dis[j],prev[i]+g[i][j]);
                }
            }
        }
        return dis[dst];
    }
}

时间复杂度:O(k*n^2)相当于是迭代k次,每次遍历所有的点进行松弛操作,更新dis[]

五、BFS求最短路及其在内向基环树中的应用

BFS适用于「无权」图(每条边权值为1),可以有环,是单源最短路算法,是很经典的论题了。图中有环会出现一个节点被多次访问的情况,为了求最短路径避免路径长度被反复更新,需要有一个访问数组来标记访问状态,这里在进队前就标记访问。

参考数据结构笔记——最短路径BFS算法队列模板如:
标记重复访问两种方式:

  • 访问数组
  • 是否是初始化的-1操作

距离更新有两种方式:

  • 前面节点的距离+1
  • 用BFS当前圈数作为距离

采用访问数组标记,距离按前一个点+1计算:

//求顶点u到其他顶点的最短路径
void BFS_Distance(Graph G,int u){
    for(i = 0;i < G.vexnum;++i){
        d[i] = w;
        path[i] = -1;
    }
    d[u] = 0;
    visited[u] = TRUE;
    EnQueue(Q,u);
    while(!isEmpty(Q)){
        DeQueue(Q,u);
        for(w = FirstNeighbor(G,u);w >= 0;w = NextNeighbor(G,u,w))
            if(!visited[w]){
                d[w] = d[u] + 1;
                path[w] = u;
                //访问数组访问标记
                visited[w] = TRUE;
                EnQueue(Q,w);
        }//if
    }//while
}

采用距离数组初始化-1判断访问,距离按前一个点+1计算:

public void bfs(int node,int[]edges,int[]dis){
        ArrayDeque<Integer> que=new ArrayDeque<>();
        que.offer(node);
        dis[node]=0;
        while(!que.isEmpty()){
            int size=que.size();
            for(int i=0;i<size;i++){
                int top=que.poll();
                //下一个点没有被访问过
                if(edges[top]!=-1&&dis[edges[top]]==-1){
                    dis[edges[top]]=dis[top]+1;
                    que.offer(edges[top]);
                }
            }
        }
    }

采用距离数组初始化-1判断访问,距离按圈数增加计算:

public void bfs(int node,int[]edges,int[]dis){
        ArrayDeque<Integer> que=new ArrayDeque<>();
        que.offer(node);
        dis[node]=0;
        int cur=0;
        while(!que.isEmpty()){
            int size=que.size();
            cur++;
            for(int i=0;i<size;i++){
                int top=que.poll();
                if(edges[top]!=-1&&dis[edges[top]]==-1){
                    dis[edges[top]]=cur;
                    que.offer(edges[top]);
                }
            }
        }
    }

基环树是一种特殊的图,可以理解为「树加一边」使之成环,每个节点只有「一条出边」,即只有一个子代节点,这可以和一个节点对应多个节点的多叉树以及无向图做区分。参考基环树,基环内向树,基环外向树内向基环树。由于每个节点至多有一个子代节点,因此可根据题意直接迭代节点的子节点即可。

public void bfs(int node,int[]edges,int[]dis){
   //当前点没有被访问过,在不是最后一个点以及有环的情况下一直算dis
   for(int d=0;node!=-1&&dis[node]==-1;node=edges[node]){
       dis[node]=d++;
   }
}

时间复杂度:O(N)

相关题目

743.网络延迟时间

套用最短路模板解题即可。
Floyd解法:

class Solution {
    int N=110;
    int M=6010;
    int[][]w=new int[N][N];
    int INF=0x3f3f3f3f;
    int n,k;
    public int networkDelayTime(int[][] times, 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:times){
            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;
    }
    public void floyd(){
        for(int k=1;k<=n;k++){
            for(int i=1;i<=n;i++){
                for(int j=1;j<=n;j++){
                    w[i][j]=Math.min(w[i][j],w[i][k]+w[k][j]);
                }
            }
        }
    }
}

朴素Dijkstra:

class Solution {
    int N=110;
    int M=6010;
    int[][]w=new int[N][N];
    int n,k;
    int INF=0x3f3f3f3f;
    boolean[]vis=new boolean[N];
    //dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
    int[]dis=new int[N];
    public int networkDelayTime(int[][] times, 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:times){
            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,dis[i]);
        }
        return ans>INF/2?-1:ans;
    }
    public void dijkstra(){
        Arrays.fill(vis,false);
        Arrays.fill(dis,INF);
        // 只有起点最短距离为 0
        dis[k]=0;
        //每次选出一个点
        for(int p=1;p<=n;p++){
            // 每次找到「最短距离最小」且「未被更新」的点 t
            int t=-1;
            //找出未选出的最短边点
            for(int i=1;i<=n;i++){
                if(!vis[i]&&(t==-1||dis[t]>dis[i])) t=i;
            }
            // 标记点 t 为已更新
            vis[t]=true;
            for(int i=1;i<=n;i++){
                dis[i]=Math.min(dis[i],dis[t]+w[t][i]);
            }
        }
    }
}

堆优化Dijkstra:

class Solution {
    int INF=0x3f3f3f3f;
    int N=110;
    int M=6010;
    int[]head=new int[N];
    int[]edge=new int[M];
    int[]nextEdge=new int[M];
    int[]w=new int[M];
    int[]dis=new int[N];
    boolean[]vis=new boolean[N];
    int idx=0;
    int n,k;
    public void add(int a,int b,int c){
        edge[idx]=b;
        nextEdge[idx]=head[a];
        head[a]=idx;
        w[idx]=c;
        idx++;
    }
    public int networkDelayTime(int[][] times, int _n, int _k) {
        n=_n;
        k=_k;
        Arrays.fill(head,-1);
        for(int[]t:times){
            int u=t[0],v=t[1],c=t[2];
            add(u,v,c);
        }
        pqDijkstra();
        int ans=0;
        for(int i=1;i<=n;i++){
            ans=Math.max(ans,dis[i]);
        }
        return ans>INF/2?-1:ans;
    }
    public void pqDijkstra(){
        Arrays.fill(vis,false);
        Arrays.fill(dis,INF);
        dis[k]=0;
        PriorityQueue<int[]> que=new PriorityQueue<>((a,b)->a[1]-b[1]);
        que.offer(new int[]{k,0});
        while(!que.isEmpty()){
            int[]top=que.poll();
            int id=top[0];

            //进行访问标记与忽略
            if(vis[id]) continue;
            vis[id]=true;
            for(int i=head[id];i!=-1;i=nextEdge[i]){
                //和最短边相连的所有边,判断距离是否进行更新
                int j=edge[i];
                if(dis[j]>dis[id]+w[i]){
                    dis[j]=dis[id]+w[i];
                    //将原先为INF,现在已经被更新的点加入到que中
                    que.offer(new int[]{j,dis[j]});
                }
            }
        }
    }
}

Bellman Ford解法:

class Solution {
    int INF=0x3f3f3f3f;
    int N=110;
    int M=6010;
    int[]head=new int[N];
    int[]edge=new int[M];
    int[]nextEdge=new int[M];
    int[]w=new int[M];
    int[]dis=new int[N];
    int idx=0;
    int n,k;
    public void add(int a,int b,int c){
        edge[idx]=b;
        nextEdge[idx]=head[a];
        head[a]=idx;
        w[idx]=c;
        idx++;
    }
    public int networkDelayTime(int[][] times, int _n, int _k) {
        n=_n;
        k=_k;
        Arrays.fill(head,-1);
        for(int[]t:times){
            int u=t[0],v=t[1],c=t[2];
            add(u,v,c);
        }
        bf();
        int ans=0;
        for(int i=1;i<=n;i++){
            ans=Math.max(ans,dis[i]);
        }
        return ans>INF/2?-1:ans;
    }
    public void bf(){
        Arrays.fill(dis,INF);
        dis[k]=0;
        //迭代n次,每次都使用上一次的结果进行松弛操作
        for(int p=1;p<=n;p++){
            int[]prev=dis.clone();
            //遍历所有节点的所有边,边索引为j
            for(int i=1;i<=n;i++){
                //通过节点编号找到与其相连的所有边
                for(int j=head[i];j!=-1;j=nextEdge[j]){
                    int b=edge[j];
                    dis[b]=Math.min(dis[b],prev[i]+w[j]);
                }
            }
        }
    }
}
787.K站中转内最便宜的航班

相比于经典的单源最短路和多源最短路问题,多了个中转的限制。采用Bellman Ford,「限制最多经过不超过 k 个点」等价于「限制最多不超过 k+1 条边」。

用邻接矩阵存图,在k+1次松弛中,每次都遍历全图,对dis[]进行更新。

class Solution {
    int N=110;
    int[][]g=new int[N][N];
    int[]dis=new int[N];
    int INF=0x3f3f3f3f;
    int n,k,src,dst;
    public int findCheapestPrice(int _n, int[][] flights, int _src, int _dst, int _k) {
        n=_n;
        k=_k;
        src=_src;
        dst=_dst;
        //题目中的序号从0开始
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                g[i][j]=g[j][j]=i==j?0:INF;
        for(int[]f:flights){
            int u=f[0],v=f[1],c=f[2];
            g[u][v]=c;
        }
        int ans=df();
        return ans>INF/2?-1:ans;
    }
    public int df(){
        Arrays.fill(dis,INF);
        dis[src]=0;
        //松弛k+1次,对应k+1条边
        for(int p=1;p<=k+1;p++){
            int[]prev=dis.clone();
            for(int i=0;i<n;i++){
                for(int j=0;j<n;j++){
                    dis[j]=Math.min(dis[j],prev[i]+g[i][j]);
                }
            }
        }
        return dis[dst];
    }
}

更进一步,由于 Bellman Ford 核心操作是每次迭代中需要遍历所有的边,因此也可以直接使用flights数组进行图遍历,而无须额外存图。

class Solution {
    int N=110;
    int[]dis=new int[N];
    int INF=0x3f3f3f3f;
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
       Arrays.fill(dis,INF);
       dis[src]=0;
       for(int p=0;p<k+1;p++){
           int[]prev=dis.clone();
           for(int[]f:flights){
               int x=f[0],y=f[1],c=f[2];
               //在flights中存的起点和终点之间更新
               dis[y]=Math.min(dis[y],prev[x]+c);
           }
       }
       int ans=dis[dst];
       return ans>INF/2?-1:ans;
    }
}
1976.到达目的地的方案数

参考题解【宫水三叶】图论综合题 : 朴素 Dijkstra + 拓扑排序 + DP

先用邻接矩阵存储稠密图,然后采用单源最短路算法Dijsktra进行最短路求解。根据最短距离数组int[] dist重建最短路的关键路径图,在此图上跑拓扑排序,结合动态规划统计方案数,排序节点上为到该节点的最短路径方案数。

class Solution {
    int N=210;
    int MOD=(int)1e9+7;
    long[][]g=new long[N][N];
    //这里需要保证所有路径加起来的和不超过INF,INF大于等于200*1E9
    long INF=(long)1e12;
    boolean[]vis=new boolean[N];
    long[]dis=new long[N];
    int[]in=new int[N];
    int n;
    public int countPaths(int _n, int[][] roads) {
        n=_n;
        //dijkstra求出最短路径长度数组dis
        for(int i=0;i<n;i++){
            for(int j=0;j<n;j++){
                g[i][j]=g[j][i]=i==j?0:INF;
            }
        }
        for(int[]r:roads){
            int u=r[0],v=r[1],c=r[2];
            g[u][v]=g[v][u]=c;
        }
        dijkstra();

        //根据最短长度数组重建图,方便拓扑排序
        HashSet<Integer>[] adj=new HashSet[N];
        for(int i=0;i<N;i++){
            adj[i]=new HashSet<>();
        }
        for(int[]r:roads){
            int u=r[0],v=r[1],c=r[2];
            g[u][v]=g[v][u]=c;
            if(dis[u]+c==dis[v]){
                adj[u].add(v);
                in[v]++;
            }
            if(dis[v]+c==dis[u]){
                adj[v].add(u);
                in[u]++;
            }
        }

        //f[i]表示在最短路径图中,到路口编号为i的方案数为f[i]
        long[]f=new long[N];
        f[0]=1;
        //入度为0的点先入队列,在该题中只有源点符合条件
        Deque<Integer> que=new ArrayDeque<>();
        que.offer(0);
        while(!que.isEmpty()){
            int top=que.poll();
            for(int i:adj[top]){
                f[i]+=f[top];
                f[i]%=MOD;
                if(--in[i]==0) que.offer(i);
            }
        }

        return (int)f[n-1];
    }

    public void dijkstra(){
        Arrays.fill(vis,false);
        Arrays.fill(dis,INF);
        dis[0]=0;
        //迭代n次,选出n个点
        for(int p=0;p<n;p++){
            int t=-1;
            //第一次通常选出前面赋值的源点dis[0]=0;
            for(int i=0;i<n;i++){
                if(!vis[i]&&(t==-1||dis[i]<dis[t])){
                    t=i;
                }
            }
            vis[t]=true;
            for(int i=0;i<n;i++){
                dis[i]=Math.min(dis[i],dis[t]+g[t][i]);
            }
        }
    }
}
2359.找到离给定两个节点最近的节点

无权图有环,可以尝试BFS求最短路算法,而不用小题大做用其他有权图算法。采用距离数组-1做访问判断,距离按圈数增加+1。

class Solution {
    public int closestMeetingNode(int[] edges, int node1, int node2) {
        int len=edges.length;
        int[] dis1=new int[len];
        int[] dis2=new int[len];
        Arrays.fill(dis1,-1);
        Arrays.fill(dis2,-1);
        bfs(node1,edges,dis1);
        bfs(node2,edges,dis2);
        int minDis=len;
        int index=-1;
        for(int i=0;i<len;i++){
            if(dis1[i]==-1||dis2[i]==-1) continue;
            int maxD=Math.max(dis1[i],dis2[i]);
            if(maxD<minDis){
                minDis=maxD;
                index=i;
            }
        }
        return index;
    }

    public void bfs(int node,int[]edges,int[]dis){
        ArrayDeque<Integer> que=new ArrayDeque<>();
        que.offer(node);
        dis[node]=0;
        int cur=0;
        while(!que.isEmpty()){
            int size=que.size();
            cur++;
            for(int i=0;i<size;i++){
                int top=que.poll();
                if(edges[top]!=-1&&dis[edges[top]]==-1){
                    dis[edges[top]]=cur;
                    que.offer(edges[top]);
                }
            }
        }
    }
}

这道题目给的是内向基环树,可以采用一个循环求最短路径的方法,

class Solution {
    public int closestMeetingNode(int[] edges, int node1, int node2) {
        int len=edges.length;
        int[] dis1=new int[len];
        int[] dis2=new int[len];
        Arrays.fill(dis1,-1);
        Arrays.fill(dis2,-1);
        bfs(node1,edges,dis1);
        bfs(node2,edges,dis2);
        int minDis=len;
        //不存在返回-1
        int index=-1;
        for(int i=0;i<len;i++){
            if(dis1[i]==-1||dis2[i]==-1) continue;
            int maxD=Math.max(dis1[i],dis2[i]);
            if(maxD<minDis){
                minDis=maxD;
                index=i;
            }
        }
        return index;
    }

    public void bfs(int node,int[]edges,int[]dis){
        //在不是最后一个点以及有环的情况下一直算dis
        for(int d=0;node!=-1&&dis[node]==-1;node=edges[node]){
            dis[node]=d++;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

互联网民工蒋大钊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值