最近打比赛遇到了大量的最短路问题,在这里总结一下最短路最常用的模版,基本可以应付大多数的最短路问题了
首先先为大家介绍一道纯模版题目,没有加任何其他限制条件:
有
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 组的第六题:
小明国庆节准备去某星系进行星际旅行,这个星系里一共有 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;
}
}
还有最近的一次力扣双周赛的倒数第二题,也是一道最短路问题:
给你一个二维数组
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:
输入: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;
}
}