最短路

概述

  • 最短路算法主要研究图上两点之间的最短路径问题。

  • 事实上,许多问题都能转化为图来求解(图的本质就是点和边的集合,点是元素,边是元素之间的二元关系)。

  • 所以最短路算法并不局限于“给出一张图,...”。

Floyd

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

概述

  • Floyd 算法是一种全源最短路算法。

  • Floyd 算法可处理负权,无法处理负环。

    • 前一条的证明见 Bellman-Ford。

    • 后一条的理由如下:

      • 路径可以过负环的两点之间的最短路为无穷小,正常的任何算法都显然算不出(根本存不了这种东西好吧)。

      • 实际效果的话,对于从 \(i\) 出发不能经过负环到 \(j\)\(i,j\),和无负环情况一样;

      • 对于能经过负环的,虽然负环大小有限,其上每个点至多作为一次“当前考虑到的最大可行中继点”被放进来,但其他非负环上点做中继的时候,它的 \(dis[i][k]+dis[k][j]\) 中可能就有 \(1/2\) 项本身过负环,所以最终的 \(dis\) 会为 \(-inf\)

  • Floyd 算法的实现基于邻接矩阵。

原理

  • 本质上是 dp,上面的代码实质上压掉了第一维。

  • 完整的 dp 如下:

    • 状态设计:\(dis_{k,i,j}\) 代表只考虑用编号小于等于 \(k\) 的点作为中继(注意,与路径端点无关!)的情况下,从 \(i\)\(j\) 的最短路距离。

    • 初始化:\(dis_{0,i,j}=e_{i\to j}\)。如果不存在对应边,那么 \(dis_{0,i,j}=+\infty\),当然赋成 \(-1\) 然后特判也行。

    • 状态转移方程:\(dis_{k,i,j}=\min(dis_{k-1,i,j},dis_{k-1,i,k}+dis_{k-1,k,j})\)

    • 第二项符号是 + 还是 min 或者别的什么,取决于题目对路径长度的定义。

证明

  • 前面一种是不经过第 \(k\) 个点中继,那么显然可以保留结果。

  • 后一种是经过第 \(k\) 个点中继。显而易见,在无负权环的图中任意一条从 \(k\) 出发/到 \(k\) 截止的最短路不可能以 \(k\) 为中继,所以使用上一次的状态来转移是合法的。

  • \(O(n^3)\),空间 \(O(n^2)\)

  • 压维而非滚维的正确性:本轮转移中肯定用到的都是 \(dis_{i,k},dis_{k,j}\) 之类含 \(k\) 的,但由上,从 \(k\) 出发/到 \(k\) 截止的最短路不经过 \(k\),故每轮转移的出发点在该轮一定不被更新。

其他应用

  • Floyd 还有一个妙用:求长度最小的简单环(关于简单环,请参考图论-定义)。

  • 考虑简单环的环长。把环内最大点记为 \(k+1\),且 \(\exists e_{i\to k+1},e_{k+1\to j},i,j\in cir\),显然该环的最小长度为 \(dis_{k,i,j}+e_{i\to k+1}+e_{k+1\to j}\)

  • 则我们可以对于每个 \(k\),在进行 Floyd 前 for(i,j) 基于上式求最小环。可以得到最小简单环环长。

  • 又因为找到的环一定包含 \(i,j,k+1\) 三个点,记录每个 \(dis\) 由谁更新后利用回溯也可以找到环上点与边。

  • 为什么后两个用的是 \(e\) 而非 \(dis\)

    • 负权图中,\(dis\) 可能会绕远路绕到 \(i,j\) 之间的点上再绕回来(这样走经过的路径长度为负数),于是这个环就不是一个简单环,甚至于它会反复计算负权边的权值。

    • 而环长度的定义是 \(\sum\limits_{e\in cir} e\),即环上每条边的权值之和。

Dijkstra

const int inf=0x3f3f3f3f;
int dis[maxn];
bitset<maxn> vis;
struct point{
	int id,dis;
	point(){}
	point(int _id,int _dis){id=_id,dis=_dis;}
	il bool operator<(const point &b)const{return dis>b.dis;}
};
priority_queue<point,vector<point> >q;
il void dijk(int s){
	For(i,1,n) dis[i]=inf; dis[s]=0; q.push(point(s,0));
	ri point now;
	while(!q.empty()){
		now=q.top(),q.pop();
		if(vis[now.id]) continue;
		vis[now.id]=1;
		for(ri edge a:e[now.id])
			if(dis[a.to]>now.dis+a.l){
				dis[a.to]=now.dis+a.l;
				q.push(point(a.to,dis[a.to]));
			}
	}
	return;
}

概述

  • Dijkstra 算法是一种单源最短路算法。

  • Dijkstra 无法处理负权边。

  • 一切单源最短路,一切,取出最短路中用到的边,这些边一定是一棵树,称为最短路树。

原理

  • 我们下面把“最短路长度尚未确定的点”称为未出队点,确定的称为已出队点(请注意,未出队不代表就在那个 priority_queue 内)。

  • 1.每次在最短路长度尚未确定的点中,选取到源点 \(s\) 最近的 \(u\)

  • 在无负权图中,可以证明,此时的 \(dis_u\) 一定就是实际的 \(s\to u\) 的最短路长度:

    • \(\forall dis_v<dis_u\)\(v\),它们都已经出队,且已尝试过更新 \(dis_u\)

    • \(\forall dis_v\geqslant dis_u\)\(v\),既然是无负权图,则一定有 \(dis_v+e_{v\to u}\geqslant dis_u\),没有更新的必要。

    • 故此时的 \(dis_u\) 一定就是实际的最短路长度。

  • 2.从 \(u\) 出发,尝试更新所有未出队点的最短路,即 \(\forall v,dis_v=\min(dis_v,dis_u+e_{u\to v})\)

    • 通常使用邻接表实现,故没有边就不更新。

    • 注意,我们虽然把 \(s\) 之外的点的 \(dis\) 都初始化为 \(inf\) ,但它不能真的取一个接近类型上限的值(譬如说 \(2139062143\)\(2^{31}-1\) 对于 int),因为这样一加会炸类型上限。

  • 3.将 \(u\) 出队。如果队列不空,则继续找最近的点,重复上述操作。

实现

  • 朴素实现

    • 就枚举呗,跟 Prim 真的非常像(上面贴的代码是优化版的)。

    • 每次找点 \(O(n)\),找 \(n\) 次,每条边会做一次更改 \(dis\) 的尝试。

    • 故总复杂度 \(O(n^2+m)\)

  • 优化实现

    • 使用各种能 \(O(\log)\) 取出最近点的数据结构。

    • 最常用的是 priority_queue,因为好写。

      • 需要注意的是我们无法更改优先队列中的元素的值(可能会有人说 mutable 关键字但我不会)。

      • 故我们不得不把 \(dis\) 放在结构体里才能让优先队列正常运行,从而我们只能每次把 \(id\)\(dis\) 打包丢进优先队列。

      • 从而会有大量无用元素积压,所以它的表现很差,极限情况下每条边都会导致一次入队(和后面的出队),\(O((n+m)\log m)\)

      • 需要额外考虑一个问题:一个点是否可能会入队多次。

        • 在它的 \(dis\) 已经彻底敲定,用它来更新别人之后,它肯定不会入队。

        • 但在那之前它可能会被更新很多次,\(dis\) 最小的那次会首先被取出,后面的都没用。

        • 所以我们应当加一个 \(vis\) 数组,一旦它被取出了一次,就 vis=1,后面再被取出直接 continue

    • 显然可以用各种堆(二叉堆/配对堆/左偏树)加上记录元素在堆中的编号来做 decrease-key,从而复杂度降至 \(O((n+m)\log n)\)。类似地,线段树也可以。

    • 有一个很神的东西叫做 Fibo 堆:

      • 这个东西只有 pop 是 \(O(\log)\),其他操作都是 \(O(1)\)

      • 故用它优化 Dijk 的理论复杂度为 \(O(m+n\log n)\)

      • 但事实上,它太难写。并且,它的复杂度是均摊的,在实际使用中表现并没有那么优秀(甚至会输给配对堆,见 OI Wiki Dijk 处的链接),故不常用。

    • 注意到除了 Fibo 堆之外,其他的在稠密图中都是负优化。所以请根据 \(m\)\(n\) 的相对大小谨慎选择。

Bellman-Ford 与 SPFA

int dis[maxn],q[maxn<<4],hd=1,tl;
bool vis[maxn];
void spfa(int s){
	For(i,1,n) dis[i]=inf; dis[s]=0; q[++tl]=s; vis[s]=1;
	while(tl>=hd){
		vis[q[hd]]=0; int now=q[hd++];
		for(bian a:e[now])
			if(dis[a.to]>dis[now]+a.l){
				dis[a.to]=dis[now]+a.l;
				if(!vis[a.to]) vis[a.to]=1,q[++tl]=a.to;
			}
	}
	return;
}

概述

  • Bellman-Ford 算法是一种单源最短路算法。

  • Bellman-Ford 算法可处理负权边,无法处理负环。

  • 狭义的 SPFA 是队列优化的 Bellman-Ford。

原理

  • Bellman-Ford:dp。

    • 状态设计:\(dis_{k,i}\) 为从 \(s\)\(i\) 只走不超过 \(k\) 步的最短路。

    • 初始化:\(dp_{0,s}=0\)

    • 状态转移方程:\(dis_{k,i}=\min(dis_{k-1,i},\min_j(dis_{k-1,j}+e_{j,i}))\)

  • \(O(n^2)\)

  • 观察上面的转移式,可以看到 \(k\) 的转移实质上是可以滚维/压维的。

  • 如果我们压它的维,转移中会有环。怎么办?

  • 考虑使用带环 dp 的方式来处理它。

  • SPFA

    • 每次某个点的最短路被修改了就把它扔进队列,准备从它出发更新别的点的最短路。

    • \(vis\) 记录一下是否已经在队列里,不要重复入队。

    • \(\Omega(m)\sim O(nm)\)。一般认为随机数据下是 \(O(m)\) 的,构造数据下是 \(O(nm)\) 的。

    • 有一系列优化,譬如 \(dis\) 小于队头就放到队头,\(dis\) 大于平均扔到队尾(有点模拟优先队列的味道),在随机数据下很有效。

    • 然而,训练有素的出题人还是可以把你卡到 \(O(nm)\)。事实上,这些优化可以被卡到指数复杂度。

    • 所以如果被卡了就去用 Dijk 吧...大型比赛一定、一定、一定不要用 SPFA!!!(除非打暴力明知 Dijk 过不了,赌一手 SPFA 能过)

其他应用:找负环

  • 路径可以过负环的两个点肯定没有最短路,但是 SPFA 可以判断(并找到)负环。
    • 任意一个点最多被每个点都更新一次,毕竟这个 dp 的本质是走谁作中转。

    • 所以如果一个点的入队次数 \(\geqslant n\),一定有负环!

    • 记录更新来源,可以不断倒序回溯,找到负环。

    • \(O(nm)\)(毕竟都入队n次了)。

    • 可以用 \(num\) 数组来优化。假如 \(i\) 更新了 \(j\)\(num[j]=num[i]+1\)\(O()\) ...这个优化的正确性毋庸置疑,但也有可能被卡成无优化。

    • 可以看到这个 \(num\) 实际代表的是最短路经过的点的个数,它 \(>n\) 则一定有负环了(一条最短路不可能含超过 \(>n\) 个点,即不能有超过 \(n-2\) 个中转点,除非有一个点过了两次)。\(num[s]=1\)

    • 需要额外附注一下的是,常规的 SPFA 会自动地清空 \(vis\),但找到负环就跳出时,记得手动清理。

差分约束算法

概述

  • 差分约束算法的一般形式如下:

  • 给定一组多元不等式(也可能含一些等式),试求解:

    • 某两个元的最大差/最小差。

    • 一组符合条件的解。

最小差问题

  • \(x_1,x_2,...\) 抽象为一组图论意义下的点,将不等式抽象为它们之间的边。

  • 考虑一个最短路问题的基本形式:\(dis_i\leqslant dis_j+e_{j\to i}\)

  • 如果我们把所有的不等式都化成类似的小于情况(\(x_i-x_j\leqslant k\)),那么就可以把它化归到上面的最短路模型里:\(e_{j\to i}=k\)

  • 实际意义:\(x_i\) 最多比 \(x_j\)\(k\)。从而求最小差的意义就是多条“\(x_i\)\(x_j\) 最多大多少”路径的最短路。

最大差问题

  • 同上。全部转化为大于情况,那就是多个“最少大多少”中的最长路。

求符合条件的解(或者判断是否有解)。

  • 做一个超级源点,剩下自己整。

  • 有负环就是无解。

  • 证明:将对应不等式左右相加(按 \(x_i-x_j\leqslant k\) 的形式),显然得到 \(0\leqslant -k,k> 0\)

例题

  • \(\text{P5960 [模板] 差分约束算法}\)

  • \(\text{P2294 [HNOI2005] 狡猾的商人}\)

posted @ 2023-02-28 13:44  未欣  阅读(29)  评论(0编辑  收藏  举报