最短路算法
记 \(w_{i,j}\) 为 \((i,j)\) 边的长度(如有重边取最小值),
\(f_{i,j}\) 表示 \(i\) 到 \(j\) 的最短路径长
\(n,m\) 表示图的点数与边数
1.Floyd朴素算法
利用邻接矩阵,考虑枚举中转点 \(k\) 更新全图上两点之间的最短路径
则有 \((i,j)\) 之间的距离 \(f_{i,j}=\min(f_{i,j},f_{i,k}+f_{k,j})\)
初始化 \(f_{i,j}=\min(w_{i,j})\) 如果邻接矩阵没有边则赋值为 \(\text{inf}\)
注意实现的时候一定要先枚举中转点
优点:
编程复杂度低,并且状态转移方程可以多变
例如传递闭包问题。
此外,由于 Floyd 算法类似于广义矩阵乘法,在一定特殊问题上有奇效
详见倍增优化 DP 专题
缺点:时空复杂度高,不易优化。
单次矩阵乘法的复杂度显然是 \(O(n^3)\) 且无法优化
最短路
#include <bits/stdc++.h>
using namespace std;
#define map mymap
const int p=100+1;
int n,m,s,t;
int ans;
int map[p][p];
int main(){
cin>>n>>m>>s>>t;
memset(map,0x3f,sizeof(map));
int x,y;
for(int i=1;i<=m;i++){
cin>>x>>y;
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
map[i][j]=min(map[i][j],map[i][k]+map[k][j]);
}
}
}
cout<<map[s][t];
return 0;
}
最小环
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int w[105][105];
int s[105][105];
int n,m;
int x,y,l;
int ans=0x3f3f3f3f;
int main()
{
scanf("%d%d",&n,&m);
memset(w,0x3f3f3f3f,sizeof(w));
for (int i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&l);
w[x][y]=min(w[x][y],l);
w[y][x]=min(w[y][x],l);
}
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
s[i][j]=w[i][j];
}
}
for (int k=1;k<=n;k++)
{
for (int i=1;i<k;i++)//判断回路
{
for (int j=i+1;j<k;j++)
ans=min(ans,w[i][j]+s[i][k]+s[k][j]);
//此时 w[i][j]最短路一定不经过k点,因为更新还没有更新到k,0到k-1更新完毕,所以这个式子才是判断回路
}
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
w[i][j]=min(w[i][j],w[i][k]+w[k][j]);
}
}
}
printf("%d\n",ans);
return 0;
}
2.Dijsktra 迪杰斯特拉算法
已知起点固定为 \(s\),要求 \(s\) 到每个点的最短距离 \(f_{s,i}\)
要求图的边权均为正
假设当前最短路已经确定的点集为 \(V\), 尚未确定最短路的集合为 \(S\)
初始化 \(V=\{s\}\) 其余点均在 \(S\) 集合
取出 \(S\) 中满足当前 \(f_{s,x},x\in S\) 最小的点 \(x\),加入集合 \(V\)
随后用该点 \(x\) 和所有与 \(x\) 相连的点 \(y\) 更新 \(f_{s,y}=\min(f_{s,y},f_{s,x}+w_{x,y})\)
重复上述过程直到所有点加入 \(V\) 即可结束
正确性证明:
考虑每一个新加入 \(V\) 的点 \(x\),其最短路必然已经确定
首先从 \(s\) 到 \(x\) 经过的点全部属于 \(V\) 的路径,其最短路径长度已经更新完毕
其次证明不可能经过还没有确定最短路的点
考虑任意一个 \(x'\in S\),则必然有 \(f_{s,x'}\geq f_{s,x}\)
如果能够用 \(x'\) 更新 \(x\),那么必然有 \(f_{s,x}\geq f_{s,x'}+w_{x',x}\)
这与 \(f_{s,x'}+w_{x',x}>f_{s,x}\) 矛盾
综上我们发现不可能用 \(S\) 中的任何其他点更新 \(x\),所以 \(x\) 的最短路已经确定
所以这个算法每次能够正确维护最小值,并且肯定可以结束
复杂度分析:
最朴素的做法是暴力扫一遍 \(S\) 找出所求的点 \(x\)
每次要求加入一个点,显然这个加入过程会持续 \(n\) 次
时间复杂度为 \(O(n^2)\)
1.无优化
普通的Dijsktra算法
#include <bits/stdc++.h>
using namespace std;
int a[101][3];
double c[101];
bool b[101];
double f[101][101];
int n,m,s,t;
void in(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d",&a[i][1],&a[i][2]);
}
memset(f,127,sizeof(f));
scanf("%d",&m);
int x,y;
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y);
f[x][y]=f[y][x]=sqrt(pow(double(a[x][1]-a[y][1]),2)+pow(double(a[x][2]-a[y][2]),2));
}
scanf("%d%d",&s,&t);
}
void dij(){
memset(b,0,sizeof(b));
b[s]=1;
c[s]=0;
for(int i=1;i<=n;i++){
c[i]=f[s][i];
}
double minl;
for(int i=1;i<n;i++){
minl=1e30;
int k=0;
for(int j=1;j<=n;j++){
if((!b[j])&&(c[j]<minl)){
minl=c[j];
k=j;
}
}
if(k==0){
break;
}
b[k]=1;
for(int j=1;j<=n;j++){
if(c[k]+f[k][j]<c[j]){
c[j]=c[k]+f[k][j];
}
}
}
}
void out(){
printf("%.2lf",c[t]);
}
int main(){
in();
dij();
out();
return 0;
}
发现上述过程要求找最小值,可以用堆来维护
对于每一条边的更新,对应插入一个新的决策点
每次取出来的点就是所求的点 \(x\)
确定一个点之后就会插入一些新的决策
不过这些决策的总数肯定是 \(m\)
单次堆的操作开销是 \(\log n\)
总时间复杂度就是 \(O(m\log n)\)
这个堆可以直接使用 STL::priority_queue (即优先队列)实现
由于优先队列不支持实时修改,只能插入和删除,所以时间复杂度是 \(O(m\log m)\)
体现在效率上可以认为是常数稍大一些
2.priority_queue 优化
优先队列优化
#include <bits/stdc++.h>
using namespace std;
const int o=1e5;
int n,m,s,t,cnt,x,y,z;
int dis[o],head[o];
bool vis[o];
struct path{
int s;
int t;
int v;
int n;
}a[o];
struct node{
int id;
int exp;
}p;
struct cmp{
bool operator()(node a,node b){
return a.exp>b.exp;
}
};
priority_queue<node,vector<node>,cmp>q;
void add(int st,int t,int v){
cnt++;
a[cnt].s=st;
a[cnt].t=t;
a[cnt].v=v;
a[cnt].n=head[st];
head[st]=cnt;
}
void in(){
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
}
void dij(int s){
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[s]=0;
p.id=s;
p.exp=0;
q.push(p);
while(!q.empty()){
p=q.top();
q.pop();
int to=p.id;
if(vis[to]){
continue;
}
vis[to]=1;
for(int i=head[to];i>0;i=a[i].n){
int j=a[i].t;
if(dis[j]>dis[to]+a[i].v){
dis[j]=dis[to]+a[i].v;
if(!vis[j]){
node r;
r.id=j;
r.exp=dis[j];
q.push(r);
}
}
}
}
}
void out(){
cout<<dis[t];
}
int main(){
in();
dij(s);
out();
return 0;
}
3.Bellman-Ford 算法和 SPFA 算法
如果不加顺序地考虑对所有边集里的边 \((x,y)\) 均执行一遍 \(f_{s,x}=\min(f_{s,x}+w_{x,y})\)
那么显然这一轮里面至少有一个点的最短路被更新了,直到每个点都不能被更新
也就是说所有点都满足 \(f_{s,x}\leq f_{s,y}+w_{y,x}\) 的时候算法就结束了
正确性证明:
上面的那个不等式就是三角不等式。
显然,最短路一定满足这个东西
假设最短路上某一点 \(x\) 不满足这个条件,
那么一定可以用 \(f_{s,x}=\min(f_{s,x}+w_{x,y})\) 更新一遍 \(x\) 的最短路
所以这个算法能够正确得到每个点的最短路
复杂度证明:
这个式子会在当前的 \(x\) 中多经过一个点 \(y\)
这会导致最短路的长度 \(+1\)
如果最短路存在的话,最短路不会成环
所以循环至多 \(n\) 次,总时间复杂度为 \(O(nm)\)
证明
这个性质需要对环的总权值分类讨论
总权值为正或 0,则走这个环必然增大原有最短路的长度/没有必要,去掉即可
总权值为负,任意和负环连通的点都能够通过不断走这个环减小路径长,此时最短路不存在
因此最短路不会成环
优化与SPFA:
注意到只有当前更新了最短路的点,才能引发下一轮最短路的更新
用一个队列维护最短路更新的点,每次取出队头进行下一轮更新
由于队列先进先出的性质,上下两轮之间永远是先更新上一轮,再更新下一轮
这个算法的时间复杂度是 \(O(km)\)
其中 \(k\) 是一个较小的常数,在随机图上通常不会大于 \(2\)
SPFA
#include <bits/stdc++.h>
using namespace std;
#define map mymap
queue<int>q;
int n,m,map[2021][2022],dis[2005];
bool vis[2005];
void spfa(){
q.push(1);
vis[1]=true;
dis[1]=0;
while(!q.empty()){
int x=q.front();
q.pop();
vis[x]=false;
for(int i=1;i<=n;i++){
if(map[x][i]>0&&(min(dis[x],map[x][i])>dis[i]||!dis[x])){
if(!dis[x]){
dis[i]=map[x][i];
}
else{
dis[i]=min(dis[x],map[x][i]);
}
if(!vis[i]){
vis[i]=true;
q.push(i);
}
}
}
}
}
int main(){
scanf("%d",&n);
memset(map,0,sizeof(map));
for(int i=2;i<=n;i++){
dis[i]=0;
}
int f,t,p;
while(scanf("%d%d%d",&f,&t,&p)==3){
if(!f&&!t&&!p){
break;
}
else{
map[f][t]=p;
}
}
spfa();
for(int i=2;i<=n;i++){
printf("%d\n",dis[i]);
}
return 0;
}
再贴一下二维 SPFA 和 SPFA 判断有无负环
upd:二维实际上就是做 \(n\) 次 SPFA,判断负环入队次数考虑 \(k\) 的大小
随机图上 \(k\) 大于 \(3\) 直接认定有负环问题并不是非常大但是还是错的
二维SPFA
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std ;
const int INF=0xfffffff ;
struct node{
int s,t,v,nxt ;
}e[1000005] ;
int n,m,k,cnt,head[100005],vis[100005][55],dis[100005][55] ;
void add(int s,int t,int v)
{
e[cnt].s=s ;
e[cnt].t=t ;
e[cnt].v=v ;
e[cnt].nxt=head[s] ;
head[s]=cnt++ ;
}
void spfa(int s)
{
for(int i=0 ;i<=n ;i++)
for(int j=0 ;j<55 ;j++)
dis[i][j]=INF ;
dis[s][0]=0 ;
memset(vis,0,sizeof(vis)) ;
vis[s][0]=1 ;
queue <pair<int,int> > q ;
q.push(make_pair(s,0)) ;
while(!q.empty())
{
pair<int,int> u=q.front() ;
q.pop() ;
vis[u.first][u.second]=0 ;
int step=u.second+1 ;
if(step>k)step=k ;
for(int i=head[u.first] ;i!=-1 ;i=e[i].nxt)
{
int tt=e[i].t ;
if(dis[tt][step]>dis[u.first][u.second]+e[i].v)
{
dis[tt][step]=dis[u.first][u.second]+e[i].v ;
if(!vis[tt][step])
{
vis[tt][step]=1 ;
q.push(make_pair(tt,step)) ;
}
}
}
}
}
int main()
{
while(~scanf("%d%d",&n,&m))
{
cnt=0 ;
memset(head,-1,sizeof(head)) ;
while(m--)
{
int a,b,c ;
scanf("%d%d%d",&a,&b,&c) ;
add(a,b,c) ;
add(b,a,c) ;
}
int s,t ;
scanf("%d%d%d",&s,&t,&k) ;
k=k/10+(k%10!=0) ;
spfa(s) ;
if(dis[t][k]==INF)puts("-1") ;
else printf("%d\n",dis[t][k]) ;
}
return 0 ;
}
SPFA判断负环
#include<iostream>
#include<queue>
#include<cstring>
#include<cstdio>
using namespace std;
int const maxNum=1001;
int const Infinity=99999999;
int map[maxNum][maxNum],dis[maxNum];//dis用来存放点的最佳路径
int nodeNum,time[maxNum];//time用来记录结点入队的次数
int vst[maxNum],visited[maxNum];//标志是否入队,标志点是否扫描过
bool SPFA(int start)//经典的SPFA算法
{
int i,p;
queue<int> que;
memset(vst,0,sizeof(vst));
memset(time,0,sizeof(time));
for(i=1;i<=nodeNum;i++)
dis[i]=Infinity;
dis[start]=0;
vst[start]=1;
que.push(start);
time[start]++;//起点先标志入队一次
while(!que.empty())
{
p=que.front();
que.pop();
vst[p]=0;
for(i=1;i<=nodeNum;i++)
{
visited[i]=true;//这里用来标志已经被扫描过的点。注意visited跟vst数组的区别
if(dis[p]+map[p][i]<dis[i])
{
dis[i]=dis[p]+map[p][i];
if(!vst[i])
{
que.push(i);
time[i]++;
if(time[i]>nodeNum)//当同一结点入队次数超过点的总数-1,即大于等于nodeNum时,存在负环,此题的关键
return true;
vst[i]=1;
}
}
}
}
return false;
}
int main(void)
{
int cas,n,num,i,s,e,w,j;
scanf("%d",&cas);//田地的个数
while(cas--)
{
scanf("%d%d%d",&nodeNum,&n,&num);//结点数,边数,虫洞数
for(i=1;i<=nodeNum;i++)
for(j=1;j<=nodeNum;j++)
map[i][j]=Infinity;
for(i=1;i<=n;i++)
{
scanf("%d%d%d",&s,&e,&w);//边的起点,边的终点,走这条边所花的时间
if(map[s][e]>=w)
{
map[s][e]=w;//注意重边这种情况,取最小的那个
}
if(map[e][s]>=w)//双向边,应该每次都比较一下,(但是这道题目不比较也过)
{
map[e][s]=w;
}
}
for(i=1;i<=num;i++)
{
scanf("%d%d%d",&s,&e,&w);
if(map[s][e]>-w)
map[s][e]=-w;//这道题目这里似乎说明得不是很严谨,如果一条边原来是Infinity,而现在有一个负的边,那不就替代了么
//题目似乎被理想化了,这种情况不考虑在其中,不知道说得对不对,望高手指教
}
memset(visited,0,sizeof(visited));
for(i=1;i<=nodeNum;i++)//考虑到图可能不是完全连通图,即存在离散的子图
{
if(visited[i]) continue;//增加这一步以减少不必要的计算
if(SPFA(i))
{
cout<<"YES"<<endl;
break;
}
} if(i==nodeNum+1)
cout<<"NO"<<endl;
}
return 0;
}
例题
P1462
二分最大值和最小值,
每次加入比最大值小的所有边,
检查最短路的长度是否小于等于 \(b\)
满足则为可行的最大值/最小值
P2910
直接 Floyd 求出任意两点之间的最短路即可
前后之间累加即可得出答案
P4568
把一个点拆成 \(k\) 个点,
即令 \((i,j)\) 这个节点表示用掉 \(j\) 张免费票之后到达 \(i\) 的状态
对于一条边 \((x,y)\)
连 \((x,i)\) 到 \((y,i)\) 边权为 \(w_{x,y}\) 的无向边
再连 \((x,i)\) 到 \((y,i+1)\) 和 \((y,i)\) 到 \((x,i+1)\) 边权均为 \(0\) 的两条有向边
重新建图跑最短路,起点为 \((1,0)\) 终点为 \((n,k)\)
不放心的话可以找 \(\min\limits_{i=0}^{k} f_{(n,k)}\)
不过贪心地看,这个没有必要
P1119
随着询问增大时间单调不降,
可以维护一个指针 \(now\) 表示当前时间已经有哪些节点被加进来了
每次新加入一个节点时,使用 Floyd 算法更新一下任意两点之间的最短路即可
本题保证询问增大时时间单调不降,
如果没有这一点的话需要存储一下所有的询问,
然后按照时间升序处理

浙公网安备 33010602011771号