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;
}

浙公网安备 33010602011771号