最短路梳理
dijkstra
模板题练手
输入 5 5
1 2 20
2 3 30
3 4 20
4 5 20
1 5 100
输出
90【数据规模和约定】 1≤n≤2000 1≤m≤10000 0≤c≤10000
dijkstra+链式前向星 (附注释)
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<iomanip>
#include<stack>
#include<map>
#define TIE ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define N 2010
#define M 10010
#define INF 0x3f3f3f3f
using namespace std;
int n,m,s,vis[N],dis[N];
int head[N],to[M*2],nxt[M*2],val[M*2],idx;
void add(int u,int v,int w){
to[idx]=v,val[idx]=w,nxt[idx]=head[u],head[u]=idx++;
}
int dijkstra(){
memset(dis,0x3f,sizeof dis);//一定不要忘记!否则一直爆0
dis[1]=0;//起点到自己距离为0
for(int i=1;i<n;++i){
int t=-1;
for(int j=1;j<=n;++j) if(!vis[j]&&(t==-1||dis[t]>dis[j])) t=j;//t先被赋值为1(下标),之后在去寻找离自己最近的
vis[t]=1;
for(int j=head[t];~j;j=nxt[j]){
int y=to[j];
dis[y]=min(dis[y],dis[t]+val[j]);
}
}
return dis[n];
}
int main(){
memset(head,-1,sizeof head);
TIE;cin>>n>>m;
for(int i=1;i<=m;++i){
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
int ans=dijkstra();
if(ans==INF) cout<<"-1"<<"\n";
else cout<<ans<<"\n";
return 0;
}
上点难度,嘿嘿~~~
输入
4 8 2
1 2 4
1 3 2
1 4 7
2 1 1
2 3 5
3 1 2
3 4 4
4 2 3
输出
10
冷静分析:作为一个优秀的蒟蒻,我第一次想跑一遍dijkstra然后把答案*2不就行了 可是这是有向图。。。所以我建了个反图(就是把所有有向边起点和终点调换一下) 那对于每个点,它的距离是back(i)+go(i)
在原图上跑一遍起点为k的最短路dis相当于back(i) 在反图上跑一遍起点为k的最短路dis2相当于go(i)
AC code~~~ |
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<iomanip>
#include<stack>
#include<map>
#define TIE ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define N 1010
#define INF 0x3f3f3f3f
using namespace std;
int n,m,k,g[N][N],dis[N],vis[N],g1[N][N],dis2[N];
void dijkstra(int g[][N],int x,int dis[]){//传参传数组类似引用即可以改变数组在主函数的值,
memset(vis,0,sizeof vis);//但除一维可以省略外,其他都得填上范围
dis[x]=0;
for(int i=1;i<n;++i){
int t=-1;
for(int j=1;j<=n;++j) if(!vis[j]&&(t==-1||dis[t]>dis[j])) t=j;
vis[t]=1;
for(int j=1;j<=n;++j){
if(g[t][j]){
dis[j]=min(dis[j],dis[t]+g[t][j]);
}
}
}
}
int main(){
memset(g,0x3f,sizeof g);
memset(g1,0x3f,sizeof g1);
memset(dis,0x3f,sizeof dis);
memset(dis2,0x3f,sizeof dis2);
TIE;cin>>n>>m>>k;
for(int i=1;i<=m;++i){
int u,v,w;
cin>>u>>v>>w;
g[u][v]=min(g[u][v],w);
g1[v][u]=min(g1[v][u],w);
}
dijkstra(g,k,dis);
dijkstra(g1,k,dis2);
int ans=0;
for(int i=1;i<=n;++i){
ans=max(ans,dis[i]+dis2[i]);
}
cout<<ans<<"\n";
return 0;
}
//在原图上跑一遍起点为k的最短路dis相当于back
//在反图上跑一遍起点为k的最短路dis2相当于go
Bellman_ford
看这样一张图,用dijkstra模拟一下。因为每一轮都会选择距离1最近的点来更新距离,然后dis[2]就光荣的变成了2并被打上了标记,实际上到dis[2]的距离是-4,这就是为什么dijkstra解决不了负权图的原因
!那这根难啃的kun骨头就由bellman_ford来啃!
算法逻辑:
-
初始化源点s到各个点v的路径dis[v] = ∞,dis[s] = 0。
-
进行n - 1次遍历,每次遍历对所有边进行松弛操作,满足则将权值更新。
松弛操作(这个东西很有名):以u为起点,v为终点,uv边长度为w为例。dis[u]代表源点s到u点的路径长度,dis[v]代表源点s到v点的路径长度。如果满足下面的式子则将dis[v]更新为dis[u] + w。
dis[v] > dis[u] + w -
遍历都结束后,若再进行一次遍历,还能得到s到某些节点更短的路径的话,则说明存在负环路
还有一种方法判断,我们进行松弛操作的时候这个点从INF一定会**不变或变小**,但最多最多也不会超过INF/2,所以一个点的值超过就存在负环
模板题
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<iomanip>
#include<stack>
#include<map>
#include<queue>
#define TIE ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define N 510
#define M 10010
#define INF 0x3f3f3f
#define ll long long
using namespace std;
struct node{
int x,y,z;
}a[M];
int n,m,k,dis[N],cpy[N];
int bellman_ford(){
memset(dis,0x3f,sizeof dis);
dis[1]=0;
for(int i=1;i<=k;++i){
memcpy(cpy,dis,sizeof dis);
for(int j=1;j<=m;++j){
int x=a[j].x,y=a[j].y,z=a[j].z;
dis[y]=min(dis[y],cpy[x]+z);
}
}
if(dis[n]>INF/2) return -1;
else return dis[n];
}
int main(){
TIE;cin>>n>>m>>k;
for(int i=1;i<=m;++i) cin>>a[i].x>>a[i].y>>a[i].z;
int ans=bellman_ford();
if(ans==-1) puts("impossible");
else cout<<ans<<"\n";
return 0;
}
tips:cpy数组备份了一下dis数组的原因是防止发生“串联”(控制迭代次数)
SPFA
对任意一条边 u->v,只有 d[u]值改变时,v 的 d[v]值才有可能被改变,所以用一个queue把值改变的点存起来,就万事大吉了😆
void spfa(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
queue<int>q;
q.push(1);
st[1]=true;
while(!q.empty()){
int t=q.front();
q.pop();
st[t]=false;
for(int i=head[t];i!=-1;i=ne[i]){
int j=e[i];
if(dist[j]>dist[t]+w[i]){
dist[j]=dist[t]+w[i];
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
}
我们知道如果一个图中不存在负环回路,那么求得的最短路一定是不包含环的简单路径,所
以最短路径边数最多为 n-1 条。我们在 SPFA 的过程中可依据此规律判断图中是否存在负环:
- 定义 cnt[y] 记录当前从 root 到达 y 节点的最短路的边数
- 初始每个点的 cnt 为 0
- 当某条(x,y,z)边可以更新 d[y]的值时,此时的 cnt[y]被更新为 cnt[x]+1,即到达 y 的最短路径条数+1,如果此时的 cnt[y] >= n,则说明该图中一定存在负环:
(由抽屉原理,在 n 个点的条件下,从 1 到 y 至少经过 n 条边时,则说明该路径中至少有 n +1 个点,那么一定有点是重复使用的。这就说明该图中一定存在负环)
bool SPFA(){
queue<int> q;
for(int i=1;i<=n;++i){
q.push(i);
vis[i]=1;
}
while(!q.empty()){
int x=q.front();
q.pop();
vis[x]=0;
for(int i=head[x];~i;i=nxt[i]){
int v=to[i];
if(dis[v]>dis[x]+val[i]){
dis[v]=dis[x]+val[i];
cnt[v]=cnt[x]+1;
if(cnt[v]>=n) return true;
if(!vis[v]){
vis[v]=1;
q.push(v);
}
}
}
}
return false;
}
然而,SPFA在遇见菊花图那种全是负权回路的毒瘤图就直接凉凉 SPFA已死
Floyd
前面我们学习的算法主要用途都是解决单源最短路问题 如果要求解任意两点之间的
最短路,可以枚举所有源点,对所有点执行单源最短路算法,但这样比RE无疑
更方便的,我们可使用 Floyd 算法~~~
Floyd 是一种动态规划算法,稠密图效果最佳(效率要高于执行 n 次 Dijkstra 算法,也高于执行 n 次 SPFA 算法),边权可正可负
缺点:时间复杂度较高,不适合计算大量数据
void Floyd() {
for (int k = 1; k <= n; ++k) {//一定要先枚举中介点
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
}
}
}
}