最短路 学习笔记

BFS

过程

如果图的边权只有一种,那么在 BFS 的过程中松弛即可。

否则如果有两种边权,一种为 \(0\),使用 0-1 BFS。使用双端队列,对于大的边权入队尾,\(0\) 边权入队首即可。

与 Dijkstra 和 SPFA 的关系

这两种算法与 BFS 的过程很像。

将 BFS 的队列换成堆就成了堆优化 Dijkstra。比如 0-1 BFS 其实就在维护队列的单调性。

在 BFS 中让点在最短路更新后重新入队,就成了 SPFA。

Floyd

过程

Floyd算法可以 \(O(n^3)\) 求图的全源最短路径。

对于邻接矩阵 \(dis_{i,j}\),设dp数组 \(f_{k,i,j}\) 表示经过前 \(k\) 个点中转,第 \(i\) 个点到第 \(j\) 个点的最短路长度。可以枚举 \(i,j\) 进行松弛,转移方程 \(f_{k,i,j}=\min\{f_{k-1,i,j},f_{k-1,i,k}+f_{k-1,k,j}\}\)

然后发现可以压掉 \(k\) 这一维,直接在邻接矩阵上dp。

代码:

for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);

注意事项:

  1. Floyd 可以判负环。当 \(f_{i,i}<0\) 时显然存在负环,不过无法判断负环是否可达。

  2. Floyd 的最外层一定是 \(k\)。结合 dp 的过程容易理解。

传递闭包

传递闭包类似于图的邻接矩阵,能到达为 \(1\),不能到达为 \(0\)

同上,如果 \(f_{i,k}=1,f_{k,j}=1\)\(f_{i,j}=1\)

for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)d[i][j]|=d[i][k]&d[k][j];

这个东西可以用 bitset 优化。

for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(d[j][i])d[j]|=d[i];

意思是如果 \(j\) 能达到 \(i\)\(i\) 能达到的点可以被 \(j\) 达到。

复杂度 \(O(\frac{n^3}{w})\)

最小环

在枚举 \(i,j\) 时,已经求出了 \(i,j\) 经过前 \(k-1\) 个点的最短路(不经过点 \(k\))。此时 \(i\to j\to k\to i\) 为一个环。

long long a[105][105],ans=0x3f3f3f3f3f3f3f3f;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)a[i][j]=dis[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(dis[i][j]+a[i][k]+a[k][j],ans);
  for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dis[i][j]=dis[j][i]=min(dis[i][j],dis[i][k]+dis[k][j]);
}

运用Floyd本质解题

P1119

在这一题中村庄重建即为允许经过该村庄中转。Floyd 的过程就是每次加入一个新点允许中转,因此当村庄重建时仿照Floyd内层循环松弛即可。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,q,dis[205][205],t[205];
int main(){
  memset(dis,0x3f,sizeof(dis)),cin>>n>>m;
  for(int i=1;i<=n;i++)cin>>t[i],dis[i][i]=0;
  for(int i=1,u,v,w;i<=m;i++)cin>>u>>v>>w,dis[u+1][v+1]=dis[v+1][u+1]=w;
  t[n+1]=0x3f3f3f3f,cin>>q;
  for(int i=1,x,y,c,pos=0;i<=q;i++){
    cin>>x>>y>>c,x++,y++;
    for(;t[pos]<=c;pos++)for(int j=1;j<=n;j++)for(int k=1;k<=n;k++)dis[j][k]=min(dis[j][k],dis[j][pos]+dis[pos][k]);
    cout<<((t[x]>c||t[y]>c||dis[x][y]==0x3f3f3f3f)?-1:dis[x][y])<<'\n';
  }
  return 0;
}

Dijkstra

过程

Dijkstra算法可以求解单源最短路径,其本质是贪心。流程如下:

  1. 将起点的距离设为 \(0\),其余设为正无穷。开一个标记数组,表示结点的最短路是否确定,初始都为假。
  2. 取出距离最近且标记为假的点,将这个点的标记改为真。
  3. 对这个点的出边连向的点进行松弛。
  4. 不断重复 2、3 直到所有点的标记为真。
bool vis[n+5];
void dijkstra(int st,int n,vector<pair<int,long long>>e[],long long dis[]){  
  for(int i=1;i<=n;i++)dis[i]=0x3f3f3f3f3f3f3f3f;
  dis[st]=0;
  while(1){
    int now=0;
    for(int i=1;i<=n;i++)if(!vis[i]&&dis[now]>dis[i])now=i;
    if(!now)break;
    vis[now]=1;
    for(int i=0;i<e[now].size();i++)dis[e[now][i].first]=min(dis[e[now][i].first],dis[now]+e[now][i].second);
  }
}

证明

设起点为 \(s\),点 \(u\) 是在第二步中确定最短路的点。如果此时点 \(u\) 的距离不是实际最短路长度,那么必然可以通过一个点松弛,设这个点为 \(x\)

\(x\) 未被标记,由于 \(x\)\(u\) 更晚标记,有 \(dis(x)\geq dis(u),dis(x)+w(x,u)\geq dis(u)\),松弛不成立。

\(x\) 已经被标记,那么 \(x\) 被标记时松弛过 \(u\),重复松弛没有意义。

综上,\(u\) 的最短路长度此时已确定。

然而,当图中存在负边时,\(,dis(x)+w(x,u)\geq dis(u)\) 不一定成立。所以 Dijkstra 只能求解非负边权图的最短路。

堆优化

上面的程序直接暴力找距离最小的点是 \(O(n^2)\) 的。这个过程可以用堆来优化。用堆存储当前可能为距离最小的点,每次取出队首,进行第 2、3 步,然后将出边连向的未标记的点加入堆,直到堆为空。

bool vis[n+5];
priority_queue<pair<long long,int>,vector<pair<long long,int>>,greater<pair<long long,int>>>q;
void dijkstra(int st,int n,vector<pair<int,long long>>e[],long long dis[]){
  for(int i=1;i<=n;i++)dis[i]=0x3f3f3f3f3f3f3f3f;
  dis[st]=0,q.push(make_pair(0,st));
  while(!q.empty()){
    int now=q.top().second;
    q.pop();
    if(vis[now])continue;
    vis[now]=1;
    for(int i=0;i<e[now].size();i++){
      int v=e[now][i].first;
      long long w=e[now][i].second;
      if(dis[v]>dis[now]+w)dis[v]=dis[now]+w,q.push(make_pair(dis[v],v));
    }
  }
}

直接使用 STL 的优先队列是 \(O(m\log m)\) 的,使用堆的种类不同,复杂度也有所变化。由于 Dijkstra 的效率相对稳定,在能使用 Dijkstra 时,尽量使用。

关于SPFA,它死了

Bellman-Ford

在最短路算法中常能见到松弛操作:对于点 \(u\),尝试通过另一个点 \(v\) 中转,即 \(dis(u)=\min(dis(u),dis(v)+w(u,v))\)。Bellman-Ford 算法每一次会枚举每条边进行松弛操作。

在图上,最短路一定没有回路,也就是说最多经过 \(n-1\) 条边。若路径上存在负环,则最短路不存在;否则去掉这个环的路径更短。因此只需要松弛 \(n-1\) 轮即可。

如果图上有负环,就可以一直松弛,因此若第 \(n\) 轮松弛成功则存在负环。

void bellman_ford(int st,int n,vector<pair<int,long long>>e[],long long dis[]){
  for(int i=1;i<=n;i++)dis[i]=0x3f3f3f3f3f3f3f3f;
  dis[1]=0;
  for(int i=1;i<n;i++)for(int j=1;j<=n;j++)for(int k=0;k<e[j].size();k++)dis[e[j][k].first]=min(dis[e[j][k].first],dis[j]+e[j][k].second);
}

复杂度 \(O(nm)\)

SPFA

过程

SPFA算法是 Bellman-Ford 的优化。

如果一个点在上一轮未被松弛,那么这一轮经由这个点中转仍然无法松弛成功。因此可以用一个队列存可能引起松弛操作的点。

由 SPFA 的原理可知,如果一个点入队 \(n\) 次及以上则存在可达负环。

实际上,SPFA 的最坏情况可以被卡回 Bellman-Ford。因此一定要慎用。

bool vis[n+5];
int cnt[n+5];
queue<int>q;
bool spfa(int st,int n,vector<pair<int,long long>>e[],long long dis[]){
  for(int i=1;i<=n;i++)dis[i]=0x3f3f3f3f3f3f3f3f;
  dis[st]=0,q.push(st),vis[st]=1,cnt[st]++;
  while(!q.empty()){
    int now=q.front();
    vis[now]=0,q.pop();
    for(int i=0;i<e[now].size();i++){
      int v=e[now][i].first;
      long long w=e[now][i].second;
      if(dis[v]>dis[now]+w){
        dis[v]=dis[now]+w;
        if(!vis[v]){
          vis[v]=1,q.push(v),cnt[v]++;
          if(cnt[v]>=n)return 1;
        }
      }
    }
  }
  return 0;
}

常见优化

如果要使用 SPFA,可以加一些优化减少暴毙的可能性。

SLF 优化:如果入队结点的距离大于队首则插入队尾,否则插入队首。

LLL 优化:取出新结点时,若当前结点的距离大于队内距离的平均值则放到队尾,改为取下一个。

Johnson

过程

Johnson 算法可以通过对边权的修改,解决掉负边权问题,跑 \(n\) 轮 Dijkstra 求出全源最短路。

首先建立超级源点并向其他点连权为 \(0\) 的边,以这个点为起点跑 Bellman-Ford 或者 SPFA 求出单源最短路并判负环。即这个点到其他点的距离为 \(h\)

然后对于每条边 \(u\to v\),重设边权 \(w\)\(w+h_u-h_v\)。此时跑 \(n\) 轮 Dijkstra,有点 \(u\) 到点 \(v\) 的最短路长度为 \(dis_{u,v}-h_u+h_v\)

时间瓶颈在于 \(n\) 轮 Dijkstra,复杂度一般为 \(O(nm\log m)\)

证明

Johnson 精妙之处在于重设边权。

考虑一种简单的重设边权的方式,对于每一条边加上一个大数。然而这样对于两条长度不同的路径,加上的值不同。这样最短路可能变化。

对于路径 \(u\to p_1\to p_2\to\cdots\to p_k\to v\),重设边权后最短路长度为 \((w(u,p_1)+h_u-h_{p_1})+(w(p_1,p_2)+h_{p_1}-h_{p_2})+\cdots+(w(p_k,v)+h_{p_k}-h_v)\)。而 \(h\) 会抵消,剩下 \(w(u,p_1)+w(p_1,p_2)+\cdots+w(p_k,v)+h_u-h_v\)。与原来相比增加 \(h_u-h_v\)。这个值对于 \(u\to v\) 的所有路径是一样的,那么最短路不会改变。

由于 \(h\) 是最短路,满足 \(h_v\leq h_u+w(u,v)\),即 \(0\leq w(u,v)+h_u-h_v\)。因此重设边权后边权非负。

次短路

非严格次短路

允许次短路与最短路的长度相同。

\(O(n^3)\)\(O(nm\log m)\) 做法:

次短路与最短路至少一条边不同。那么先记录最短路,然后枚举这一条不同的边断开跑最短路。

严格次短路

不允许次短路与最短路的长度相同。

法一:两次最短路。

跑出起点的单源最短路径和所有点到终点的最短路径 \(dis1,dis2\)。(建反图跑终点的单源最短路)。显然次短路中会存在一条边不按最短路走,其他边都走最短路。设这条边为 \(u\to v\),则一条路径为 \(dis1(u)+w(u,v)+dis2(v)\),枚举即可。

感觉这一种方式最靠谱,复杂度就是最短路算法的复杂度。

法二:维护最短路和次短路 \(dis1,dis2\),分讨松弛的情况。

\(dis1(v)>dis1(u)+w(u,v)\):松弛最短路,原来的最短路变为次短路。

\(dis2(v)>dis1(u)+w(u,v)\):用 \(u\) 的最短路松弛 \(v\) 的次短路。

\(dis2(v)>dis2(u)+w(u,v)\):松弛 \(v\) 的次短路。

感觉这一种方法比较不靠谱,由于松弛时可能更新不了最短路但能更新次短路,因此 SPFA 中次短路被更新也要入队,dijkstra 中不能使用 vis,网上暂时没找到复杂度证明。

输出方案

记录一下前驱 \(pre\),松弛时 \(pre_u\gets v\)。输出方案时从终点倒着往回走到起点。

[[图论]]

posted @ 2024-03-01 09:38  lgh_2009  阅读(2)  评论(0编辑  收藏  举报