目录
SPFA算法(邻接表+队列,Bellman算法优化而来,与Dijkstra算法很像)
最短路问题总结:
BFS
处理边权全为1的图
朴素Dijstra算法(邻接矩阵)
时间复杂度:
O(n^2)
算法思路:
- dist[ ] :存储最短距离
- S:装入已确定最短路的点
初始化:
伪代码:
for(1~n) // 循环n次,每次确定一个点的最短距离
for(1~n) // 找出n个点中距离最短的那个点
t <-- 找出不在S中距离最近的点
S <-- t
更新与t点有关其他点的最短距离
例题(简单) 849. Dijkstra求最短路 I - AcWing题库
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n≤500
1≤m≤10^5
图中涉及边长均不超过10000。输入样例:
3 3 1 2 2 2 3 1 1 3 4
输出样例:
3
import java.io.*;
import java.util.*;
class Main{
static int N = 510;
static int n,m;
static int[][] g = new int[N][N]; // 邻接矩阵
static int[] dist = new int[N]; // 存储与1的最短距离
static boolean[] st = new boolean[N]; // 相当与S,判断是否确定最短距离
public static void main(String[] args) throws IOException{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String[] s = in.readLine().split(" ");
n = Integer.parseInt(s[0]);
m = Integer.parseInt(s[1]);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
g[i][j] = 0x3f3f3f3f;
while(m-->0){
s = in.readLine().split(" ");
int a = Integer.parseInt(s[0]);
int b = Integer.parseInt(s[1]);
int c = Integer.parseInt(s[2]);
g[a][b] = Math.min(g[a][b],c);
}
// 朴素Dijstra算法
// 初始化
for(int i=1;i<=n;i++)
dist[i] = 0x3f3f3f3f;
dist[1] = 0;
for(int i=0;i<n;i++){
int t = -1; // t是n个点中距离1距离最短的那个点,当t不确定的时候,为-1
for(int j=1;j<=n;j++){
if(st[j]==false&&(t==-1||dist[j]<dist[t])) // 这句话细品,首先这个点没确定最短距离,其次,这个点为-1或者小于当前最短距离都可以进行更新
t = j;
}
st[t] = true;
// 更新与t点有关的其他点到1的最短距离
for(int j=1;j<=n;j++)
dist[j] = Math.min(dist[j],dist[t]+g[t][j]);
}
//
if(dist[n]==0x3f3f3f3f) System.out.println(-1);
else System.out.println(dist[n]);
}
}
初始化最大值不能用maxInteger.MAX_VALUE
因为dist数组初始化都是max,并且还有加操作,会有整数溢出的问题,原因是在dist[t] + g[t][j] 这一句,若dist取Integer.MAX_VALUE,则溢出后反而会变成一个负数,就变成一个更小的值了。这里可以取5000000,原因是题目中说每条边的长度不超过10000,且一共有500个点,最短路的长度再大也不可能超过二者的乘积。
堆优化版Dijkstra算法(优先队列+邻接表)
改进:
相较于朴素Dijkstra,堆优化版在第二个循环(即通过循环n次,找出当前的最短距离)做出了修改。利用最小堆的方式,可以直接取出堆顶(即dist的最小值)。
且无需在第一个循环循环n次(即循环n次,每次确定一个点的最短距离),只需要判定堆中还有没有值来判断是否全部已获取到最短距离。
时间复杂度:
O(mlogn)
算法实现:
优先队列(是最小堆实现的)(java是PriorityQueue,其他不清楚)
邻接表:
- 使用数组模拟邻接表,见2024/2/7打卡树与图的深度优先遍历DFS---数的重心**-CSDN博客)
S:已确定最短距离的点的集合
dist[]:最短距离存储
伪代码:
Queue q // 优先队列
q.add(<1,0>) // 添加第一个点 1 和对应的最短距离 0
while(q不空) {
取出队头,t <- 点 ,dist <- 距离
如果该点已确定最短距离,跳过继续,重新取队头
否则,将该点装入S
然后更新与 t 点有关的其他点的距离值,再将与t相关的点和对应的最短距离装入队列
}
例题(简单) 850. Dijkstra求最短路 II - AcWing题库
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n,m≤1.5×10^5
图中涉及边长均不小于 0,且不超过 10000。
数据保证:如果最短路存在,则最短路的长度不超过 10^9。输入样例:
3 3 1 2 2 2 3 1 1 3 4
输出样例:
3
import java.io.*;
import java.util.*;
class Main{
static int N = 150010;
static int n,m,idx;
static int[] h = new int[N];
static int[] e = new int[2*N];
static int[] ne = new int[2*N];
static int[] w = new int[2*N]; // 存储边的权重
static int[] dist = new int[N]; // 存储最短距离
static boolean[] st = new boolean[N]; // 判断是否确定最短距离
static Queue<PII> q = new PriorityQueue<>(); // 优先队列
public static void main(String[] args) throws IOException{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String[] s = in.readLine().split(" ");
n = Integer.parseInt(s[0]);
m = Integer.parseInt(s[1]);
for(int i=1;i<=n;i++){ // 初始化h[] 和 dist[]
h[i] = -1;
dist[i] = 0x3f3f3f3f;
}
while(m-->0){
s = in.readLine().split(" ");
int a = Integer.parseInt(s[0]);
int b = Integer.parseInt(s[1]);
int c = Integer.parseInt(s[2]);
if(a!=b) add(a,b,c); // 防止自环
}
q.offer(new PII(1,0)); //初始化队列
dist[1] = 0;
while(!q.isEmpty()){
PII p = q.poll();
int t = p.t; // 点
int d = p.d; // 最短距离
if(st[t]) continue; // 如果已确定最短距离,跳过
st[t] = true;
for(int i=h[t];i!=-1;i=ne[i]){ // 更新与t相关的其他点,并将其装入队列
int j = e[i];
int weight = w[i];
dist[j] = Math.min(dist[j],d+weight); // d+weight:t的最短距离加边的权重
q.offer(new PII(j,dist[j]));
}
}
if(dist[n]==0x3f3f3f3f) System.out.println(-1);
else System.out.println(dist[n]);
}
// 添加边
public static void add(int a,int b,int c){
w[idx] = c;
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
}
// 实现向队列中添加点和对应最短距离,实现Comparable,用dist来进行排序
class PII implements Comparable<PII>{
int t;
int d;
public PII(int t,int d){
this.t = t;
this.d = d;
}
public int compareTo(PII i){
return this.d-i.d;
}
}
Bellman-Ford算法
如果存在负权回路,可能没有最短距离。
时间复杂度:
O(nm)
算法思路:
结构体:存储边和权重 struct{ a, b, w} edge[m]
dist[] :存储每个店的最短距离
伪代码:
for(1~n) // 循环n次 (循环到第k次的意义:边从起点向外扩展到k层,更准确的来说,当前的最短路径是从1开始经过不超过k条边的点的最短路径)
备份copy当前的dist[ ] (不然可能出现串联,因为该次循环里,之前的修改过的路径会影响到后面的边,这样第一层循环就没有意义了,其实不用也可以,看题目要求)
for(循环所有边)
根据边的权重和备份的dist,更新点的最短距离
// 不可以用当前dist,否则第一个循环将没有意义
可以判断是否有负环:
根据第一个循环的特性(循环到第k次的意义:边从起点向外扩展到k层,点更新到距离起点k条边)。所以,当循环到第n次的时候,如果最短距离还有更新,说明到从1到该点的边的个数>= n,说明存在负环。具体解释如下:
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出
impossible
。注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
点的编号为 1∼n。
输出格式
输出一个整数,表示从 11 号点到 n 号点的最多经过 k 条边的最短距离。
如果不存在满足条件的路径,则输出
impossible
。数据范围
1≤n,k≤500,
1≤m≤10000,
1≤x,y≤n,
任意边长的绝对值不超过 10000。输入样例:
3 3 1 1 2 1 2 3 1 1 3 3
输出样例:
3
import java.io.*;
import java.util.*;
class Main{
static int N = 520;
static int n,m,k;
static int[] dist = new int[N];
static List<PII> l = new ArrayList<>(); // 存储边
public static void main(String[] args) throws IOException{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String[] s = in.readLine().split(" ");
n = Integer.parseInt(s[0]);
m = Integer.parseInt(s[1]);
k = Integer.parseInt(s[2]);
// 初始化dist
for(int i=1;i<=n;i++)
dist[i] = 0x3f3f3f3f;
dist[1] = 0;
for(int i=0;i<m;i++){
s = in.readLine().split(" ");
int a = Integer.parseInt(s[0]);
int b = Integer.parseInt(s[1]);
int c = Integer.parseInt(s[2]);
l.add(new PII(a,b,c));
}
// Bellman-Ford算法
for(int i=0;i<k;i++){ // 循环k次
int[] backup = Arrays.copyOf(dist,n+1); // 复制备份dist
for(PII j: l){ // 循环所有边
int a = j.a;
int b = j.b;
int w = j.w;
dist[b] = Math.min(dist[b],backup[a]+w); // 更新***
}
}
if(dist[n]>0x3f3f3f3f/2) System.out.println("impossible"); // 这里除以2,不是等于的原因是,存在负权边,ex:5 -> ne 权重为-2,5不可达,但是-2会影响n
else System.out.println(dist[n]);
}
}
// 存储边,代替结构体
class PII{
int a;
int b;
int w;
public PII(int a,int b,int w){
this.a = a;
this.b = b;
this.w = w;
}
}
SPFA算法(邻接表+队列,Bellman算法优化而来,实现与堆优化版Dijkstra算法很像)
- 算法要求图中不能出现负权回路
- 对Bellman算法用宽搜来做优化(每一次循环边更新最短距离,其中有一些是不会被更新的)
- 也可以处理正权图 ,但是spfa会被数据卡,导致时间复杂度增高
时间复杂度:
一般O(m) ,最坏O(nm)
算法思路:
数组模拟邻接表+队列
伪代码:
Queue q
q <- 1
while(q不空)
t <- 取出q的队头
更新 t 的所有出边 t -> b
如果更新成功且 b 点没有在队列中 ,将 b 装入 q 中
spfa判断是否存在负权回路,原理与Bellman算法相同(利用到某个点的最短距离的经过的边数>=n,说明存在负权回路)
例题 AcWing 851. spfa求最短路 - AcWing
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出
impossible
。数据保证不存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
数据范围
如果路径不存在,则输出
impossible
。1≤n,m≤10^5,
图中涉及边长绝对值均不超过 10000。输入样例:
3 3 1 2 5 2 3 -3 1 3 4
输出样例:
2
import java.io.*;
import java.util.*;
class Main{
static int N = 100010;
static int n,m,idx;
static int[] h = new int[N];
static int[] e = new int[2*N];
static int[] ne = new int[2*N];
static int[] w = new int[2*N];
static int[] dist = new int[N];
static boolean[] st = new boolean[N]; // 判断是否在队列中
static Queue<Integer> q = new LinkedList<>();
public static void main(String[] args) throws IOException{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String[] s = in.readLine().split(" ");
n = Integer.parseInt(s[0]);
m = Integer.parseInt(s[1]);
for(int i=1;i<=n;i++){
h[i] = -1;
dist[i] = 0x3f3f3f3f;
}
dist[1] = 0;
while(m-->0){
s = in.readLine().split(" ");
int a = Integer.parseInt(s[0]);
int b = Integer.parseInt(s[1]);
int w = Integer.parseInt(s[2]);
add(a,b,w);
}
//SPFA算法
q.add(1);
while(!q.isEmpty()){
int t = q.poll();
st[t] = false;
for(int i=h[t];i!=-1;i=ne[i]){ // 更新所有与t相关的边
int j = e[i];
int weight = w[i];
if(dist[j]>dist[t]+weight){
dist[j] = dist[t]+weight; // 更新边
if(!st[j]){ // 没有在队列中才加进去
q.offer(j);
st[j] = true;
}
}
}
}
if(dist[n] == 0x3f3f3f3f) System.out.println("impossible");
else System.out.println(dist[n]);
}
// 添加边
public static void add(int a,int b,int weight){
w[idx] = weight;
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
}
例题2 AcWing 852. spfa判断负环 - AcWing
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
如果图中存在负权回路,则输出
Yes
,否则输出No
。数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过 10000。输入样例:
3 3 1 2 -1 2 3 4 3 1 -4
输出样例:
Yes
import java.io.*;
import java.util.*;
class Main{
static int N = 100010;
static int n,m,idx;
static int[] h = new int[N];
static int[] e = new int[2*N];
static int[] ne = new int[2*N];
static int[] w = new int[2*N];
static int[] dist = new int[N];
static int[] cnt = new int[N];
static boolean[] st = new boolean[N];
static Queue<Integer> q = new LinkedList<>();
public static void main(String[] args) throws IOException{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String[] s = in.readLine().split(" ");
n = Integer.parseInt(s[0]);
m = Integer.parseInt(s[1]);
Arrays.fill(h,-1);
while(m-->0){
s = in.readLine().split(" ");
int a = Integer.parseInt(s[0]);
int b = Integer.parseInt(s[1]);
int w = Integer.parseInt(s[2]);
add(a,b,w);
}
if(spfa()==-1) System.out.println("Yes");
else System.out.println("No");
}
//SPFA算法
public static int spfa(){
//这里不用初始化dist的原因:不求最短距离,只判断是否存在负环,
//如果是负边,才进行更改距离和边数的操作,正边对于判断负环没有影响
//这里需要将所有的点都装入队列中,因为该图可能不是一个连通图,从一个点出发可能到不了负环
for(int i=1;i<=n;i++){
q.add(i);
st[i] = true;
}
while(!q.isEmpty()){
int t = q.poll();
st[t] = false;
for(int i=h[t];i!=-1;i=ne[i]){
int j = e[i];
//如果有负环,进入负环的第一个点就会弹出队列后不断加入队列,因为循环一次环以后,从t到该点的距离一定为负(负环)
//如果不存在负环,那么每个点就不会循环被装入队列,循环就可以结束
if(dist[j]>dist[t]+w[i]){//边长为负的时候满足
dist[j] = dist[t]+w[i];
cnt[j] = cnt[t]+1;
if(cnt[j]>=n) return -1;//如果到该点的边数大于n了,说明存在负环
if(!st[j])//如果该数不在队列里面,就将它装入队列,如果在队列里面,就不再装入(装入无意义,不进行重复一样的操作)
//不装入意味着先找到这个点的最短距离之后才进行这个点之后的操作
q.offer(j);
st[j] = true;
}
}
}
return 0;
}
// 添加边
public static void add(int a,int b,int weight){
w[idx] = weight;
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
}
Floyd算法(邻接矩阵+三重循环)
时间复杂度:
O(n^3)
算法思路:(原理基于动态规划)
原理:d[k,i,j] 只经过1~k之间的点的i到j的最短距离
循环之前 d[i][j] 存的是边
循环之后 d[i][j] 存的是从 i 到 j 的最短距离
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
d[i][j] = Math.min(d[i][j],d[i][k]+d[k][j]);
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出
impossible
。数据保证图中不存在负权回路。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。
输出格式
共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出
impossible
。数据范围
1≤n≤200,
1≤k≤n^2,
1≤m≤20000,
图中涉及边长绝对值均不超过 10000。输入样例:
3 3 2 1 2 1 2 3 2 1 3 1 2 1 1 3
输出样例:
impossible 1
import java.io.*;
import java.util.*;
class Main{
static int N = 210;
static int n,m,k;
static int[][] g = new int[N][N];
static int[] dist = new int[N];
static boolean[] st = new boolean[N];
public static void main(String[] args) throws IOException{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String[] s = in.readLine().split(" ");
n = Integer.parseInt(s[0]);
m = Integer.parseInt(s[1]);
k = Integer.parseInt(s[2]);
// 初始化g[][]
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
g[i][j] = 200000010;
if(i==j) g[i][j] = 0;
}
}
// 添加边
while(m-->0){
s = in.readLine().split(" ");
int a = Integer.parseInt(s[0]);
int b = Integer.parseInt(s[1]);
int w = Integer.parseInt(s[2]);
g[a][b] = Math.min(g[a][b],w);
}
// Floyd算法
for(int h=1;h<=n;h++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
g[i][j] = Math.min(g[i][j],g[i][h]+g[h][j]);
}
}
}
// 输出
while(k-->0){
s = in.readLine().split(" ");
int a = Integer.parseInt(s[0]);
int b = Integer.parseInt(s[1]);
if(g[a][b]>200000010/2) System.out.println("impossible");
else System.out.println(g[a][b]);
}
}
}