最短路

最短路

记号约定

\(n\)是点数,\(m\)是边数。

\(s\)是源点。

\(D\left(u\right)\)是从\(s\)点到\(u\)点的实际最短路,\(D\left(u,v\right)\)是从\(u\)点到\(v\)点的实际最短路。

\(dis\left(u\right)\)是从\(s\)点到\(u\)点的即时最短路,\(dis\left(u,v\right)\)是从\(u\)点到\(v\)点的即时最短路。

\(w\left(u,v\right)\)是从\(u\)点到\(v\)点的边权值。

说明

若一个图上存在负环,即负权边连成的环,就是负环。在有负环的图中,我们可以不断的绕着负环,所以最短路为\(-\infty\)

单源最短路

单源最短路,是从一个源点到其余点的最短路。

\(SPFA\)

\(Shortest\ Path\ Faster\ Algorithm\) (更快的最短路)

松弛操作

对于节点\(u\),对于所有与\(u\)点相连的\(v\)节点,使\(dis\left(v\right)=\min\left(dis\left(u\right),dis\left(u\right)+w\left(u,v\right)\right)\)

这么做的目的是显然的,用\(s\to u\)的最短路去更新\(s\to v\)的最短路。

过程

我们发现只有被进行过松弛操作的节点才有用来松弛的价值,这是显然的,如果没有经过松弛,就没有可能更新最短路。

所以我们可以使用一个队列,将被松弛过的节点放入队列中,每次从队头取出节点进行松弛,最后队列为空,就表示最短路算法的结束。

在实际实现中,要判断是否存在负环。实际上,我们可以证明一个节点最多被松弛\(n\)次,所以在实现中,我们记录每个节点松弛的次数,就能判断出负权图。

性质

时间复杂度\(\Omega\left(km\right)\)\(O\left(nm\right)\)\(k\)为常数,一般为二。

由于代码中没有对最短路的不降要求,所以可以用于有负权边的图。

\(Code:\)
void SPFA(int s){
    memset(dis,0x7f,sizeof(dis));
    memset(vis,false,sizeof(vis));
    dis[s]=0;
    vis[s]=true;
    q.push(s);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        vis[u]=false;
        for(int i=head[u];i;i=nxt[i]){
            int v=to[i];
            if(dis[v]>dis[u]+val[i]){
                dis[v]=dis[u]+val[i];
                if(!vis[x]){
                    vis[v]=true;
                    q.push(v);
                }
            }
        }
    }
}
应用

由于\(SPFA\)可以处理负权边,所以可以用来寻找负环。
还可以解不等式组。

定义

差分约束是一种特殊的一元不等式组,其中每一项都形如\(x_{i}-x_{j}\le m_{k}\),其中\(m_{k}\)是常数。
差分约束算法可以求出一组可行解。

推理

我们注意到,每一个不等式都可以变形成\(x_{i}\le x_{j}+m_{k}\)
同时对于一个图的最短路\(dist[]\),一定有\(dist(y)<=dist(x)+val(x,y)\)
我们发现这两个式子的形式极为相似,于是考虑转化。
我们将\(x_{i}\)\(x_{j}\)视为节点,\(m_{k}\)视为边权,从\(x_{j}\)\(x_{i}\)连一条权值为\(m_{k}\)的有向边。

过程

设立\(0\)号节点,向所有节点连一条\(0\)边,然后从\(0\)开始跑单源最短路。
如果图中存在负环,则无解;否则\(x_{i}=dist[i]\)是一组可行解。

扩展

\(x_{i}-x_{j}\ge m_{k}\Leftrightarrow x_{j}-x_{i}\le -m_{k}\Leftrightarrow add(i,j,-m_{k})\)
\(x_{i}=x_{j}\Leftrightarrow x_{i}-x_{j}\ge 0,x_{i}-x_{j}\le 0\Leftrightarrow add(i,j,0),add(j,i,0)\)
\(\frac{x_{i}}{x_{j}}\le m_{k}\Leftrightarrow \log_{2}x_{i}-\log_{2}x_{j}\le m_{k}\Leftrightarrow add(j,i,\log_{2}m_{k})\)

\(Code:\)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5*1e3+5;
int to[N<<1],nxt[N<<1],val[N<<1],head[N],tot=0,cnt[N],dis[N];
bool vis[N];
queue<int>q;
inline int read(){
    int x=0;bool s=0;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')s=1;c=getchar();}
    while('0'<=c&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    return s?-x:x;
}
inline void write(int x){
    if(x<0)putchar('-'),x=-x;
    if(x>9)write(x/10);
    putchar(x%10+'0');
}
void add(int u,int v,int w){
	to[++tot]=v;
	nxt[tot]=head[u];
	val[tot]=w;
	head[u]=tot;
}
int main(){
	int n=read(),m=read();
	for(int i=1;i<=n;i++)add(0,i,0);
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=read();
		add(v,u,w);
	}
	memset(dis,0x7f,sizeof(dis));
	dis[0]=0;
	q.push(0);
	vis[0]=true;
	cnt[0]++;
	while(!q.empty()){
		int u=q.front();
		q.pop();
		vis[u]=false;
		for(int i=head[u];i;i=nxt[i]){
			int v=to[i];
			if(dis[v]>dis[u]+val[i]){
				dis[v]=dis[u]+val[i];
				if(!vis[v]){
					q.push(v);
					vis[v]=true;
					cnt[v]++;
					if(cnt[v]==n+1){
						printf("NO");
						return 0;
					}
				}
			}
		}
	}
	for(int i=1;i<=n;i++)printf("%d ",dis[i]);
	return 0;
}
题目

P5960 【模板】差分约束算法 模板题
P1260 工程规划 双倍经验
P1993 小 K 的农场

\(Dijkstra\)

\(Dijkstra\)只能用在非负权边的图上。

过程

将原图中的点分为两个集合,\(T\)所有未确定最短路的节点集合,\(S\)为已确定的集合,显然\(D\left(s\right)=dis\left(s\right)=0\)

从源点开始,反复重复下列操作:

\(1.\)\(T\)集合中取出最短路最小的节点\(u\),放入集合\(S\)

\(2.\)\(u\)去松弛所有出边\(\left(u,v\right)\)

直到\(T=\emptyset\),算法结束。

正确性证明

\(T\)集合为未确定最短路的集合,移出\(T\)集合就意味着\(u\)点的最短路确定,即\(dis\left(u\right)=D\left(u\right)\),证明的关键,就是证明只用一次松弛就能将最短路确定下来。

我们设\(k\)为当前操作次数。

命题:\(\forall k\in N_{+},D\left(u\right)=dis\left(u\right)\)

证明:

\(k=1\)时,\(S=\left\{s\right\}\)\(D\left(u\right)=dis\left(u\right)=0\)

设当\(k=l\)时成立,当\(k=l+1\)时:

\(v\)点是\(l+1\)步时选择的节点,我们设\(v\)点是由\(u\)点松弛的,则\(u\in S\),即\(D\left(u\right)=dis\left(u\right)\)

\(v\)点的最短路径不是\(s\to u\to v\),则在\(v\)点的最短路径上有一个或多个节点\(\in T\),即存在一条路径\(s\to x\to y\to v\),其中\(y\)是路径上第一个不属于\(T\)集合的节点,\(x\)\(y\)的前驱节点。

因为边权非负,有\(D\left(y\right)\le dis\left(y\right)\le D\left(v\right)\le dis\left(v\right)\)

由于当\(v\)节点被取出时,\(y\)节点没有被取出,所以\(dis\left(v\right)\le dis\left(y\right)\)

\(\therefore D\left(v\right)=dis\left(v\right)\)

\(\therefore s\to u\to v\)\(v\)点的最短路

由数学归纳法得,命题成立。

性质

根据实现方式的不同,有不同的时间复杂度。

优先队列:\(O\left(m\log m\right)\)

\(Code:\)
struct node{
    int num,dist;
    bool operator <(const node &a)const{return dist>a.dist;}
    node(int a,int b){
        num=a,dist=b;
    }
};
priority_queue<node>q;
void Dijkstra(int s){
    memset(vis,false,sizeof(vis));
    memset(dis,0x7f,sizeof(dis));
   	dis[s]=0;
    q.push(node(s,0));
    while(!q.empty()){
        int point=q.top().num;
        q.pop();
        if(vis[point])continue;
        vis[point]=true;
        for(int i=head[point];i;i=nxt[i]){
            int son=to[i];
            if(dis[son]>dis[point]+1){
                dis[son]=dis[point]+1;
                q.push(node(son,dis[son]));
            }
       	}
    }
}

全源最短路

即任意两点之间的最短路。

\(Floyed\)

原理

\(Floyed\)本质上是\(DP\)思想。
\(f_{i,j,k}\)\(i\to j\)只经过\(1\cdots k\)的最短路,考虑转移:
\(f_{i,j,k}=\left\{\begin{matrix}val\left(i,j\right)\cdots\cdots\left[k=0\right]\\\min\left({f_{i,j,k-1},f_{i,k,k-1}+f_{k,j,k-1}}\right)\cdots\cdots\left[otherwise\right]\end{matrix}\right.\)
解释一下上面的式子,当\(k=0\)时,性质是显然的。
\(f_{i,j,k-1}\)是不经过\(k\)的最短路,而\(f_{i,k,k-1}+f_{k,j,k-1}\)是经过\(k\)点的最短路。两个数据取最小值,就是最短路。
而我们发现,\(f_{\forall,\forall,k}\)只会从\(f_{\forall,\forall,k-1}\)转移过来,显然可以把这一位压缩掉。

性质

无负环图。

时间复杂度\(O\left(n^{3}\right)\),空间复杂度\(O\left(n^{2}\right)\)

\(Code:\)
for (k = 1; k <= n; k++) {
  	for (x = 1; x <= n; x++) {
    	for (y = 1; y <= n; y++) {
      		f[x][y] = min(f[x][y], f[x][k] + f[k][y]);
    	}
  	}
}
posted @ 2022-12-05 22:19  |Roland|  阅读(87)  评论(0)    收藏  举报