题目 spfa求最短路
给定一个 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
spfa 算法思路
明确一下松弛的概念。
-
考虑节点u以及它的邻居v,从起点跑到v有好多跑法,有的跑法经过u,有的不经过。
-
经过u的跑法的距离就是distu+u到v的距离。
-
所谓松弛操作,就是看一看distv和distu+u到v的距离哪个大一点。
-
如果前者大一点,就说明当前的不是最短路,就要赋值为后者,这就叫做松弛。
spfa算法文字说明:
-
建立一个队列,初始时队列里只有起始点。
-
再建立一个数组记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。
-
再建立一个数组,标记点是否在队列中。
-
队头不断出队,计算始点起点经过队头到其他点的距离是否变短,如果变短且被点不在队列中,则把该点加入到队尾。
-
重复执行直到队列为空。
-
在保存最短路径的数组中,就得到了最短路径。
spfa 图解:
- 给定一个有向图,如下,求A~E的最短路。
-
源点A首先入队,然后A出队,计算出到BC的距离会变短,更新距离数组,BC没在队列中,BC入队
-
B出队,计算出到D的距离变短,更新距离数组,D没在队列中,D入队。然后C出队,无点可更新。
-
D出队,计算出到E的距离变短,更新距离数组,E没在队列中,E入队。
-
E出队,此时队列为空,源点到所有点的最短路已被找到,A->E的最短路即为8
代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100010;
int h[N], e[N], w[N], ne[N], idx;//邻接表,存储图
int st[N];//标记顶点是不是在队列中
int dist[N];//保存最短路径的值
int q[N], hh, tt = -1;//队列
void add(int a, int b, int c){//图中添加边和边的端点
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void spfa(){
q[++tt] = 1;//从1号顶点开始松弛,1号顶点入队
dist[1] = 0;//1号到1号的距离为 0
st[1] = 1;//1号顶点在队列中
while(tt >= hh){//不断进行松弛
int a = q[hh++];//取对头记作a,进行松弛
st[a] = 0;//取完队头后,a不在队列中了
for(int i = h[a]; i != -1; i = ne[i])//遍历所有和a相连的点
{
int b = e[i], c = w[i];//获得和a相连的点和边
if(dist[b] > dist[a] + c){//如果可以距离变得更短,则更新距离
dist[b] = dist[a] + c;//更新距离
if(!st[b]){//如果没在队列中
q[++tt] = b;//入队
st[b] = 1;//打标记
}
}
}
}
}
int main(){
memset(h, -1, sizeof h);//初始化邻接表
memset(dist, 0x3f, sizeof dist);//初始化距离
int n, m;//保存点的数量和边的数量
cin >> n >> m;
for(int i = 0; i < m; i++){//读入每条边和边的端点
int a, b, w;
cin >> a >> b >> w;
add(a, b, w);//加入到邻接表
}
spfa();
if(dist[n] == 0x3f3f3f3f )//如果到n点的距离是无穷,则不能到达
cout << "impossible";
else cout << dist[n];//否则能到达,输出距离
return 0;
}
题目 spfa判断负环
给定一个 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
算法分析
使用spfa算法解决是否存在负环问题
求负环的常用方法,基于SPFA,一般都用方法 2(该题也是用方法 2):
- 方法 1:统计每个点入队的次数,如果某个点入队n次,则说明存在负环
- 方法 2:统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于n,则也说明存在环
y总的原话
每次做一遍spfa()
一定是正确的,但时间复杂度较高,可能会超时。初始时将所有点插入队列中可以按如下方式理解:
在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa
,将虚拟源点加入队列中。然后进行spfa
的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于视频中的做法了。那么视频中的做法可以找到负环,等价于这次spfa
可以找到负环,等价于新图有负环,等价于原图有负环。得证。
-
dist[x]
记录虚拟源点到x的最短距离 -
cnt[x]
记录当前x点到虚拟源点最短路的边数,初始每个点到虚拟源点的距离为0,只要他能再走n步,即cnt[x] >= n
,则表示该图中一定存在负环,由于从虚拟源点到x至少经过n条边时,则说明图中至少有n + 1个点,表示一定有点是重复使用 -
若
dist[j] > dist[t] + w[i]
,则表示从t点走到j点能够让权值变少,因此进行对该点j进行更新,并且对应cnt[j] = cnt[t] + 1
,往前走一步
注意:该题是判断是否存在负环,并非判断是否存在从1开始的负环,因此需要将所有的点都加入队列中,更新周围的点
时间复杂度 一般:O(m) 最坏:O(nm)
Java 代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
public class Main {
static int n;
static int m;
static int N = 2010;
static int M = 10010;
static int[] h = new int[N];
static int[] e = new int[M];
static int[] ne = new int[M];
static int[] w = new int[M];
static int idx = 0;
static int[] dist = new int[N];//记录虚拟点到x的最短距离
static int[] cnt = new int[N];//从虚拟点到x经过的边数
static boolean[] st = new boolean[N];
public static void add(int a,int b,int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
public static boolean spfa()
{
Queue<Integer> queue = new LinkedList<Integer>();
//将所有点进入队列
for(int i = 1;i <= n;i++)
{
queue.add(i);
st[i] = true;
}
while(!queue.isEmpty())
{
int t = queue.poll();
st[t] = false;
for(int i = h[t]; i != -1;i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n) return true;
if(!st[j])
{
queue.add(j);
st[j] = true;
}
}
}
}
return false;
}
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String[] str1 = reader.readLine().split(" ");
n = Integer.parseInt(str1[0]);
m = Integer.parseInt(str1[1]);
Arrays.fill(h, -1);
while(m -- > 0)
{
String[] str2 = reader.readLine().split(" ");
int a = Integer.parseInt(str2[0]);
int b = Integer.parseInt(str2[1]);
int c = Integer.parseInt(str2[2]);
add(a,b,c);
}
if(spfa()) System.out.println("Yes");
else System.out.println("No");
}
}