目录
最短路
在带权图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E) 中,每条边都有一个权值
w
i
w_i
wi ,即边的长度。路径的长度为路径上所有边权之和。
最短路问题是指:求图中某点到另一点的最短路径的长度。 例如下图:
从点
1
1
1到点
4
4
4的最短路为
5
5
5,路径是
1
→
2
→
3
→
4
1\rightarrow 2\rightarrow 3\rightarrow 4
1→2→3→4。 特殊地,对于无权图或者带权图每条边权值相同的图,最短路可以通过BFS得到。
但是对于一般的带权图,就不能通过BFS得到最短路了,因为会出现两条边加起来小于一 边的情况,比如上图中的
1
→
2
→
3
1\rightarrow 2\rightarrow 3
1→2→3长度小于
1
→
3
1\rightarrow 3
1→3,这样如果BFS先访问到3,把最短路记录为
1
→
3
1\rightarrow 3
1→3的长度5,后续也不再更改,那就不对了。
接下来我们\解决最短路问题的相关算法,通过接下来学到的算法,我们就可以解决带权图最短路问题了。
多源最短路–floyd
接下来介绍一种算法Floyd算法可以解决最短路问题,所谓多源则是它可以求出以每个点为起点到其它每个点的最短路。
不过其实有一种特殊情况是求不出最短路的,就是有负环的情况,此时可以不断在这个负环上转圈,所以负环上的点之间最短路为负无穷,这种情况下一般也只是判断出负环,不做其它处理,Floyd 算法无法判断这样的情况,所以就在没有负环的情况下使用,负环的判断在之后的算法中会给出方法。
Floyd算法是-种利用动态规划的思想、计算给定的带权图中任意两个顶点之间最短路径的算法。无权图可以直接把边权看
作1。
我们用
d
p
[
k
]
[
i
]
[
j
dp[k][i][j
dp[k][i][j]示
i
i
i到
j
j
j能经过1 ~ k的点的最短路。那么实际上
d
p
[
0
]
[
i
]
[
j
]
dp[0][i][j]
dp[0][i][j]就是原图,如果
i
,
j
i,j
i,j之间存在边,那么
i
,
j
i,j
i,j之间不经过任何点的最短路就是边长,则
i
,
j
i, j
i,j之间的最短路为无穷大。
那么对于
i
,
j
i,j
i,j之间经过1 ~ k的最短路
d
p
[
k
]
[
i
]
[
j
]
dp[k][i][j]
dp[k][i][j]可以通过经过1 ~ k - 1的最短路转移过来。
●如果不经过第k个点,那么就是
d
p
[
l
k
−
1
]
[
i
]
[
j
]
dp[lk - 1][i][j]
dp[lk−1][i][j]。
●如果经过第k个点,那么就是
d
p
[
k
−
1
]
[
i
]
[
k
]
+
d
p
[
k
−
1
]
[
k
]
[
j
]
dp[k - 1][i][k] + dp[k - 1][k][j]
dp[k−1][i][k]+dp[k−1][k][j]。
所以就有转移
d
p
[
k
]
[
i
]
[
j
]
=
m
i
n
(
d
p
[
k
−
1
]
[
i
]
[
j
]
,
d
p
[
k
−
1
]
[
i
]
[
k
]
+
d
p
[
k
−
1
]
[
k
]
[
j
]
)
\displaystyle dp[k][i][j] = min(dp[k - 1][i][j], dp[k - 1][i][k] + dp[k - 1][k][j])
dp[k][i][j]=min(dp[k−1][i][j],dp[k−1][i][k]+dp[k−1][k][j])
我们仔细分析,
d
p
[
k
]
dp[k]
dp[k] 只能由
d
p
[
k
−
1
]
dp[k - 1]
dp[k−1]转移过来。并且
d
p
[
k
−
1
]
[
i
]
[
k
]
=
d
p
l
[
k
]
[
i
]
[
k
]
dp[k - 1][i][k] = dpl[k][i][k]
dp[k−1][i][k]=dpl[k][i][k],因为
i
i
i到
k
k
k的最短路中间肯定不会
经过
k
k
k。同理,
d
p
[
k
−
1
]
[
k
]
[
j
]
=
d
p
[
k
]
[
k
]
[
i
]
dp[k - 1][k][j] = dp[k][k][i]
dp[k−1][k][j]=dp[k][k][i]。
那么转移实际上变成了
d
p
[
k
]
[
i
]
[
j
]
=
m
i
n
(
d
p
[
k
−
1
]
[
i
]
[
j
]
,
d
p
[
k
]
[
i
]
[
k
]
+
d
p
[
k
]
[
k
]
[
j
]
)
dp[k][i][j]=min(dp[k−1][i][j],dp[k][i][k]+dp[k][k][j])
dp[k][i][j]=min(dp[k−1][i][j],dp[k][i][k]+dp[k][k][j])
这时候,我们尝试把
k
k
k这一维去掉,就用
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 来表示
i
,
j
i, j
i,j之间的最短路,那么转移变成了
∀
1
≤
k
≤
n
∀1 \leq k \leq n
∀1≤k≤n
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
]
[
j
]
,
d
p
[
i
]
[
k
]
+
d
p
[
k
]
[
j
]
)
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])
我们写出最终的 Floyd 的形式,这也是常用的写法,优化了一维的空间。并且写法更加简单。如果理解了动态规划的思想,你就一定明白了为什么枚举的中间点 k k k一定要写在最外面。没有理解这一点,很容易把 3 3 3个循环的顺序弄错了。
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
using namespace std;
const int MAXN=505;
int floyd[MAXN][MAXN];
int s,t;
int main(){
memset(floyd,0x3f,sizeof(floyd));
int n,m;
scanf("%d",&n);
for(int i=1;i<=n;i++){
floyd[i][i]=0;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
scanf("%d",&floyd[i][j]);
}
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(floyd[i][j]>floyd[i][k]+floyd[k][j]){
floyd[i][j]=floyd[i][k]+floyd[k][j];
}
}
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
printf("%d ",floyd[i][j]);
}
printf("\n");
}
return 0;
}
这里在使用的时候因为邻接矩阵存的是距离,所以需要把不连通的部分赋值为一个很大的整数,避免其对答案产生影响,这样也可以根据这个无穷大判断两点间是否可达。
如果需要记录路径,可以记录一下每两个点之间最后是被哪个点更新的,然后一条路就可以被拆成两半不断递归找到路径了。
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int MAXN=205;
int G[MAXN][MAXN];
int dis[MAXN];
bool vis[MAXN];
int pre[MAXN];
void print(int x){
if(pre[x]==0){
printf("%d",x);
return;
}
print(pre[x]);
printf(" %d",x);
}
int main(){
int n,m,s,t;
scanf("%d %d",&n,&m);
int a,b;
memset(G,0x3f,sizeof(G));
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=m;i++){
scanf("%d %d",&a,&b);
G[a][b]=1;
}
scanf("%d %d",&s,&t);
dis[s]=0;
for(int i=1;i<=n;i++){
int minn=0x3f3f3f3f;
int k=0;
for(int j=1;j<=n;j++){
if(vis[j]==0 && dis[j]<minn){
minn=dis[j];
k=j;
}
}
if(k==0) continue;
vis[k]=1;
for(int j=1;j<=n;j++){
if(dis[j]>dis[k]+G[k][j]){
dis[j]=dis[k]+G[k][j];
pre[j]=k;
}
}
}
if(s==t){
printf("0\n%d",s);
return 0;
}
if(dis[t]>m){
printf("no solution");
return 0;
}
printf("%d\n",dis[t]);
print(t);
return 0;
}
单源最短路–dijkstra
算法概念
解决单源最短路径问题常用 Dijkstra 算法,用于计算一个顶点到其他所有顶点的最短路径。Dijkstra 算法的主要特点是以起点为中心,逐层向外扩展(这一点类似于 bfs,但是不同的是,bfs 每次扩展一个层,但是 Dijkstra 每次只会扩展一个点),每次都会取一个最近点继续扩展,直到取完所有点为止。
注意:Dijkstra 算法要求图中不能出现负权边。
算法流程
我们定义带权图
G
G
G 所有顶点的集合为
V
V
V,接着我们再定义已确定从源点出发的最短路径的顶点集合为
U
U
U,初始集合
U
U
U 为空,记从源点
s
s
s 出发到每个顶点
v
v
v 的距离为
d
v
d_v
dv ,初始
d
s
=
0
d_s=0
ds=0接着执行以下操作:
1.从
V
−
U
V-U
V−U中找出一个距离源点最近的顶点
v
v
v,将
v
v
v加入集合
U
U
U。
2.并用
d
v
d_v
dv 和顶点
v
v
v连出的边来更新和
v
v
v相邻的、不在集合
U
U
U中的顶点的
d
d
d,这一步称为松弛操作。
3.重复步骤 1 和 2,直到
V
=
U
V=U
V=U 或找不出一个从
s
s
s出发有路径到达的顶点,算法结束。
如果最后
V
≠
U
V \neq U
V=U,说明有顶点无法从源点到达;否则每个
d
i
d_i
di 表示从
s
s
s出发到顶点
i
i
i的最短距离。
Dijkstra 算法的时间复杂度为
O
(
V
2
)
\mathcal {O}(V^2)
O(V2),其中
V
V
V表示顶点的数量。
算法演示
初始每个顶点的
d
d
d设置为无穷大
inf
\inf
inf,源点
M
M
M的
d
M
d_M
dM设置为
0
0
0。当前
U
=
∅
U=\emptyset
U=∅,
V
−
U
V-U
V−U中
d
d
d最小的顶点是
M
M
M。从顶点
M
M
M 出发,更新相邻点的
d
d
d。
更新完毕,此时
U
=
{
M
}
U=\{M\}
U={M},
V
−
U
V-U
V−U中
d
d
d 最小的顶点是
W
W
W。从
W
W
W 出发,更新相邻点的
d
d
d。
更新完毕,此时
U
=
{
M
,
W
}
U=\{M,W\}
U={M,W},
V
−
U
V-U
V−U 中
d
d
d 最小的顶点是
E
E
E。从
E
E
E出发,更新相邻顶点的
d
d
d。
更新完毕,此时
U
=
{
M
,
W
,
E
}
U=\{M, W, E\}
U={M,W,E},
V
−
U
V-U
V−U 中
d
d
d 最小的顶点是
X
X
X。从
X
X
X出发,更新相邻顶点的
d
d
d。
更新完毕,此时
U
=
{
M
,
W
,
E
,
X
}
U=\{M,W,E,X\}
U={M,W,E,X},
V
−
U
V-U
V−U 中
d
d
d 最小的顶点是
D
D
D。从
D
D
D 出发,没有其他不在集合
U
U
U 中的顶点。
此时
U
=
V
U=V
U=V,算法结束,单源最短路计算完毕。
代码实现
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int MAXN=2505;
int G[MAXN][MAXN];
int dis[MAXN];
bool vis[MAXN];
int main(){
int n,m,s,t;
scanf("%d %d %d %d",&n,&m,&s,&t);
int a,b,c;
memset(G,0x3f,sizeof(G));
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=m;i++){
scanf("%d %d %d",&a,&b,&c);
G[a][b]=G[b][a]=c;
}
vis[s]=0;
dis[s]=0;
for(int i=1;i<=n;i++){
int minn=0x3f3f3f3f;
int k=0;
for(int j=1;j<=n;j++){
if(vis[j]==0 && dis[j]<minn){
minn=dis[j];
k=j;
}
}
if(!k) continue;
vis[k]=1;
for(int j=1;j<=n;j++){
if(dis[j]>dis[k]+G[k][j]){
dis[j]=dis[k]+G[k][j];
}
}
}
printf("%d",dis[t]);
return 0;
}
时间复杂度 O ( V 2 ) \mathcal {O}(V^2) O(V2),考虑优化
优化
首先是松弛与 k k k点相邻的点操作:
for(int j=1;j<=n;j++){
if(dis[j]>dis[k]+G[k][j]){
dis[j]=dis[k]+G[k][j];
}
}
很容易想到用邻接表进行优化,只枚举相邻的即可
还可以更快?
我们把眼光转向寻找距离原点最近的点的那一波操作,找最值,想到什么?没错,优先队列(堆)
最终优化:
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
using namespace std;
const int MAXN = 2505;
bool vis[MAXN];
int n, m, dis[MAXN];
struct edge{
int v,w;
edge(){};
edge(int V, int W) {
v=V;
w=W;
}
};
struct node{
int u,dis_;
node(){}
node(int U,int D) {
u=U;
dis_=D;
}
bool operator<(const node a)const{return dis_>a.dis_;}
};
priority_queue<node> q;
vector<edge> cost[MAXN];
void add(int su,int sv,int scost) {
cost[su].push_back(edge(sv,scost));
cost[sv].push_back(edge(su,scost));
}
void Dijkstra(int s){
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
q.push(node(s,0));
while(!q.empty()) {
int t=q.top().u;
q.pop();
if(vis[t]){
continue;
}
vis[t]=true;
for(int i=0;i<cost[t].size();i++){
int v=cost[t][i].v;
if(dis[v]>dis[t]+cost[t][i].w){
dis[v]=dis[t]+cost[t][i].w;
q.push(node(v,dis[v]));
}
}
}
}
int main() {
int n,m;
int s,t;
scanf("%d %d %d %d", &n, &m,&s,&t);
for(int i=1;i<=m;i++) {
int t1,t2,t3;
scanf ("%d %d %d",&t1,&t2,&t3);
add(t1,t2,t3);
}
Dijkstra(s);
printf("%d",dis[t]);
return 0;
}
时间复杂度 O ( M l o g M ) , M \mathcal {O}(MlogM),M O(MlogM),M为边数
单源最短路–Bellman-Ford
曾今被我念成了 B e l l m a n − F l o y d Bellman-Floyd Bellman−Floyd [手动狗头]
算法概念
Bellman-Ford算法:对每条边执行更新,迭代N-1次。
具体操作是对图进行最多n-1次松弛操作,每次操作对所有的边进行松弛,为什么是n-1次操作呢?这是因为我们输入的边不一定是按源点由近至远,万一是由远至近最坏情况就得n-1次,我们可以以一个单链
A
→
B
→
C
→
D
A\rightarrow B\rightarrow C\rightarrow D
A→B→C→D来举例
初始化为全部距离原点为INF
假如现在输入边是
C
→
D
,
3
C \rightarrow D,3
C→D,3
然鹅没有任何一条边得到松弛
又输入
B
→
C
,
2
B \rightarrow C,2
B→C,2
然鹅依然没有任何一条边得到松弛
现在输入
A
→
B
,
1
A \rightarrow B,1
A→B,1
哎,好像可以了,只需要从A到B一路松弛下去,就OK了
好消息:Bellman-Ford可以应用于负权图
算法演示
正权图
初始化
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
、
3
、
4
、
5
、
6
]
=
∞
Dis[2、3、4、5、6] = ∞
Dis[2、3、4、5、6]=∞
对第1条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
1
Dis[2] = 1
Dis[2]=1
D
i
s
[
3
、
4
、
5
、
6
]
=
∞
Dis[3、4、5、6] = ∞
Dis[3、4、5、6]=∞
对第2条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
1
Dis[2] = 1
Dis[2]=1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
、
5
、
6
]
=
∞
Dis[4、5、6] = ∞
Dis[4、5、6]=∞
对第3条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
1
Dis[2] = 1
Dis[2]=1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
、
5
、
6
]
=
∞
Dis[4、5、6] = ∞
Dis[4、5、6]=∞
对第4条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
1
Dis[2] = 1
Dis[2]=1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
]
=
5
Dis[4] = 5
Dis[4]=5
D
i
s
[
5
、
6
]
=
∞
Dis[5、6] = ∞
Dis[5、6]=∞
对第5条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
1
Dis[2] = 1
Dis[2]=1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
]
=
5
Dis[4] = 5
Dis[4]=5
D
i
s
[
5
]
=
10
Dis[5] = 10
Dis[5]=10
D
i
s
[
6
]
=
∞
Dis[6] = ∞
Dis[6]=∞
对第6条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
1
Dis[2] = 1
Dis[2]=1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
]
=
5
Dis[4] = 5
Dis[4]=5
D
i
s
[
5
]
=
8
Dis[5] = 8
Dis[5]=8
D
i
s
[
6
]
=
∞
Dis[6] = ∞
Dis[6]=∞
对第7条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
1
Dis[2] = 1
Dis[2]=1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
]
=
5
Dis[4] = 5
Dis[4]=5
D
i
s
[
5
]
=
8
Dis[5] = 8
Dis[5]=8
D
i
s
[
6
]
=
11
Dis[6] = 11
Dis[6]=11
对第8条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
1
Dis[2] = 1
Dis[2]=1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
]
=
5
Dis[4] = 5
Dis[4]=5
D
i
s
[
5
]
=
8
Dis[5] = 8
Dis[5]=8
D
i
s
[
6
]
=
11
Dis[6] = 11
Dis[6]=11
到此,这个图的单源最短路已全部求出
现在让我们看看负权图吧:
负权图
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
、
3
、
4
、
5
、
6
]
=
∞
Dis[2、3、4、5、6] = ∞
Dis[2、3、4、5、6]=∞
对第1条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
1
Dis[2] = 1
Dis[2]=1
D
i
s
[
3
、
4
、
5
、
6
]
=
∞
Dis[3、4、5、6] = ∞
Dis[3、4、5、6]=∞
对第2条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
1
Dis[2] = 1
Dis[2]=1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
、
5
、
6
]
=
∞
Dis[4、5、6] = ∞
Dis[4、5、6]=∞
对第3条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
−
5
Dis[2] = -5
Dis[2]=−5
D
i
s
[
3
]
=
−
2
Dis[3] = -2
Dis[3]=−2
D
i
s
[
4
、
5
、
6
]
=
∞
Dis[4、5、6] = ∞
Dis[4、5、6]=∞
唉?dis[2]貌似有什么不对,-5怎么走出来的呀?不对,-5好像还真的走出来,
1
→
2
→
3
→
2
1\rightarrow2\rightarrow3\rightarrow2
1→2→3→2就行了,发现了什么,
2
→
3
2\rightarrow3
2→3是一个“负环”,其实,在无向图中,凡是有负权边就相当于有负环了,因为可以在这条边上左右横跳,就可以让路程无限短。怎么办呢?那就不管它了呗,先换成有向图走完再说
对第3条边进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
−
1
Dis[2] = -1
Dis[2]=−1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
、
5
、
6
]
=
∞
Dis[4、5、6] = ∞
Dis[4、5、6]=∞
第4条边进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
−
1
Dis[2] = -1
Dis[2]=−1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
]
=
3
Dis[4] = 3
Dis[4]=3
D
i
s
[
5
、
6
]
=
∞
Dis[5、6] = ∞
Dis[5、6]=∞
对第5条边进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
−
1
Dis[2] = -1
Dis[2]=−1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
]
=
3
Dis[4] = 3
Dis[4]=3
D
i
s
[
5
]
=
8
Dis[5] = 8
Dis[5]=8
D
i
s
[
6
]
=
∞
Dis[6] = ∞
Dis[6]=∞
对第6条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
−
1
Dis[2] = -1
Dis[2]=−1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
]
=
3
Dis[4] = 3
Dis[4]=3
D
i
s
[
5
]
=
8
Dis[5] = 8
Dis[5]=8
D
i
s
[
6
]
=
∞
Dis[6] = ∞
Dis[6]=∞
对第7条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
−
1
Dis[2] = -1
Dis[2]=−1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
]
=
3
Dis[4] = 3
Dis[4]=3
D
i
s
[
5
]
=
8
Dis[5] = 8
Dis[5]=8
D
i
s
[
6
]
=
10
Dis[6] = 10
Dis[6]=10
对第8条边双向进行松弛之后
D
i
s
[
1
]
=
0
Dis[1] = 0
Dis[1]=0
D
i
s
[
2
]
=
−
1
Dis[2] = -1
Dis[2]=−1
D
i
s
[
3
]
=
2
Dis[3] = 2
Dis[3]=2
D
i
s
[
4
]
=
3
Dis[4] = 3
Dis[4]=3
D
i
s
[
5
]
=
8
Dis[5] = 8
Dis[5]=8
D
i
s
[
6
]
=
10
Dis[6] = 10
Dis[6]=10
OK,求完了,等等,刚才不是还留下了一个负环的问题吗?
我们想想负环是不是可以无限松弛,那么我们就可以在算法跑完后再跑一遍,看看有没有可以继续松弛的,如果有,那么这个图一定有负环
代码
附赠路径输出
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int MAXN=505;
struct edge{
int u,v;
int val;
}G[MAXN];
int n,m;
int dis[MAXN];
int pre[MAXN];
int w[MAXN];
int s,t;
void print(int x){
if(pre[x]==0){
printf("%d",x);
return;
}
print(pre[x]);
printf(" %d",x);
}
bool BellmanFord(){
dis[s]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(dis[G[j].v]>dis[G[j].u]+G[j].val){
dis[G[j].v]=dis[G[j].u]+G[j].val;
pre[G[j].v]=G[j].u;
}
else if(dis[G[j].v]==dis[G[j].u]+G[j].val){
dis[G[j].v]=dis[G[j].u]+G[j].val;
pre[G[j].v]=min(G[j].u,pre[G[j].v]);
}
}
}
bool flag=true;
for(int i=1;i<=m;i++){//判负环
if(dis[G[i].v]>dis[G[i].u]+G[i].val){
return 0;
}
}
return flag;
}
int main(){
memset(dis,0x3f,sizeof(dis));
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d %d %d",&G[i].u,&G[i].v,&G[i].val);
pre[G[i].v]=G[i].u;
}
scanf("%d %d",&s,&t);
if(!BellmanFord()){
printf("No Solution");
}
else{
printf("%d\n",dis[t]);
print(t);
}
return 0;
}
单源最短路–SPFA
它死了
NOI2018 Day 1,T1 出题人卡了 SPFA 并在讲课时说其死了。
算法概念
在Bellmanford算法中,有许多松弛是无效的。这给了我们很大的改进的空间。SPFA算法正是对Bellmanford算法的改进。它是由西南交通大学段丁凡1994提出的。它采用了队列和松弛技术。先将源点加入队列。然后从队列中取出一个点(此时该点为源点),对该点的邻接点进行松弛,如果该邻接点松弛成功且不在队列中,则把该点加入队列。如此循环往复,直到队列为空,则求出了最短路径。
判断有无负环:如果某个点进入队列的次数超过N次则存在负环 ( 存在负环则无最短路径,如果有负环则会无限松弛,而一个带n个点的图至多松弛n-1次)
算法流程
在 SPFA 算法中,使用 d i d_i di表示从源点到顶点 i i i的最短路,额外用一个队列来保存即将进行拓展的顶点列表,并用 i n _ q u e u e i in\_queue_i in_queuei 来标识顶点 i i i是不是在队列中。
- 初始队列中仅包含源点,且源点 s s s的 d s = 0 d_s=0 ds=0
- 取出队列头顶点
u
u
u,扫描从顶点
u
u
u出发的每条边,设每条边的另一端为
v
v
v,边
<
u
,
v
>
<u,v>
<u,v> 权值为
w
w
w,若
d
u
+
w
<
d
v
d_u+w<d_v
du+w<dv,则
- 将 d v d_v dv修改为 d u + w d_u+w du+w
- 若 v v v不在队列中,则将 v v v入队
重复步骤 2 2 2直到队列为空
最终 d d d 数组就是从源点出发到每个顶点的最短路距离。如果一个顶点从没有入队,则说明没有从源点到该顶点的路径。
如果进队次数超过 n n n次,那么说明图中存在负环。
算法思想
在一定程度上,也可以认为 SPFA 是由 BFS 的思想转化而来。从不含边权或者说边权为
1
1
1个单位长度的图上的 BFS,推广到带权图上,就得到了 SPFA。只是 BFS 能保证第一次访问就一定是最短路,而 SPFA 在每次更新了最短路以后又重新入队从而去更新后续结点的最短路。比如下图,
2
2
2搜到
3
3
3的时候会再次更新
3
3
3最短路。
代码:
#include<cstdio>
#include<iostream>
#include<queue>
#include<vector>
#include<cstring>
using namespace std;
const int MAXN=1000005;
struct node{
int v;
int w;
node(){}
node(int V,int W){
v=V,w=W;
}
};
int n,m,s,t;
vector<node> G[MAXN];
queue<int> que;
bool vis[MAXN];
int dis[MAXN];
void add(int u,int v,int w){
G[u].push_back(node(v,w));
}
void spfa(){
dis[s]=0;
vis[s]=1;
que.push(s);
while(!que.empty()){
int u=que.front();
que.pop();
vis[u]=0;
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v,w=G[u][i].w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
if(!vis[v]){
que.push(v);
vis[v]=1;
}
}
}
}
}
int main(){
scanf("%d %d",&n,&m);
int a,b,c;
s=1,t=n;
for(int i=1;i<=m;i++){
scanf("%d %d %d",&a,&b,&c);
add(a,b,c);
add(b,a,c);
}
for(int i=1;i<=n;i++){
dis[i]=0x3f3f3f3f;
}
spfa();
printf("%d",dis[t]);
return 0;
}