图论进阶之路-最短路模版(基于小根堆实现)

最近打比赛遇到了大量的最短路问题,在这里总结一下最短路最常用的模版,基本可以应付大多数的最短路问题了

首先先为大家介绍一道纯模版题目,没有加任何其他限制条件:

743. 网络延迟时间

有 n 个网络节点,标记为 1 到 n

给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。

现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

 

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

 

示例 2:

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

示例 3:

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

 

提示:

  • 1 <= k <= n <= 100
  • 1 <= times.length <= 6000
  • times[i].length == 3
  • 1 <= ui, vi <= n
  • ui != vi
  • 0 <= wi <= 100
  • 所有 (ui, vi) 对都 互不相同(即,不含重复边)

这是一道典型的模版题,我给出了基于小根堆的实现方案,并给每一步的作用注释了详解

class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {
        //记录所有结点,将所有结点的邻接边以及对应权值挂在结点下
        List<List<int[]>> lingjie = new ArrayList<>();
        //创建1到n的结点
        for(int i=0;i<=n;i++){
            lingjie.add(new ArrayList<int[]>());
        }
        //为每一个结点添加邻接边以及其权值,0代表边,1代表权值
        for(int[] edge : times){
            lingjie.get(edge[0]).add(new int[]{edge[1],edge[2]});
        }
        //记录从源点到索引结点的最短距离,初始化默认无穷远
        int[] distance = new int[n+1];
        Arrays.fill(distance,Integer.MAX_VALUE);
        distance[k] = 0;
        //记录已经计算过最短距离的结点(从堆顶弹出就代表已经计算过了,不可能找更短的路径了)
        boolean[] visited = new boolean[n+1];
        //创建一个小顶堆维护当前结点的最短路径
        //0,代表结点,1,代表最短路径
        PriorityQueue<int[]> dui = new PriorityQueue<>((a,b)->a[1]-b[1]);
        dui.offer(new int[]{k,0});
        //不断将堆顶结点弹出,将其所有邻接边拿到并对其接应结点的最短路更新
        while(!dui.isEmpty()){
            int node = dui.poll()[0];
            //如果该结点的邻接点已经处理过了,就不能再处理了
            if(visited[node]){
                continue;
            }
            visited[node] = true;
            //处理该结点的所有邻接边
            for(int[] edges : lingjie.get(node)){
                //若当前结点到相邻结点能够使得相邻结点的路径更短,就更新为较短的路径
                if(!visited[edges[0]] && edges[1] + distance[node] < distance[edges[0]]){
                    distance[edges[0]] = edges[1] + distance[node];
                    dui.offer(new int[]{edges[0],distance[edges[0]]});
                }
            }
        }
        //此时所有点的最短路都已经被求出,我们返回最大值即可,若无法达到则是Integer.MAXVALUE
        int ans = -1;
        for(int i=1;i<=n;i++){
            ans = Math.max(ans,distance[i]);
        }
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }
}

将这个模版记住就能够应付大多树的最短路图论问题了,很多题都是这个模版的变形

就比如 前几天 (2024.4.13) 的第十五届蓝桥杯 Java B 组的第六题:

第十五届蓝桥杯 javaB组 第六题

小明国庆节准备去某星系进行星际旅行,这个星系里一共有 n 个星球,其中布置了 m 道双向传送门,第 i 道传送门可以连接 ai,bi 两颗星球(ai , bi 且任意两颗星球之间最多只有一个传送门)。

他看中了一款 “旅游盲盒”,一共有 Q 个盲盒,第 i 个盲盒里的旅行方案规定了旅行的起始星球 xi 和最多可以使用传送门的次数 yi。只要从起始星球出发,使用传送门不超过规定次数能到达的所有星球都可以去旅行。

小明关心在每个方案中有多少个星球可以旅行到。小明只能在这些盲盒里随机选一个购买,他想知道能旅行到的不同星球的数量的期望是多少。


输入格式
输入共 m + Q + 1 行。

第一行为三个正整数 n,m,Q。

后面 m 行,每行两个正整数 ai,bi。

后面 Q 行,每行两个整数 xi,yi。

输出格式
输出共一行,一个浮点数(四舍五入保留两位小数)。


样例输入
3 2 3
1 2
2 3
2 1
2 0
1 1

样例输出
2.00


提示
【样例说明】

第一个盲盒可以旅行到 1, 2, 3。

第二个盲盒可以旅行到 2。

第三个盲盒可以旅行到 1, 2。

所以期望是 (3 + 1 + 2)/3 = 2.00。


【评测用例规模与约定】

对于 20% 的评测用例,保证 n ≤ 300。

对于 100% 的评测用例,保证 n ≤ 1000,m ≤ min{n(n−1)/2, 5n},Q ≤ 50000,0 ≤ yi ≤ n。

这题与模版题的区别在于单向边变双向边,其余的做法跟模版一模一样,只需在求出所有结点的最短路后计算小于等于限制的点个数即可,再用一个 dp表 记录已经搜索过的起始结点降低时间复杂度

import java.util.*;

public class Main {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt(),m = sc.nextInt(),q = sc.nextInt();
		List<List<Integer>> graph = new ArrayList<>();
		for(int i=0;i<=n;i++){
			graph.add(new ArrayList<Integer>());
		}
		for(int i=0;i<m;i++){
			int a = sc.nextInt(),b = sc.nextInt(); 
			graph.get(a).add(b);
			graph.get(b).add(a);
		}
		int[][] dp = new int[n+1][n+1];//dp表记录已经搜过的起始位置对应的distance数组
		Arrays.fill(dp, null);
		long ans = 0;
		for(int i=0;i<q;i++){
			//起始点
			int s = sc.nextInt();
			//限制
			int limit = sc.nextInt();
			if(dp[s] != null){
				ans += count(dp[s],limit);
				continue;
			}
			//开始计算最短路
			int[] distance = new int[n+1];
			Arrays.fill(distance, Integer.MAX_VALUE);
			boolean[] vsited = new boolean[n+1];
			PriorityQueue<int[]> dui = new PriorityQueue<>((a,b)->(a[1]-b[1]));
			distance[s] = 0;
			dui.offer(new int[]{s,0});
			while(!dui.isEmpty()){
				int node = dui.poll()[0];
				if(vsited[node]){
					continue;
				}
				vsited[node] = true;
				for(Integer to : graph.get(node)){
					if(!vsited[to] && distance[node]+1 < distance[to]){
						distance[to] = distance[node]+1;
						int[] a = new int[2];
						a[0] = to;
						a[1] = distance[to];
						dui.offer(a);
					}
				}
			}
			ans += count(distance,limit);
			dp[s] = distance;
		}
		sc.close();
		System.out.printf("%.2f",ans/(double)q);
	}
	static int count(int[] dis,int limit){
		int count = 0;
		for(int n : dis){
			if(n <= limit){
				count++;
			}
		}
		return count;
	}
}

还有最近的一次力扣双周赛的倒数第二题,也是一道最短路问题:

3112. 访问消失节点的最少时间

给你一个二维数组 edges 表示一个 n 个点的无向图,其中 edges[i] = [ui, vi, lengthi] 表示节点 ui 和节点 vi 之间有一条需要 lengthi 单位时间通过的无向边。

同时给你一个数组 disappear ,其中 disappear[i] 表示节点 i 从图中消失的时间点,在那一刻及以后,你无法再访问这个节点。

注意,图有可能一开始是不连通的,两个节点之间也可能有多条边。

请你返回数组 answer ,answer[i] 表示从节点 0 到节点 i 需要的 最少 单位时间。如果从节点 0 出发 无法 到达节点 i ,那么 answer[i] 为 -1 。

 

示例 1:

输入:n = 3, edges = [[0,1,2],[1,2,1],[0,2,4]], disappear = [1,1,5]

输出:[0,-1,4]

解释:

我们从节点 0 出发,目的是用最少的时间在其他节点消失之前到达它们。

  • 对于节点 0 ,我们不需要任何时间,因为它就是我们的起点。
  • 对于节点 1 ,我们需要至少 2 单位时间,通过 edges[0] 到达。但当我们到达的时候,它已经消失了,所以我们无法到达它。
  • 对于节点 2 ,我们需要至少 4 单位时间,通过 edges[2] 到达。

示例 2:

 

d51221a659c989d6a86c30585b7547f2.png

输入:n = 3, edges = [[0,1,2],[1,2,1],[0,2,4]], disappear = [1,3,5]

输出:[0,2,3]

解释:

我们从节点 0 出发,目的是用最少的时间在其他节点消失之前到达它们。

  • 对于节点 0 ,我们不需要任何时间,因为它就是我们的起点。
  • 对于节点 1 ,我们需要至少 2 单位时间,通过 edges[0] 到达。
  • 对于节点 2 ,我们需要至少 3 单位时间,通过 edges[0] 和 edges[1] 到达。

示例 3:

输入:n = 2, edges = [[0,1,1]], disappear = [1,1]

输出:[0,-1]

解释:

当我们到达节点 1 的时候,它恰好消失,所以我们无法到达节点 1 。

 

提示:

  • 1 <= n <= 5 * 104
  • 0 <= edges.length <= 105
  • edges[i] == [ui, vi, lengthi]
  • 0 <= ui, vi <= n - 1
  • 1 <= lengthi <= 105
  • disappear.length == n
  • 1 <= disappear[i] <= 105

与模版题不同的地方在于双向边,结点加了会消失的限制

我们只需在结点搜索其连接的边之前先判断该结点是否已经消失即可

class Solution {
    public int[] minimumTime(int n, int[][] edges, int[] disappear) {
        int[] distance = new int[n];
        Arrays.fill(distance,Integer.MAX_VALUE);
        distance[0] = 0;
        boolean[] visted = new boolean[n];
        List<List<int[]>> graph = new ArrayList<>();
        for(int i=0;i<n;i++){
            graph.add(new ArrayList<int[]>());
        }
        for(int[] edge : edges){
            graph.get(edge[0]).add(new int[]{edge[1],edge[2]});
            graph.get(edge[1]).add(new int[]{edge[0],edge[2]});
        }
        PriorityQueue<int[]> dui = new PriorityQueue<>((a,b)->a[1]-b[1]);
        dui.offer(new int[]{0,0});
        int time = 0;
        while(!dui.isEmpty()){
            int node = dui.poll()[0];
            if(visted[node]){
                continue;
            }
            visted[node] = true;
            //在每一次搜集邻接表之前先判断该结点是否已经消失
            if(distance[node] >= disappear[node]){
                distance[node] = -1;
                continue;
            }
            for(int[] edge : graph.get(node)){
                int u = edge[0],v = edge[1];
                if(!visted[u] && distance[node]+v < distance[u]){
                    distance[u] = distance[node]+v;
                    dui.offer(new int[]{u,distance[u]});
                }
            }
        }
        //将正无穷的不可到达结点设置为 -1 
        for(int i=0;i<n;i++){
            if(distance[i] == Integer.MAX_VALUE){
                distance[i] = -1;
            }
        }
        return distance;
    }
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值