最短路问题

引入

最短路问题(sp),即在图上求出两点之间总权值之和最小的路径。

最短路问题分为单源最短路(sssp)和多源最短路(mssp),前者只需要求出一个点到其余所有的最短路径权值和,而后者要求出图上任意两点的最短路径权值和。

最短路问题有以下四种常见算法:(设所给有向图中有 \(n\) 个点,\(m\) 条边)

算法 应用 时间复杂度 备注
Floyd mssp / sssp \(O(n^3)\)
Dijkstra / heap-Dijkstra sssp 二叉堆优化后 \(O((n+m)\log n)\) 可以使用堆优化,不能处理负环
Bellmen-Ford / spfa sssp \(O(nm)\) 能处理负环
会被卡掉
有人说spfa已经死了

本文中,所有“松弛”操作的定义为:如果 \(u\rightarrow k\rightarrow v\) 的路径比 \(u\rightarrow v\) 的路径短,那么将这两点间的距离更新为前者。

if (dis[u][v] > dis[u][k] + dis[k][v]) {
    dis[u][v] = dis[u][k] + dis[k][v];
}

多源最短路问题(mssp)

Floyd

Floyd 算法的本质是dp。

\(f[k][u][v]\) 表示经过点 \(k\) 松弛后 \(u\rightarrow v\) 的最短路径权值和。

刚开始:

\[f[0][u][v]=\begin{cases}w_{u,v} & (u、v之间有边) \\ +\infty & (u、v之间没有边)\end{cases} \]

求最短路的过程就是不断松弛的过程,其状态转移方程为:

\[f[k][u][v] = min\{\ f[k-1][u][k]+f[k-1][k][v]\ \} \]

Floyd 需要三层循环,分别枚举中转点、起点、终点,这三层循环的顺序是 Floyd 算法唯一需要注意的地方。

如图所示,我们考虑把枚举起点放在最外层循环,第一个枚举到的是 \(u\),此时我的最优选择是通过 \(k\) 中转,最短路为 \(u\rightarrow k\rightarrow v\),由于 \(k\) 点出发的最短路尚未更新,此时 \(u\rightarrow v\) 的最短路权值为 \(101\)。而之后从 \(u\) 出发的最短路将不再有机会更新,所以权值为 \(3\) 的最短路 \(u\rightarrow k\rightarrow x\rightarrow v\) 并没有被找到。

枚举终点同理会有问题,因此,枚举中转点 \(k\) 应该放在最外层循环。

最后,可以使用滚动数组优化空间。时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2)\)

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

Floyd 的应用:传递闭包

洛谷 B3611 【模板】传递闭包

判断有向图上任意两点是否连通的问题。由于是有向图,无法使用并查集,只能用 Floyd。

如果两点间最短路径不是 \(+\infty\),那么这两点就可以互相到达。

当然,实现的时候 f[][] 中只需要存一个 bool 即可。

for (int k = 1; k <= n; ++k) {
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++i) {
            f[i][j] |= f[i][k] & f[k][j];
        }
    }
}

单源最短路问题(sssp)

洛谷 P4779 【模板】单源最短路径(标准版)

Dijkstra / heap-Dijkstra

一句话:求从 \(s\) 出发到其余各点的最短路,每次从尚未选过的节点中,选取权值和最小的路径 \(s\rightarrow u\),用 \(u\) 松弛其所有的子节点 \(v\)

由于每次选的是全局最小,在没有负权边的情况下,以 \(u\) 作为中转点对 \(s\rightarrow v\) 进行一次松弛,路径权值和肯定是在 \(s\rightarrow u\) 的基础上加上一个正权值 \(w_{u,v}\),因此每次选择的点 \(x\),路径 \(s\rightarrow x\) 的权值和总是递增的。

因此以 \(u\) 作为中转点的时候,\(s\rightarrow u\) 的路径必然是权值和最小的那一条,保证了正确性。同时,再使用 \(s\rightarrow u\) 作为中转点进行松弛也不会有效果,所以每个点只需要做一次中转点就可以了。

用优先队列(已经实现好的堆)维护全局权值和最小的路径,同时标记已经做过中转点的点,\(m\) 次入堆,\(n\) 次出堆,时间复杂度 \(O((n+m)\log n)\)

void heap_dijkstra()
{
	priority_queue< pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > >q;
	for (int i = 1; i <=n; ++i) {
		dis[i] = inf;
	}
	dis[s]=0;
	q.push(make_pair(0, s));
	while (!q.empty()) {
		int u=q.top().second; q.pop();
		if (vis[u]) continue;
		vis[u]=true;
		for (int i = head[u]; i; i=e[i].nxt) {
			int v = e[i].to;
			if (dis[v] > dis[u]+e[i].val) {
				dis[v] = dis[u]+e[i].val;
				if (!vis[v]) q.push(make_pair(dis[v], v)); 
			}
		}
	}
	for (int i = 1; i <= n; ++i) {
		cout << dis[i] << " ";
	}
}

Bellman-ford / spfa

一句话:求从 \(s\) 出发到其余各点的最短路,每次选一个点 \(u\),对到其子节点 \(v\) 的路径 \(s\rightarrow v\) 进行松弛。spfa 是 Bellman-ford 算法的队列实现版本。

如果 \(s\rightarrow v\) 的路径经过 \(u\) 中转后边权变小,那么对于 \(v\) 的子节点 \(x\),路径 \(s \rightarrow x\) 也可能会松弛成功,所以每次成功松弛一个节点后,将其所有子节点加入队列(已经在队列中就不用重复加了)。

void spfa()
{
	queue<int> q;
	for (int i = 1; i <= n; ++i) {
		dis[i] = inf;
		vis[i] = 0;
	}
	q.push(s); dis[s]=0; vis[s]=true;
	while (!q.empty()) {
		int u=q.front(); q.pop(); vis[u]=0;
		for (int i = head[u]; i; i=e[i].nxt) {
			int v = e[i].to;
			if (dis[v] > dis[u]+e[i].val) {
				dis[v] = dis[u]+e[i].val;
				if (!vis[v]) {
					vis[v] = true;
					q.push(v);
				}
			}
		}
	}
	for (int i = 1; i <= n; ++i) {
		cout << dis[i] << " ";
	}
} 

spfa 的应用:判断负环

洛谷 P3385 【模板】负环

由于负环的存在,边权会越绕越小,因此图中不存在最短路,Dijkstra 会在这里被卡成 tle。(tle 判负环法)

记录 \(s\rightarrow u\) 的路径长度为 \(viscnt[u]\),每次节点 \(u\) 的子节点 \(v\) 经过 \(u\) 松弛成功后,\(viscnt[v] = viscnt[u] + 1\)

最后,一旦 \(viscnt[i]\) 超过图上点的个数,那么一定是在某处绕过圈了,那就说明图上存在负环。


bool spfa()
{
	// 清空
	while (!q.empty()) { q.pop(); }
	memset(dis, 0x3f3f3f3f, sizeof(dis));
	memset(viscnt, 0, sizeof(viscnt));
	memset(vis, 0, sizeof(vis));

	//
	dis[1]=0; vis[1]=1; q.push(1);
	while (!q.empty()) {
		int u=q.front(); vis[u]=false; q.pop();
		for (int i = head[u]; i; i=e[i].nxt) {
			int v=e[i].v, w=e[i].w;
			if (dis[v] > dis[u]+w) {
				dis[v] = dis[u]+w;
				viscnt[v] = viscnt[u] + 1;
				if (viscnt[v] >= n) { return true; }
				if (!vis[v]) { vis[v]=true; q.push(v); }
			}
		}
	}
	return false;
}
posted @ 2023-07-31 12:19  LittleDrinks  阅读(17)  评论(0编辑  收藏  举报