最短路算法

\(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 算法更新一下任意两点之间的最短路即可

本题保证询问增大时时间单调不降,
如果没有这一点的话需要存储一下所有的询问,
然后按照时间升序处理


习题

posted @ 2022-02-07 21:22  2K22  阅读(213)  评论(0)    收藏  举报