单源次短路算法 学习笔记

次短路:顾名思义就是一张图中第二短的路径。
分类:1. 边不可重复经过的次短路问题。边可重复经过的次短路问题。
2. 严格次短路(次短路长度必须大于最短路长度)。非严格次短路(次短路长度可以大于或等于最短路长度)。

一 、边不可重复经过的次短路问题

例题:LuoguP1491 集合位置

题目分析

此题要求的是一张图的非严格次短路。且每条边不可重复经过。

思路

采用删边法。
我们先对原先的图跑一边最短路,记录最短路的路径。
之后每次删去最短路的一条边,重新跑最短路,在这几次跑出来的结果中,取最小值,就是最终答案。

正确性证明:
显然,次短路和最短路的路径必然不是相同的。
因为最短路的路径肯定是整张图中最优的,我们每次去掉最短路上的一条路径,剩下的路径必然不会比最短路的路径更优。
在删掉一条边后的剩下的路径中取得最短路径,且肯定不会比原先图的最短路径更优,那么这一条路就必然是次短路径。

做法:
对于这道例题而言,我们先记录每个点的坐标位置,在连边时用两点间距离公式算出距离,连双向边,记得开 double
在 dijkstra 函数里传入两个参数,也就是我们要删除的边所连接的两个顶点,第一次跑最短路时传入 1 1,跑最短路是特判,如果是 1 1,那么我们每次记录上当前顶点最短路的前驱,我们可以定义数组 prevv,则 prevvi 表示编号为 i 的顶点在最短路中的前驱顶点的编号。如果传进来的参数不为 1 1,则判定当前将要松弛的边所连接的两个顶点是否为传进来的参的顶点编号,如果是的话则直接 continue,相当于把这条边给删掉了(实质上是把这条边忽视掉,和删边是一样的效果)。

剩下就是照常跑最短路的操作了。

在主函数里,我们遍历 prevv 数组中所有的边,传入 dijkstra 中,每次取 distn 的最小值,这样最终的最小值就是我们的答案。

Q:如果这道题求的是严格次短路应该怎么办?
A:在第一次求解最短路时,将最短路记录下来。之后在遍历 prevv 数组求删边后的最短路时,若求出来和第一次求解最短路的路径长度相同,则忽视掉当前结果。

代码

每次遍历 prevv 数组中所有的边,故最坏时间复杂度为 O(m(m+n)logn)
代码实现:

#include <bits/stdc++.h>
#define int long long
const int N = 2e4 + 5;
using namespace std;
inline int read(){
	int r = 0,w = 1;
	char c = getchar();
	while (c < '0' || c > '9'){
		if (c == '-'){
			w = -1;
		}
		c = getchar();
	}
	while (c >= '0' && c <= '9'){
		r = (r << 3) + (r << 1) + (c ^ 48);
		c = getchar();
	}
	return r * w;
}
int head[N],cnt = 0;
struct edge{
	int nxt,to;
	double w;
}e[N << 1];
void edge_add(int x,int y,double z){
	cnt++;
	e[cnt].nxt = head[x];
	head[x] = cnt;
	e[cnt].to = y;
	e[cnt].w = z;
}
int n,m,prevv[N];
namespace dij{
	struct point{
		int id;
		double dis;
		friend bool operator < (point a,point b){
			return a.dis > b.dis;
		}
	};
	priority_queue<point> q;
	double dist[N];
	bool vis[N];
	void dijkstra(int dx,int dy){
		for (int i = 1;i <= n;i++){
			dist[i] = DBL_MAX;
		}
		memset(vis,0,sizeof(vis));
		dist[1] = 0;
		q.push((point){1,0});
		while (!q.empty()){
			int tmp = q.top().id;
			q.pop();
			if (!vis[tmp]){
				vis[tmp] = 1;
				for (int i = head[tmp];i;i = e[i].nxt){
					int v = e[i].to;
					double w = e[i].w;
					if (tmp == dx && v == dy || tmp == dy && v == dx){
						continue;
					}
					if (dist[v] > dist[tmp] + w){
						dist[v] = dist[tmp] + w;
						q.push((point){v,dist[tmp] + w});
						if (dx == -1 && dy == -1){
							prevv[v] = tmp;
						} 
					}
				} 
			}
		}
	}
}
using namespace dij;
signed main(){
	n = read(),m = read();
	int x[N],y[N];
	for (int i = 1;i <= n;i++){
		x[i] = read(),y[i] = read();
	}
	for (int i = 1;i <= m;i++){
		int p = read(),q = read();
		double dis = sqrt((x[p] - x[q]) * (x[p] - x[q]) + (y[p] - y[q]) * (y[p] - y[q]));
		edge_add(p,q,dis);
		edge_add(q,p,dis);
	}
	dijkstra(-1,-1);
	double ans = DBL_MAX;
	for (int i = n;i != 1;i = prevv[i]){
		int dx = i,dy = prevv[i];
		dijkstra(dx,dy);
		ans = min(ans,dist[n]);
	}
	if (ans == DBL_MAX){
		printf("-1");
		return 0;
	}
	printf("%.2lf",ans);
	return 0;
} 

二、边可重复经过的次短路问题

例题:[USACO06NOV] Roadblocks G

题目分析

此题要求的是一张图的严格次短路。且每条边可重复经过。

思路

用两个 dist 数组,分别记录最短路和次短路。
当最短路可以更新时,那么次短路就继承之前的最短路的长度。
当次短路可以更新,且次短路更新后不会超过最短路长度,那么次短路更新。

注意:

  1. 若次短路更新了,那么需要将次短路的顶点编号和 dis 也放入优先队列 q 中。因为你还不能保证目前的次短路就是最优了,还需要进行松弛更新。
  2. 相对于原来的 dijkstra 板子,我们需要去掉 vis 数组的判断,因为vis 数组肯定对最短路没有影响的,它就是用来确定目前的最短路是否是最优的。但,当一个顶点的最短路是最优的,这个顶点的次短路不一定是最优的。
  3. 相对于原来的 dijkstra 板子,我们不仅需要提前将 优先队列 q 的第一个元素的 id 取出来,我们还需要提前将第一个元素的 dis 成员取出来,因为在原来的板子里,优先队列中第一个元素的 dis 就等于 disttmptmp 为优先队列中第一个元素的 id)。但,在这里我们有两个 dist 数组,当前使用哪一个 dist 数组取决于当前优先队列里第一个元素的 dis 成员。

代码

与原版 dijkstra 复杂度相同。O((m+n)logn)
PS:若这道题要求非严格次短路,只需将当次短路可以更新时的后半部分条件稍作修改即可(dist[v][1]<dis_now+w 改成 dist[v][1]<=dis_now+w)。
代码实现:

#include <bits/stdc++.h>
#define int long long
const int N = 1e5 + 5;
using namespace std;
inline int read(){
	int r = 0,w = 1;
	char c = getchar();
	while (c < '0' || c > '9'){
		if (c == '-'){
			w = -1;
		}
		c = getchar();
	} 
	while (c >= '0' && c <= '9'){
		r = (r << 3) + (r << 1) + (c ^ 48);
		c = getchar();
	}
	return r * w;
}
int n,m,s;
struct edge{
	int nxt,to,w;
}e[N << 1];
int cnt = 0,head[N];
void edge_add(int x,int y,int z){
	cnt++;
	e[cnt].nxt = head[x];
	head[x] = cnt;
	e[cnt].to = y;
	e[cnt].w = z;
}
namespace dij{
	struct point{
		int id,dis;
		friend bool operator < (point a,point b){
			return a.dis > b.dis;
		}
	};
	priority_queue<point> q;
	int dist[N][5];
	void dijkstra(){
		memset(dist,0x3f,sizeof(dist));
		dist[1][1] = 0;
		q.push((point){1,0});
		while (!q.empty()){
			int tmp = q.top().id;
			int dis_now = q.top().dis;
			q.pop();
			for (int i = head[tmp];i;i = e[i].nxt){
				int v = e[i].to;
				int w = e[i].w;
				if (dist[v][1] > dis_now + w){
					dist[v][2] = dist[v][1];
					dist[v][1] = dis_now + w;
					q.push((point){v,dist[v][1]});
					q.push((point){v,dist[v][2]});
				}
				else if (dist[v][2] > dis_now + w && dist[v][1] < dis_now + w){
					dist[v][2] = dis_now + w;
					q.push((point){v,dist[v][2]});
				}
			}
		}
	}
}
using namespace dij;
signed main(){
	n = read(),m = read();
	for (int i = 1;i <= m;i++){
		int x = read(),y = read(),z = read();
		edge_add(x,y,z);
		edge_add(y,x,z);
	}
	dijkstra();
	printf("%lld",dist[n][2]);
	return 0;
}
posted @   Wiueh_Plus  阅读(1664)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示