主要算法 Dijkstra 算法 & Bellman-Ford 算法
Dijkstra 算法
原理
以点为研究对象的贪心策略。
实现步骤
- 将图中的顶点分为已经找到最短路的点(下面称黑点)和尚未找到最短路的点(下面称白点)。
- 在所有白点中。找到距离起点 s 最近的点 cur 并染成黑色,
vis[cur]=1
。 - 以 cur 为中转点,松弛 cur 的邻接点 y。 4. 重复步骤 22、步骤 33,直到所有点染成黑色。
时间复杂度 O(N^2)
优化策略
dis_idisi 会随着松弛(relax)操作更新,明显的,可以将其理解为动态求最小值,能用优先队列 priority_queue 优化。 用优先队列维护所有白点,并且是小根堆。
堆优化必用知识点——重载运算符(overload)
定义
是指将加减乘除等运算符修改为自定义的含义
标程
struct Node
{
int y;
long double val;
bool operator<(const Node &b)const
{
return val>b.val;
}
};
优化后Dijkstra时间复杂度
O(mlog n)
Dijkstra 与 BFS 的关系
BFS 是边权为 11 的 dijkstra。
注意事项
- 不能用于正负边权混杂的图。
- 正权不能跑最长路。
- 注意避免松弛操作溢出,
#define int long long
。 - 多次调用 dijkstra 要重置
vis[]
和dis[]
。
标程(弱化版)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5, M=1e5+5;
int dis[N], n, m, s; bool vis[N];
struct node
{
int y, val;
};
vector<node> nbr[N];
void dijkstra()
{
for(int i=1;i<=n;i++)
{
dis[i]=2147483647;
}
dis[s]=0;
for(int i=1;i<=n;i++)
{
int mini=1e18, id;
for(int j=1;j<=n;j++)
{
if(mini>dis[j]&&!vis[j])
{
id=j;
mini=dis[j];
}
}
vis[id]=1;
for(auto nxt:nbr[id])
{
int y=nxt.y, val=nxt.val;
if(dis[id]+val<dis[y])
{
dis[y]=dis[id]+val;
}
}
}
}
signed main()
{
cin>>n>>m>>s;
for(int i=1;i<=m;i++)
{
int x, y, w;
cin>>x>>y>>w;
nbr[x].push_back({y,w});
}
dis[s]=0;
dijkstra();
for(int i=1;i<=n;i++)
{
cout<<dis[i]<<" ";
}
}
标程(堆优化版)
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n, m, s, dis[N];
bool vis[N];
struct node
{
int y, val;
};
struct Node
{
int y, val;
bool operator<(const Node &b)const
{
return val>b.val;
}
};
vector<node> nbr[N];
void dijkstra()
{
for(int i=1;i<=n;i++)
{
dis[i]=2147483647;
}
dis[s]=0;
priority_queue<Node> q;
q.push((Node){s,0});
while(!q.empty())
{
Node now=q.top();
q.pop();
int cur=now.y;
if(vis[cur])
{
continue;
}
vis[cur]=1;
for(auto qq:nbr[cur])
{
int nxt=qq.y, w=qq.val;
if(dis[nxt]>dis[cur]+w)
{
dis[nxt]=dis[cur]+w;
q.push((Node){nxt,dis[nxt]});
}
}
}
}
signed main()
{
cin>>n>>m>>s;
for(int i=1;i<=m;i++)
{
int x, y, w;
cin>>x>>y>>w;
nbr[x].push_back((node){y,w});
}
dijkstra();
for(int i=1;i<=n;i++)
{
cout<<dis[i]<<" ";
}
return 0;
}
例题1——P1339
题目大意
有一个 n 个点 m 条边的无向图,请求出从 s 到 t 的最短路长度。
样例输入 #1
7 11 5 4 2 4 2 1 4 3 7 2 2 3 4 3 5 7 5 7 3 3 6 1 1 6 3 4 2 4 3 5 6 3 7 2 1
样例输出 #1
7
Floyd——80 pts 做法
就一模板。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, m, dis[2505][2505], t, s;
signed main()
{
cin>>n>>m>>s>>t;
for(int i=1;i<=2500;i++)
{
for(int j=1;j<=2500;j++)
{
dis[i][j]=1e9;
}
}
for(int i=1;i<=m;i++)
{
int x, y, w;
cin>>x>>y>>w;
dis[x][y]=dis[y][x]=w;
}
for(int i=1;i<=n;i++)
{
dis[i][i]=0;
}
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(dis[i][k]==1000000000||dis[k][j]==1000000000)
{
continue;
}
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
cout<<dis[s][t];
}
dijkstra——100 pts 做法
也就一模板,注意是无向图。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, m, s, t;
struct node
{
int y, val;
};
vector<node> nbr[2505];
int dis[2505];
bool vis[2505];
struct Node
{
int y, val;
bool operator<(const Node &b)const
{
return val>b.val;
}
};
void dijkstra()
{
memset(vis,0,sizeof vis);
priority_queue<Node> q;
for(int i=1;i<=n;i++)
{
dis[i]=2147483647;
}
q.push((Node){s,0});
dis[s]=0;
while(!q.empty())
{
Node now=q.top();
q.pop();
int cur=now.y;
if(vis[cur])
{
continue;
}
vis[cur]=1;
for(auto nxt:nbr[cur])
{
int y=nxt.y, val=nxt.val;
if(dis[cur]+val<dis[y])
{
dis[y]=dis[cur]+val;
q.push((Node){y,dis[y]});
}
}
}
}
signed main()
{
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++)
{
int x, y, w;
cin>>x>>y>>w;
nbr[x].push_back((node){y,w});
nbr[y].push_back((node){x,w});
}
dis[s]=0;
dijkstra();
cout<<dis[t];
}
例题2——P1629
题目大意
有一个邮递员要送东西,邮局在节点 1。他总共要送 n−1 样东西,其目的地分别是节点 22 到节点 n。由于这个城市的交通比较繁忙,因此所有的道路都是单行的,共有 m 条道路。这个邮递员每次只能带一样东西,并且运送每件物品过后必须返回邮局。求送完这 n-1 样东西并且最终回到邮局最少需要的时间。
样例输入 #1
5 10 2 3 5 1 5 5 3 5 6 1 2 8 1 3 8 5 3 4 4 1 8 4 5 3 3 5 6 5 4 2
样例输出 #1
83
分析
很明显,求起点到各个点的最短路,一遍是求起点到各个点最短路和,另一遍求各个点返回起点最短路和,跑两遍 Dijkstra。 建反图,两次 Dijkstra 之间要清空。
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5, M=1e5+5;
int dis[N], n, m, s;
bool vis[N];
struct node
{
int y, val;
};
struct Node
{
int y, val;
bool operator<(const Node &b)const
{
return val>b.val;
}
};
vector<node> nbr[N];
void dijkstra()
{
memset(vis,0,sizeof vis);
priority_queue<Node> q;
for(int i=1;i<=n*2;i++)
{
dis[i]=2147483647;
}
q.push((Node){s,0});
dis[s]=0;
while(!q.empty())
{
Node now=q.top();
q.pop();
int cur=now.y;
if(vis[cur])
{
continue;
}
vis[cur]=1;
for(auto nxt:nbr[cur])
{
int y=nxt.y, val=nxt.val;
if(dis[cur]+val<dis[y])
{
dis[y]=dis[cur]+val;
q.push((Node){y,dis[y]});
}
}
}
}
int t;
signed main(void)
{
int cnt=0;
cin>>n>>m;
s=1;
for(int i=1;i<=m;i++)
{
int x, y, w;
cin>>x>>y>>w;
nbr[x].push_back({y,w});
nbr[y+n].push_back({x+n,w});
}
dis[s]=0;
dijkstra();
for(int i=2;i<=n;i++)
{
cnt+=dis[i];
}
s=n+1;
dis[s]=0;
dijkstra();
for(int i=2+n;i<=n+n;i++)
{
cnt+=dis[i];
}
cout<<cnt;
}
拓展提升 P1346 P1576
P1576关键代码——Dijkstra
memset(vis,0,sizeof vis);
dis[s]=100.0;
q.push(s);
vis[s]=1;
while(!q.empty())
{
int cur=q.front();
q.pop();
vis[cur]=0;
for(auto qq:nbr[cur])
{
int nxt=qq.y;
double w=qq.w;
if(dis[cur]/w<dis[nxt])
{
dis[nxt]=dis[cur]/w;
if(!vis[nxt])
{
vis[nxt]=1;
q.push(nxt);
}
}
}
}
Bellman-Ford 算法
概述
Bellman-Ford 算法是由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特 创立的,求解单源最短路径问题的一种算法。
有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。
它的原理是对图进行 m-1 次松弛操作,得到所有可能的最短路径。
其优于 Dijkstra 算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达 O(n×m)。但算法可以进行若干种优化,提高了效率。
实现
双重循环枚举,和暴力差不多。
#include<bits/stdc++.h>
#define int long long
using namespace std;
struct node
{
int x, y, w;
}e[2000005];
int n, m, s;
int dis[200005];
void bellman_ford()
{
for(int i=1;i<=n;i++)
{
dis[i]=2147483647;
}
dis[s]=0;
for(int t=1;t<n;t++)
{
for(int i=1;i<=m;i++)
{
if(dis[e[i].x]+e[i].w<dis[e[i].y])
{
dis[e[i].y]=dis[e[i].x]+e[i].w;
}
}
}
}
signed main()
{
cin>>n>>m>>s;
for(int i=1;i<=m;i++)
{
cin>>e[i].x>>e[i].y>>e[i].w;
}
bellman_ford();
for(int i=1;i<=n;i++)
{
cout<<dis[i]<<" ";
}
}
SPFA 算法
死了的算法。
理论上对 Bellman-Ford 算法进行优化,但这种优化被证实可卡掉的。
用队列进行松弛。
除了队列优化(SPFA)之外,Bellman–Ford 还有其他形式的优化,这些优化在部分图上效果明显,但在某些特殊图上,最坏复杂度可能达到指数级。
- 堆优化:将队列换成堆,与 Dijkstra 的区别是允许一个点多次入队。在有负权边的图可能被卡成指数级复杂度。
- 栈优化:将队列换成栈(即将原来的 BFS 过程变成 DFS),在寻找负环时可能具有更高效率,但最坏时间复杂度仍然为指数级。
- LLL 优化:将普通队列换成双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则插入队首。
- SLF 优化:将普通队列换成双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则插入队首。
- D´Esopo–Pape 算法:将普通队列换成双端队列,如果一个节点之前没有入队,则将其插入队尾,否则插入队首。
上述“其他优化”来自 OI-WIKI
但一句话总结,都会被卡。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int t, n, m;
struct node
{
int y, w;
};
vector<node> nbr[N];
int cnt[N], dis[N];
bool vis[N];
void spfa()
{
memset(cnt,0,sizeof cnt);
memset(vis,0,sizeof vis);
memset(dis,0x3f,sizeof dis);
queue<int> q;
q.push(1);
vis[1]=1;
while(!q.empty())
{
int cur=q.front();
q.pop();
vis[cur]=0;
for(auto qq:nbr[cur])
{
int nxt=qq.y, val=qq.w;
if(dis[cur]+val<dis[nxt])
{
dis[nxt]=dis[cur]+val;
cnt[nxt]=cnt[cur]+1;
if(cnt[nxt]>n-1)
{
cout<<"YES\n";
return ;
}
q.push(nxt);
}
}
}
cout<<"NO\n";
return ;
}
signed main()
{
cin>>t;
while(t--)
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
nbr[i].clear();
}
for(int i=1;i<=m;i++)
{
int x, y, w;
cin>>x>>y>>w;
if(w>=0)
{
nbr[y].push_back((node){x,w});
}
nbr[x].push_back((node){y,w});
}
spfa();
}
}
我们用队列来维护哪些结点可能会引起松弛操作,就能避免访问不必要的边。
这个算法已经死了就不多赘述了。