002.最短路径问题

最短路算法

BFS

BFS本身就是最短路算法,适合无权值的无向图

Dijkstra
基于贪心思想的单源最短路算法

\(e[u]\)存节点\(u\)的所有出边的终点和边权;

\(d[u]\)\(u\)到源点\(s\)的最小距离,初始化时,\(d[s]=0,d[u](u\neq s)=INF\)

\(vis[u]\)标记\(u\)是否出圈,初始化\(vis[u]=0\).

算法流程:

Dijkstra算法对点的处理顺序是距离源点由远及近的:

\((1)\)从源点出发,先遍历集合\(T\)内的点\((即vis[i]=0)\)找到距离源点最近的点\(i\)

\((2)\)将最近点\(i\)移除集合\(T\),并且更新该点的所有出边的终点\(j\)的结果:\(d[j]=\min(d[j],d[i]+w_{i,j})\)

P3371 【模板】单源最短路径(弱化版)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
const int M = 5e5 + 5;
const int INF = 2147483647;
struct node{
    int v,w;
};
vector<node> e[N];
int d[N],vis[N] = {0},n,m,s;

inline void add(int u,int v,int w){
    e[u].push_back({v,w});
}

void dijkstra(int s){
    for(int i = 0 ; i <= n ; i ++) d[i] = INF;
    d[s] = 0;
    for(int i = 1 ; i <= n-1 ; i ++){//枚举次数
        int u = 0;
        for(int j = 1 ; j <= n ; j ++)//枚举点
            if(!vis[j] && d[u] > d[j]) u = j;
        vis[u]=1;//标记u出圈
        for(auto ed : e[u]){//枚举邻边
            int v = ed.v , w = ed.w;
            if(d[v] > d[u] + w)
                d[v] = d[u] + w;
        }
    }
}

int main(){
    cin >> n >> m >> s;
    for(int i = 0 ; i < m ; i ++){
        int a,b,c;
        cin >> a >> b >> c;
        add(a,b,c);
    }
    dijkstra(s);
    for(int i=1;i<=n;i++)
    printf("%d ",d[i]);
    return 0;
}

时间复杂度:\(O(n^2)\)

堆优化版本

创建\(pair\)类型的大根堆\((存入\{-d[i],i\})\),距离取负值,这样保证距离最小的元素在堆顶

\((1)\)初始化\(\{0,s\}\)入队,\(d[s]=0\),其他点标记为正无穷;

\((2)\)堆顶弹出距离最小点\(u\),若\(u\)遍历过则跳过,否则打标记;

\((3)\)\(u\)的出边松弛操作,把\(\{-d[v],v\}\)压入队尾;

\((4)\)重复\((2)(3)\)步操作,直到队列为空

P4779 【模板】单源最短路径(标准版)
#include<bits/stdc++.h>
using namespace std;

const int N=100010;
int n,m,s,a,b,c;
struct edge{int v,w;};
vector<edge> e[N];
int d[N],vis[N];

void dijkstra(int s){
  memset(d,0x3f,sizeof d); d[s]=0; 
  priority_queue<pair<int,int>> q;
  q.push({0,s});
  while(q.size()){
    auto t=q.top(); q.pop();
    int u=t.second;
    if(vis[u])continue; //再出队跳过
    vis[u]=1; //标记u已出队
    for(auto ed : e[u]){
      int v=ed.v, w=ed.w;
      if(d[v]>d[u]+w){
        d[v]=d[u]+w;
        q.push({-d[v],v}); //大根堆
      }
    }
  }
}
int main(){
  cin>>n>>m>>s;
  for(int i=0; i<m; i++)
    cin>>a>>b>>c, e[a].push_back({b,c});
  dijkstra(s);
  for(int i=1;i<=n;i++) printf("%d ",d[i]); 
}

时间复杂度:\(O(m\log m)\)

Floyd算法

是动态规划算法,也是全源最短路算法

状态表示

\(d[k][i][j]\)表示从节点\(i\)到节点\(j\),且中间只经过节点\([1,k]\)的最短路径长度

状态计算

路径的选择分为两类:

\((1)\)路径不经过\(k\)点,继承原值:\(d[k][i][j]=d[k-1][i][j]\)

\((2)\)路径经过\(k\)点,松弛操作:\(d[k][i][j]=d[k-1][i][k]+d[k-1][k][j]\).

这里第一维变量\(k\)可以省略

初始化时采用邻接矩阵存图

\((1)\)\(i \neq j\),无边则\(d[i][j]=inf\);有边则\(d[i][j]=w_{i,j}\)

\((2)\)\(i=j\),则\(d[i][j]=0\).

void floyd(){
    for(int k = 1 ; k <= n ; k ++)
        for(int i = 1 ; i <= n ; i ++)
            for(int j = 1 ; j <= n ; j ++)
                d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}

显然时间复杂度为\(O(n^3)\)

路径的记录与递归输出
int d[N][N],p[N][N];

void floyd(){
    for(int k = 1 ; k <= n ; k ++)
        for(int i = 1 ; i <= n ; i ++)
            for(int j = 1 ; j <= n ; j ++)
                if(d[i][j] > d[i][k]+d[k][j]){
                    d[i][j] = d[i][k] + d[k][j];
                    p[i][j] = k;
                }
}

void path(int i,int j){
    if(p[i][j] = 0) return;
    int k = p[i][j];
    path(i,k);
    printf("%d",k);
    path(k,j);
}
利用Floyd算法解决最小环问题

解决最小环问题利用了Floyd算法分层计算的性质,把最小环拆成了三部分:

设最小环中编号最大的顶点为\(k\),环上与\(k\)相邻的两个点为\(i,j\),则在外层循环枚举到点\(k\)时,该环的长度为\(ans = d_{ij}+w_{jk}+w_{ki}\)

故在循环时对每个\(k\)枚举满足$i < k \(且\)j < k\(的点对\)(i,j)$优选答案即可

P6175 无向图的最小环问题
#include<bits/stdc++.h>
using namespace std;
const int N = 105;
int w[N][N],d[N][N],n,m,a,b,c,ans = 1e8; 

void floyd(){
    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,d[i][j] + w[k][i] + w[j][k]);
        for(int i = 1 ; i <= n ; i ++)
            for(int j = 1 ; j <= n ; j ++)
                d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
    }
}
int main(){
    cin >> n >> m;
    for(int i = 1 ; i <= n ; i ++)
        for(int j = 1 ; j <= n ; j ++)
            if(i != j) w[i][j] = 1e8;
    for(int i = 1 ; i <= m ; i ++){
        cin >> a >> b >> c;
        w[a][b] = w[b][a] = c;
    }
    memcpy(d,w,sizeof d);
    floyd();
    if(ans == 1e8) puts("No solution.");
    else cout << ans << endl;
    return 0;
}
Bellman-Ford算法

基于松弛操作的单源最短路算法

\(e[u]\)存储\(u\)点的出边的邻点和边权;

\(d[u]\)存储\(u\)到源点的距离.

算法流程

\((1)\)初始化\(d[s]=0\)\(d[u](u\neq s)= inf\);

\((2)\)执行多轮循环。每轮循环,对所有边都尝试进行一次松弛操作;

\((3)\)当一轮循环中没有成功的松弛操作时,算法停止.

该算法可以处理负边权兼判断是否存在负环

const int inf=0x3f3f3f3f;
bool bellmanford(int s){
    memset(d,inf,sizeof d); d[s] = 0;//初始化
    bool flag;//是否松弛
    for(int i = 1 ; i <= n ; i ++){//n轮
        flag = false;
        for(int u = 1 ; u <= n ; u ++){//n个点
            if(d[u] == inf) continue;//距离为无穷大肯定不能用来松弛
            for(auto ed : e[u]){//u的出边
                int v = ed.v, w = ed.w;
                if(d[v] > d[u] + w){//尝试松弛
                    d[v] = d[u] + w;
                    flag = true;
                }
            }
        }
        if(!flag) break;
    }
    return flag;//第n轮=true则有负环
}

时间复杂度:\(O(nm)\)

P3385 【模板】负环
 #include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int N=2010;
int n,m,s,a,b,c;
struct edge{int v,w;};
vector<edge> e[N];
int d[N];

bool bellmanford(){
    memset(d,inf,sizeof d); d[1] = 0;//初始化
    bool flag;//是否松弛
    for(int i = 1 ; i <= n ; i ++){//n轮
        flag = false;
        for(int u = 1 ; u <= n ; u ++){//n个点
            if(d[u] == inf) continue;//距离为无穷大肯定不能用来松弛
            for(auto ed : e[u]){//u的出边
                int v = ed.v, w = ed.w;
                if(d[v] > d[u] + w){//尝试松弛
                    d[v] = d[u] + w;
                    flag = true;
                }
            }
        }
        if(!flag) break;
    }
    return flag;//第n轮=true则有负环
}

int main(){
    int T; cin >> T;
    while(T--){
        cin >> n >> m;
        for(int i = 1 ; i <= m ; i ++){
            cin >> a >> b >> c;
            if(c >= 0){
                e[a].push_back({b,c});
                e[b].push_back({a,c});
            }
            else e[a].push_back({b,c});
        }
        if(bellmanford()) cout << "YES" << endl;
        else cout << "NO" << endl;
        for(int i = 1 ; i <= n ; i ++) e[i].clear();
    }
    return 0;
}
SPFA算法

Bellman ford算法的优化

只有本轮被更新过的点,才有资格对其出边进行松弛操作

\(vis[u]\)标记\(u\)是否在队列中;

\(cnt[v]\)记录边数,兼判负环.

算法流程

\((1)\)初始化\(s\)入队,标记\(s\)在队内,\(d[s]=0,d[u(u\ne s)]=inf\)

\((2)\)从队头弹出点\(u\),标记\(u\)不在队内;

\((3)\)枚举\(u\)的所有出边,执行松弛操作;记录\(s\)走到\(v\)的边数,并判断负环;如果\(v\)不在队内则把\(v\)压入队尾,并且打上标记;

\((4)\)重复\((2)(3)\)步操作,直到队列为空.

queue<int>q;
bool spfa(int s){
    memset(d,inf,sizeof d);
    d[s] = 0;
    vis[s] = 1;
    q.push(s);
    while(q.size()){
        int u = q.front();
        q.pop();
        vis[u] = 0;
        for(auto ed : e[u]){
            int v = ed.v, w = ed.w;
            if(d[v] > d[u] + w){//松弛操作
                d[v] = d[u] + w;
                cnt[v] = cnt[u] + 1;//记录边数
                if(cnt[v] >= n) return true;
                if(!vis[v]) q.push(v),vis[v] = 1;
            }
        }
    }
    return false;
}

最坏时间复杂度:\(O(nm)\)

P3385 【模板】负环
 #include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int N=2010;
int n,m,s,a,b,c;
struct edge{int v,w;};
vector<edge> e[N];

bool spfa(int s){
    int d[N],vis[N] = {0},cnt[N] = {0};
    queue<int>q;
    memset(d,inf,sizeof d);
    d[s] = 0;
    vis[s] = 1;
    q.push(s);
    while(q.size()){
        int u = q.front();
        q.pop();
        vis[u] = 0;
        for(auto ed : e[u]){
            int v = ed.v, w = ed.w;
            if(d[v] > d[u] + w){//松弛操作
                d[v] = d[u] + w;
                cnt[v] = cnt[u] + 1;//记录边数
                if(cnt[v] >= n) return true;
                if(!vis[v]) q.push(v),vis[v] = 1;
            }
        }
    }
    return false;
}


int main(){
    int T; cin >> T;
    while(T--){
        cin >> n >> m;
        for(int i = 1 ; i <= m ; i ++){
            cin >> a >> b >> c;
            if(c >= 0){
                e[a].push_back({b,c});
                e[b].push_back({a,c});
            }
            else e[a].push_back({b,c});
        }
        if(spfa(1)) cout << "YES" << endl;
        else cout << "NO" << endl;
        for(int i = 1 ; i <= n ; i ++) e[i].clear();
    }
    return 0;
}
Johnson算法

全源最短路算法

算法流程

\((1)\)新建一个 虚拟源点\(0\),从该点向其他所有点连一条边权为\(0\)的边,再用SPFA算法求出\(0\)到其他所有点的最短路\(h(i)\)

\((2)\)将新图的边权改造为:\(w_{u,v} + h(u) - h(v)\),这样确保边权非负;

\((3)\)以每个点为起点,跑\(n\)Heap-Dijkstra算法,求出任意两点最短路.

正确性证明

非负性

新图的边权:\(w'_{u,v}=w_{u,v}+h(u)-h(v)\)

\((1)0\)\(v\)的最短路径经过\(u\),则\(h(v)=h(u)+w_{u,v}\)

\((2)0\)\(v\)的最短路径不经过\(u\),则\(h(v) \le h(u)+ w_{u,v}\)

得证

最优性

新图的最短路\(d'_{s,t}= d_{s,t}+h(s)-h(t)\)

设新图上\(s\)点到\(t\)点经过的序列为:\(s \rightarrow x_1 \rightarrow ···\rightarrow x_n \rightarrow t\)

则其长度为\(d'_{s,t} = w'_{s,x_1} +\sum_1^{n-1}w'_{x_i,x_{i+1}}+w'_{x_n,t} = w_{s,x_1} + h(s) - h(x_1) +\sum_1^{n-1}(w_{x_i,x_{i+1}} +h(x_i)-h(x_i+1))+w_{x_n,t}+h(x_n)-h(t)\)

\(=w_{s,x_1} + \sum_1^{n-1}w_{x_i,x_{i+1}}+w_{x_n,t}+h(s)-h(t)=d_s,t + h(s)-h(t)\).

\(h\)也称为势能函数

P5905 【模板】全源最短路(Johnson)
#include<algorithm>
#include<cstring>
#include<iostream>
#include<queue>
#define N 30010
#define INF 1000000000
using namespace std;

int n,m,a,b,c;
struct edge{int v,w;};
vector<edge> e[N];
int vis[N],cnt[N];
long long h[N],d[N];

void spfa(){
    queue<int>q;
    memset(h,63,sizeof h);
    memset(vis,false,sizeof vis);
    h[0]=0,vis[0]=1;q.push(0);
    while(q.size()){
        int u=q.front(); q.pop();vis[u]=0;
        for(auto ed : e[u]){
            int v=ed.v,w=ed.w;
            if(h[v]>h[u]+w){
                h[v]=h[u]+w;
        cnt[v]=cnt[u]+1;
        if(cnt[v]>n){
          printf("-1\n");exit(0);
        }
                if(!vis[v])q.push(v),vis[v]=1;
            }
        }
    }
}
void dijkstra(int s){
    priority_queue<pair<long long,int>>q;
    for(int i=1;i<=n;i++)d[i]=INF;
    memset(vis,false,sizeof vis);
    d[s]=0; q.push({0,s});
    while(q.size()){
        int u=q.top().second;q.pop();
        if(vis[u])continue;
        vis[u]=1;
        for(auto ed : e[u]){
            int v=ed.v,w=ed.w;
            if(d[v]>d[u]+w){
                d[v]=d[u]+w;
                if(!vis[v]) q.push({-d[v],v});
            }
        }
    }
}
int main(){
  cin>>n>>m;
  for(int i=0;i<m;i++)
    cin>>a>>b>>c, e[a].push_back({b,c});
    for(int i=1;i<=n;i++)
      e[0].push_back({i,0});//加虚拟边
      
    spfa();
    for(int u=1;u<=n;u++)
      for(auto &ed:e[u])
        ed.w+=h[u]-h[ed.v];//构造新边
    for(int i=1;i<=n;i++){
        dijkstra(i);
        long long ans=0;
        for(int j=1;j<=n;j++){
            if(d[j]==INF) ans+=(long long)j*INF;
            else ans+=(long long)j*(d[j]+h[j]-h[i]);
        }
        printf("%lld\n",ans);
    }
    return 0;
}
posted @ 2025-06-16 20:07  _P_D_X  阅读(20)  评论(0)    收藏  举报