最短路

最短路

如下图(引自知乎):

求两点之间的最短路径。

最短路有两种:单源和全源。

常用的有四种最短路算法:

  • Floyd(全)
  • SPFA(单)
  • Dijkstra(单)
  • Johnson(全)

还有两种最短路的应用:

  • 最短路计数
  • 最短路径

蒟蒻的最短路题单

Floyd(多源最短路)

Floyd 其实是一种 DP 思想。

核心只有四行代码:

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

v 数组的初始化也非常简单:

\(v_{i,i}=0,v_{i,j}=+\infty\)

再把输入的 i,j 更新就 OK 了

时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2)\)

需要注意的是一般有人写循环就会直接 for_i_j_k 依次来,但是 Flody 中 k 必须在第一维,否则会 WA

比如我们来看一个例子:

\(i=1,j=2,k=3\) 时,此时 \(dis_{1,3}=+\infty\),无法更新到 \(dis_{1,2}\)

\(k=4\) 的时候,将 \(dis_{1,2}\) 更新为 \(dis_{1,4}+dis_{4,3}=4\)

然后在 \(i=1,j=3,k=4\) 时,更新 \(dis_{1,3}=2\),但无法在将 \(dis_{1,2}\) 更新为 \(dis_{1,3}+dis_{3,2}=3\)。所以在最终结果中, \(dis_{1,2}=4\)

具体原因就是其思想:DP

\(f_{k,i,j}\) 表示经过前 k 的点 i 和 j 的最短路

DP 方程就是 \(f_{k,i,j}=min(f_{k-1,i,j},f_{k-1,i,k}+f_{k-1,k,j})\)

然后滚动数组优化一下,就是 \(f_{i,j}=min(f_{i,j},f_{i,k}+f_{k,j})\)

Floyd 适用于点数较少的图上,可以用来判负环:在经过 Floyd 核心代码处理后的图上再跑一遍 Floyd ,如果有权值被更新,则存在负环。

也可以用来找最小环。

例题

Dijkstra(单源最短路)

Flody 是 DP 的思想,而 Dij 是贪心的思想。

限制条件:无负权。

Dij 的思路主要是:找到每次更新后 dis 值最小的点,用这个最小的点再去维护它所指向的点。

由于没有负边权,所以每次选择最小的点更新的路径一定是最短路。

因为要对图中的 n 的点都进行更新,再加上每次找最小的点又要遍历一次,所有的边还要遍历一遍,所以朴素的 Dij 算法时间复杂度为 \(O(n^2)\)

dis[s]=0;
while(!vis[s])
{
	int minn=2147483647;
	vis[s]=1;
	for(int i=fir[s];i;i=nex[i])
	{
		int p=poi[i];
		if(!vis[p]&&dis[p]>dis[s]+val[i])
			dis[p]=dis[s]+val[i];
	}
	for(int i=1;i<=n;i++)
		if(!vis[i]&&dis[i]<minn)
			minn=dis[i],s=i;
}

在多数情况下我们要对朴素 dij 进行优化。

遍历 n 个点事无法改变的,那么有没有一种办法更快找到最小权值的点?

堆优化

利用二叉小根堆,将点和它的 dis 值存入堆中,每次用 \(O(\log n)\) 的时间取出并删除堆顶,然后遍历其发出的边,更新相邻点的 dis 值。

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

但是 priority_queue 不支持随机删除,所以时间复杂度 \(O(m\log m)\)

typedef pair<int,int> Pr;
priority_queue<Pr,vector<Pr>,greater<Pr> >h;
memset(dis,127,sizeof(dis));
dis[s]=0;h.push(Pr(0,s));
while(h.size())
{
	int now=h.top().second;h.pop();
	if(vis[now])continue;
	vis[now]=1;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(dis[p]>dis[now]+val[i])
		{
			dis[p]=dis[now]+val[i];
			h.push(Pr(dis[p],p));
		}
	}
}

这里的堆用优先队列来实现,有 STL 就没必要再手打堆。

如果不想用 pair,也可以用 struct 的 operator

struct node{
	int id,val;
	bool operator <(const node &b)const
	{
		return val>b.val;
	}
};
priority_queue<node>h;
while(h.size())
{
	int now=h.top().id;h.pop();
	if(vis[now])continue;
	vis[now]=1;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(dis[p]>dis[now]+val[i])
		{
			dis[p]=dis[now]+val[i];
			h.push((node){p,dis[p]});
		}
	}
}

线段树优化

和堆优差不多,通过线段树找到最小点权及其位置

走过的位置就设置成极大值

线段树上就维护单点修改和全局最小值查询

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

struct Seg_tree{
	int le,ri;
	int minn,id;
}tre[inf<<2];
void pushup(int i)
{
	if(tre[i<<1].minn>tre[i<<1|1].minn)
		tre[i].minn=tre[i<<1|1].minn,tre[i].id=tre[i<<1|1].id;
	else tre[i].minn=tre[i<<1].minn,tre[i].id=tre[i<<1].id;
}
void build(int i,int l,int r)
{
	tre[i].le=l;tre[i].ri=r;
	if(l==r)
	{
		tre[i].minn=2139062143;
		tre[i].id=l;
		return;
	}
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	pushup(i);
}
void change(int i,int id,int k)
{
	if(tre[i].le==tre[i].ri)
	{
		tre[i].minn=k;
		return;
	}
	int mid=(tre[i].le+tre[i].ri)>>1;
	if(id<=mid)change(i<<1,id,k);
	else change(i<<1|1,id,k);
	pushup(i);
}
void dij(int s)
{
	memset(dis,127,sizeof(dis));
	dis[s]=0;change(1,s,0);
	for(int i=1;i<n;i++)
	{
		int now=tre[1].id;
		change(1,now,2139062143);
		for(int i=fir[now];i;i=nex[i])
		{
			int p=poi[i];
			if(dis[p]>dis[now]+val[i])
			{
				dis[p]=dis[now]+val[i];
				change(1,p,dis[p]);
			}
		}
	}
}

桶优化

图片来自 2021 年 NOIp 夏令营中孙铭远讲师的图论课件

代码是自己打的

while(1)
{
	vis[now]=1;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(dis[p]>dis[now]+val[i])
		{
			dis[p]=dis[now]+val[i];
			T[dis[p]].push_back(p);
			maxn=max(maxn,dis[p]);
		}
	}
	last=now;
	for(int i=1;i<=maxn;i++)
	{
		if(T[i].size())
		{
			if(vis[T[i][0]]==0)now=T[i][0];
			last=i;T[i].erase(T[i].begin());
			break;
		}
	}
	if(last==now)break;
}

模板

SPFA(单源最短路)

SPFA 在国际上被称为 “队列优化的 Bellman-Ford ”算法 ,只是在中国被称为 "Shortest Path Faster Algorithm" ,简称 "SPFA" 。

而且

SPFA 的基本思路是:

先将源点入队,然后遍历源点发出的边,将其指向的点的 dis 值更新(如果能的话),然后源点出队。如果指向的点没有入队就将其入队。直到队列为空。

memset(dis,127,sizeof(dis));
dis[s]=0;h.push(s);
while(h.size())
{
	int now=h.front();h.pop();
	vis[now]=0;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(dis[now]+val[i]<dis[p])
		{
			dis[p]=dis[now]+val[i];
			if(vis[p])continue;
			vis[p]=1;h.push(p);
		}
	}
}

随机数据下,SPFA 的时间复杂度是 \(O(km)\)\(k\) 是一个常数,平均取 \(2\)。但在某些 hack 数据下,SPFA 会退化成 Bellman-Ford ,时间复杂度为 \(O(nm)\)

SPFA 有很多优化,如:SLF 和 LLL 等,但都容易被卡。

如何让 SPFA SPFA。

例题

但是 SPFA 也有自己的用处:

判负环

众所周知,判负环有两种方法:

  1. 入队次数

    原理:显然,对于一个没有负环的图,一个点最多松弛 \(n-1\) 轮。当一个点的入队次数超过 \(n\) 时,就存在负环。

    做法:我们记录一个点的入队次数,当大于点数 \(n\) 时,判定存在负环。

bool spfa()
{
	dis[1]=0;h.push(1);
	while(h.size())
	{
		int now=h.front();h.pop();
		vis[now]=0;
		for(int i=fir[now];i;i=nex[i])
		{
			int p=poi[i];
			if(dis[p]>dis[now]+val[i])
			{
				dis[p]=dis[now]+val[i];
				num[p]++;
				if(num[p]>n)return 1;
				if(vis[p])continue;
				h.push(p);vis[p]=1;
			}
		}
	}
	return 0;
}
  1. 最短路长度

    原理:一个没有负环的图的最短路上的边数不超过 \(n\)

    做法:记录到当前点的最短路径的边数,当大于点数 \(n\) 时,判定存在负环。

bool SPFA()
{
	dis[1]=0;h.push(1);
	while(h.size())
	{
		int now=h.front();h.pop();
		vis[now]=0;
		for(int i=fir[now];i;i=nex[i])
		{
			int p=poi[i];
			if(dis[p]>dis[now]+val[i])
			{
				dis[p]=dis[now]+val[i];
				len[p]=len[now]+1;
				if(len[p]>=n)return 1;
				h.push(p),vis[p]=1;
			}
		}
	}
	return 0;
}

显然,第二种方法的时间复杂度更优。

差分约束系统

虽然洛谷有【模板】标签的不是它,但这个非官方模板比官方模板更有名气。

此算法与 差分 并无关联。

差分约束的形式 m 组 n 元一次不等式。

那么,不等式和最短路有什么关系呢?

回想最短路,我们做完所有松弛操作之后,对于每条边和其连接的两个点 \(u,v\),都有如下关系:

\[dis_v\le dis_u+val_{u,v} \]

因为松弛操作就是将 \(dis_v>dis_u+val_{u,v}\)\(v\) 点的点权更新。

再看题目中给的不等式:

  1. \(a-b\ge c\)
  2. \(a-b\le c\)
  3. \(a=b\)

整理一下,可以得到以下四个不等式:

  1. \(b\le a-c\)
  2. \(a\le b+c\)
  3. \(a\le b+0\)
  4. \(b\le a+0\)

结合最短路角度中的不等关系,就可以建图:

ins(b,a,-c);
ins(a,b,c);
ins(a,b,0);
ins(b,a,0);

为了防止图不连通导致的错误判断,差分约束都会建一个虚拟源点“0 点”,将所有的点都与 0 点相连,边权为 0,这样整个图就一定联通了。

即:

for(int i=1;i<=n;i++)
	ins(0,i,0);

而这样为什么是对的呢?

这个操作就相当于加了一堆这样的不等式:

\(0\le a+0\)

显然,原本就成立的不等式加入到不等式组中对答案没有影响。

最后,在图上跑一遍 SPFA,当图中存在负环,则不等式组无解,否则每个点的 dis 值即为可能解。

对于差分约束而言,重点不在其本身如何解决,而在于这种图论建模的思想。

最小费用最大流

即 EK 算法。

Johnson(多源最短路)

简单来讲,Johnson 就是跑了 n 遍 Dij。

那么,时间复杂度就为 \(O(nm\log m)\)

模板

之前说过, Dij 不能处理含负边权的图的最短路问题。

题目描述:

  1. 边权可能为负,……

emm……

让我们观赏一下大型纪录片《SPFA的复活之路》

再往下看题目描述:

  1. 部分数据卡 n 轮 SPFA 算法

。。。换题

仔细想想,我们可以像差分约束一样,设置一个超级源点 0 点,而且 0 点到每个点的边权值也为 0。

先用一轮 SPFA 求出 0 点所有节点的最短路 \(h_i\)(顺便判一手负环)。之前的边权值 \(val_{u,v}\) 就可以更新为 \(val_{u,v}+h_u-h_v\)

因为对于最短路上,有 \(val_{u,v}+h_u\ge h_v\),故 \(val_{u,v}+h_u-h_v\ge 0\),故无负权值存在,也就可以跑 n 轮堆优 Dij 去求解了。

这一步叫 re_weight

void re_weight()
{
	for(int i=1;i<=n;i++)
		for(int j=fir[i];j;j=nex[j])
			val[j]+=dis1[i]-dis1[poi[j]];
}

最后,再减去起点 \(h_s\) 并加上终点 \(h_t\),就是两个点之间的最短路了。

为什么呢?

设求出的一条最短路径为 \(s\rightarrow n_1\rightarrow n_2\cdots n_k\rightarrow t\)

那么重赋值之后的最短路长度为

\[(val_{s,n_1}+h_s-h_{n_1})+(val_{n_1,n_2}+h_{n_1}-h_{n_2})+\cdots \]

\[+(val_{n_{k-1},n_k}+h_{n_{k-1}}-h_{n_k})+(val_{n_k,t}+h_{n_k}-h_t) \]

拆括号,化简得:

\[val_{s,n_1}+val_{n_1,n_2}+\cdots+val_{n_{k-1},n_k}+val_{n_k,t}+h_s-h_t \]

于是将得到的结果 \(-h_s+h_t\) 便是两点之间的最短路。

同时,也证明 Johnson 算法的正确性。

最短路计数

用 SPFA 或者堆优 Dij 都是可以的。

只需要加上递推式:

if(dis[p]==dis[now]+val[i])
	num[p]+=num[now];

num 就表示最短路的条数。

整个就是这样:

memset(dis,127,sizeof(dis));
dis[1]=0;num[1]=1;
h.push(pr(dis[1],1));
while(h.size())
{
	pr data=h.top();h.pop();
	int now=data.second;
	if(vis[now])continue;
	vis[now]=1;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(dis[now]+val[i]==dis[p])
			num[p]+=num[now];
		if(dis[now]+val[i]<dis[p])
		{
			dis[p]=dis[now]+val[i];
			num[p]=num[now];
			h.push(pr(dis[p],p));
		}
	}
}

例题 ,记得取模。

最短路径

同样是可以用 SFPA 和堆优 Dij,在求最短路的时候记录下经过的点中 dis 最小的的前驱,最后再从终点反向搜回去。

像这样:

memset(dis,127,sizeof(dis));
dis[1]=0,vis[1]=1;
h.push(1);
while(h.size())
{
	int now=h.front();h.pop();
	vis[now]=0;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(dis[p]>dis[now]+val[i])
		{
			dis[p]=dis[now]+val[i];
			pre[p]=now;
			if(vis[p])continue;
			h.push(p),vis[p]=1;
		}
	}
}
int now=n;
while(now!=1)
{
    ans_.push_back(now);
    now=pre[now];
}
ans_.push_back(1);

ans_ 就是反向存着的从 1 到 n 的最短路径。

例题

次短路与 k 短路

posted @ 2022-02-09 14:59  Zvelig1205  阅读(551)  评论(0编辑  收藏  举报