最短路练习

每想到一些天地都容纳不下的说法
心里就烧起烟霞
没去过心上人流浪的白发天涯
哪里懂镜月水花

最短路练习

在“动态规划”的学习中,所谓“无后效性”其实告诉我们,动态规划对状态空间的遍历构成一张有向无环图,遍历顺序就是该有向无环图的一个拓扑序。有向无环图中的节点对应问题中的“状态”,图中的边则对应状态之间的“转移”,转移的选取就是动态规划中的“决策”。

通信线路

340. 通信线路 - AcWing题库

简单来说,本题是在无向图上求出一条从1到N的路径,使路径上第K+1大的边权尽量小。


思路一:分层图最短路

可以仿照动态规划的思想,用 D ˙ [ x , p ] \dot{D}[x,p] D˙[x,p]表示从 1 号节点到达基站 χ \chi χ,途中已经指定了 p p p条电缆免费时,经过的路径上最贵的电缆的花费最小是多少(也就是选择一条从 1到 x x x的路径,使路径上第 p + 1 p+1 p+1大的边权尽量小)。若有一条从 x x x y y y长度为z 的无向边,则应该用 max ⁡ ( D [ x , p ] , z ) \max(D[x,p],z) max(D[x,p],z)更新 D [ y , p ] D[y,p] D[y,p]的最小值,用 D [ x , p ] D[x,p] D[x,p]更新 D [ y , p + 1 ] D[y,p+1] D[y,p+1]的最小值。前者表示不在电缆 ( x , y , z ) (x,y,z) (x,y,z)上使用免费升级服务,后者表示使用。

显然,我们刚才设计的状态转移是有后效性的。在有后效性时,一种解决方案就是利用迭代思想,借助 SPFA 算法进行动态规划,直至所有状态收敛(不能再更新)。

从最短路问题的角度去理解,图中的节点也不仅限于“整数编号”,可以扩展到二维,用二元组 ( x , p ) (x,p) (x,p)代表一个节点,从 ( x , p ) (x,p) (x,p) ( y , p ) (y,p) (y,p)有长度为 z 的边,从 ( x , p ) (x,p) (x,p) ( y , p + 1 ) (y,p+1) (y,p+1)有长度为 0 的边。 D [ x , p ] D[x,p] D[x,p]表示从起点 (1,0) 到节点 ( x , p ) (x,p) (x,p),路径上最长的边最短是多少。这是 N ∗ K N*K NK个点, P ∗ K P*K PK条边的广义最短路问题,被称为分层图最短路。对于非特殊构造的数据,SPFA 算法的时间复杂度为 O ( t N P ) O(tNP) O(tNP),其中 t t t为常数, 实际测试可以 AC。本题也让我们进一步领会了动态规划与最短路问题的共通性。

SPFA

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int> 
#define fi first
#define se second
const int N=1005;
const int inf=0x7f7f7f7f;
struct tu{
	int v,w;
};
vector<tu> e[N];
int d[N][N];
bool v[N][N];
int n,m,k;
void spfa(int s){
	queue<pii> q;
	memset(d,inf,sizeof d);
	memset(v,0,sizeof v);
	d[s][0]=0;v[s][0]=1;q.push({s,0});	
	while(q.size()){
		pii f=q.front();
		q.pop();
		int i=f.fi,j=f.se;
		v[i][j]=0;
		for(auto x:e[i]){
			int y=x.v,z=x.w;
			if(d[y][j]>max(d[i][j],z)){
				d[y][j]=max(d[i][j],z);
				if(!v[y][j])
				q.push({y,j}),v[y][j]=1;
			}
			if(j<k&&d[y][j+1]>d[i][j]){
				d[y][j+1]=d[i][j];
				if(!v[y][j+1])
				q.push({y,j+1}),v[y][j+1]=1;
			}
		}
	}
}
void slove(){
	cin>>n>>m>>k;
	for(int i=1;i<=m;i++){
		int a,b,c;cin>>a>>b>>c;
		e[a].push_back({b,c});
		e[b].push_back({a,c});
	}
	spfa(1);
	int an=inf;
	for(int i=0;i<=k;i++){
		an=min(an,d[n][i]);
	}
	if(an==inf) cout<<-1<<endl;
	else cout<<an<<endl;
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	//cin>>_;
	while(_--)
	slove();
	return 0;
}

当然数据范围不算大,利用Dijkstra也可以通过这题

Dijkstra

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,pair<int,int>> 
#define fi first
#define se second
const int N=1005;
const int inf=0x7f7f7f7f;
struct tu{
	int v,w;
};
vector<tu> e[N];
int d[N][N];
bool v[N][N];
int n,m,k;
priority_queue<pii,vector<pii>,greater<pii>> q;
void dijk(int s){
	memset(d,inf,sizeof d);
	d[s][0]=0;
	q.push({0,{s,0}});
	while(q.size()){
		pii f=q.top();
		q.pop();
		int i=f.se.fi,j=f.se.se;
		if(v[i][j]) continue;
		v[i][j]=1;
		for(auto x:e[i]){
			int y=x.v,z=x.w;
			if(d[y][j]>max(d[i][j],z)){
				d[y][j]=max(d[i][j],z);
				q.push({d[y][j],{y,j}});
			}
			if(j< k&&d[y][j+1]>d[i][j]){
				d[y][j+1]=d[i][j];
				q.push({d[y][j+1],{y,j+1}});
			}
		}
	}
}
void slove(){
	cin>>n>>m>>k;
	for(int i=1;i<=m;i++){
		int a,b,c;cin>>a>>b>>c;
		e[a].push_back({b,c});
		e[b].push_back({a,c});
	}
	dijk(1);
	int an=inf;
	for(int i=0;i<=k;i++){
		an=min(an,d[n][i]);
	}
	if(an==inf) cout<<-1<<endl;
	else cout<<an<<endl;
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	//cin>>_;
	while(_--)
	slove();
	return 0;
}

思路二:二分,双端队列 0-1bfs

求最大的最小,不难想到熟悉的二分答案;

本题的答案显然具有单调性,因为支付的钱更多时,合法的升级方案一定包含了花费更少的升级方案。所以我们可以二分答案,把问题转化为:是否存在一种合法的升级方法,使花费不超过 mid。

转化后的判定问题非常容易。只需要把升级价格大于 mid 的电缆看作长度为 1 的边,把升级价格不超过mid的电缆看作长度为 0 的边,然后求从 1 到 N N N的最短路是否不超过 K K K即可。可以用双端队列 BFS 求解这种边权只有 0 和 1 的最短路问题。整个算法时间复杂度为 O ( ( N + P ) log ⁡ M A X _ L ) \mathcal{O}((N+P)\log MAX\_L) O((N+P)logMAX_L)

双端队列的贪心策略

  • 权值为0的边:直接加入队首(类似Dijkstra的优先队列,保证优先处理当前最优解)。
  • 权值为1的边:加入队尾(按BFS层级处理)。
  • 这样能保证队列始终按"当前删除边数"从小到大排序,优先扩展删除边数少的路径。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int> 
#define fi first
#define se second
const int N=1005;
const int inf=0x7f7f7f;
struct tu{
	int v,w;
};
vector<tu> e[N];
int d[N];
int n,m,k;
deque<int> q;
bool chack(int mid){
	memset(d,inf,sizeof d);
	d[1]=0;
	q.push_back(1);
	while(q.size()){
		int f=q.front();
		q.pop_front();
		for(auto x:e[f]){
			int y=x.v,z=x.w;
			if(z>mid) z=1;
			else z=0;
			if(d[y]>d[f]+z){
				d[y]=d[f]+z;
				if(z==0)
				q.push_front(y);
				else
				q.push_back(y);
			}
		}
	}
	return d[n]<=k;
}
void slove(){
	cin>>n>>m>>k;
	for(int i=1;i<=m;i++){
		int a,b,c;cin>>a>>b>>c;
		e[a].push_back({b,c});
		e[b].push_back({a,c});
	}
	int l=0,r=inf;
	while(l<r){
		int mid=l+r+1>>1;
		if(chack(mid))
		r=mid-1;
		else 
		l=mid;
	}
	if(l==inf) cout<<-1;
	else{
	    if(!chack(l)) l++;
	    cout<<l;
	} 
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	//cin>>_;
	while(_--)
	slove();
	return 0;
}

最优贸易

341. 最优贸易 - AcWing题库

P1073 [NOIP 2009 提高组] 最优贸易 - 洛谷

本题是让我们在一张节点带有权值的图上找出一条从 1 到 n n n的路径,使路径上能选出两个点 p , q p,q p,q (先经过 p p p后经过 q q q),并且“节点 q q q的权值减去节点 p p p的权值”最大。

思路一:双向SPFA

根据题意,我们可以看出,我们需要找到一条从1到n的路,路上会经过两个节点,这两个点,一个点买,一个点卖。我们希望这两个点差价最大。
那么我们就可以先用SPFA或Dijkstra搜索从1号点出发,到所有节点路径上的最小值min[i];
然后我们在读入数据时,再建立一个反向图,我们再以n为起点进行SPFA,记录路线上所有节点到n的最大值max[i].
然后我们再枚举遍历所有节点,取max(max[i]-min[i])就是我们的结果。
当然我们需要初始所有节点的min为最大,maxs为最小

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int N=1e5+5;
const int inf=0x3f3f3f;
vector<int> e1[N],e2[N];
int vl[N];
int n,m;
int d1[N],d2[N];
bool v[N];
void spfa1(int s){
	queue<int> q;
	memset(d1,inf,sizeof d1);
	memset(v,0,sizeof v);
	d1[s]=vl[s];v[s]=1;q.push(s);	
	while(q.size()){
		int f=q.front();
		q.pop();
		v[f]=0;
		for(auto x:e1[f]){
			if(d1[x]>min(d1[f],vl[x])){
				d1[x]=min(d1[f],vl[x]);
				if(!v[x])
				q.push(x),v[x]=1;
			}
		}
	}
}
void spfa2(int s){
	queue<int> q;
	memset(d2,-inf,sizeof d2);
	d2[s]=vl[s];v[s]=1;q.push(s);	
	while(q.size()){
		int f=q.front();
		q.pop();
		v[f]=0;
		for(auto x:e2[f]){
			if(d2[x]<max(d2[f],vl[x])){
				d2[x]=max(d2[f],vl[x]);
				if(!v[x])
				q.push(x),v[x]=1;
			}
		}
	}
}
void slove(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	cin>>vl[i];
	for(int i=1;i<=m;i++){
		int a,b,c;cin>>a>>b>>c;
        e1[a].push_back(b);
		e2[b].push_back(a);
		if(c==2){
			e1[b].push_back(a);
			e2[a].push_back(b);
		}
	}
	spfa1(1);
	spfa2(n);
	int an=-inf;
	for(int i=1;i<=n;i++)
	an=max(an,d2[i]-d1[i]);
	cout<<an;
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--)
	slove();
	return 0;
}

思路二:分层建图最短路

!!!这个方法的时间复杂度和空间复杂度都比第一个要高!!洛谷的数据水可以ak,acwing上严格数据会超时或超空间!!仅供学习思路!!!

我们这么想,从1出发在某个节点买入,在某个节点卖出,最后走到n。所以整个过程分为3个部分,因为可能存在有向边,所以可能在低点买入后,无法达到最高的卖出点。

那么我们把图分为3层,每层节点之间的边权都是0,因为同层之间无论怎么走没有买卖交易,所以钱数不变。第一层表示在那个节点买入,一旦确定买入节点后,那么就通过单向边进入第二层对应的节点,并且边权是这个节点买入价格的负数;第二层再确定某个节点卖出,从这个节点通过单向边进入第三层,边权是这个节点的价格。那么所有走过边权的路径相加就是答案,我们希望答案最大,那么就是SPFA求最长路径。

比如w[i]用来记录1号到i号的最长路径,那么我们最后输出w[3*n]即可。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int N=3e5+5;
const int inf=0x3f3f3f;
struct edge{
	int v,w;
};
vector<edge> e[N];
int n,m;
int d[N];
bool v[N];
void spfa(int s){
	queue<int> q;
	memset(d,-inf,sizeof d);
	d[s]=0;v[s]=1;q.push(s);	
	while(q.size()){
		int u=q.front();
		q.pop();
		v[u]=0;
		for(auto x:e[u]){
			if(d[x.v]<d[u]+x.w){
				d[x.v]=d[u]+x.w;
				if(!v[x.v])
				q.push(x.v),v[x.v]=1;
			}
		}
	}
}
void slove(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int vl;cin>>vl;
		e[i].push_back({i+n,-vl});
		e[i+n].push_back({i+n+n,vl});
	}
	for(int i=1;i<=m;i++){
		int a,b,c;cin>>a>>b>>c;
		e[a].push_back({b,0});
		e[a+n].push_back({b+n,0});
		e[a+n+n].push_back({b+n+n,0});
		if(c==2){
			e[b].push_back({a,0});
			e[b+n].push_back({a+n,0});
			e[b+n+n].push_back({a+n+n,0});
		}
	}
	spfa(1);
	cout<<d[n+n+n];
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--)
	slove();
	return 0;
}

道路与航线(拓扑排序+Dijkstra)

342. 道路与航线 - AcWing题库

本题是一道明显的单源最短路问题,但图中带有负权边,不能使用 Dijkstra 算法。若直接用 SPFA 算法求解,因为测试数据经过了特殊构造,所以程序无法在规定时限内输出答案。题目中有一个特殊条件——双向边都是非负的,只有单向边可能是负的,并月单向边不构成环。我们应该利用这个性质来解答本题。
如果只把双向边(也就是题目中的“道路”)添加到图里,那么会形成若干个连通块。若把每个连通块整体看作一个“点”,再把单向边(也就是题目中的“航线”)添加到图里,会得到一张有向无环图。在有向无环图上,无论边权正负,都可以按照拓扑序进行扫描,在线性时间内求出单源最短路。这启发我们用拓扑序的框架处理整个图, 但在双向边构成的每个连通块内部使用堆优化的 Dijkstra 算法快速计算该块内的最短路信息。

详细的算法流程如下

  1. 只把双向边加入到图中,用深度优先遍历划分图中的连通块,记 id[x] 为节点 x 所属的连通块的编号。
  2. 统计每个连通块的总入度(有多少条边从连通块之外指向连通块之内),记 rd[i] 为第 i 个连通块的总入度。
  3. 建立一个队列 q(存储连通块编号,用于拓扑排序),最初队列中包含所有总入度为零的连通块编号(当然其中也包括 id[S])。设 d[S] = 0,其余点的 d 值为 +∞。
  4. 取出队头的连通块 i,对该连通块执行堆优化的 Dijkstra 算法,具体步骤为:
    (1) 建立一个堆 pq,把第 i 个连通块的所有节点加入堆 pq 中。
    (2) 从堆中取出 d[x] 最小的节点 x。
    (3) 若 x 被扩展过,回到步骤(2),否则进入下一步。
    (4) 扫描从 x 出发的所有边 (x, y, z),用 d[x] + z 更新 d[y]。
    (5) 若 id[x] = id[y](仍在连通块内)并且 d[y] 被更新,则把 y 插入堆。
    (6) 若 id[x] ≠ id[y],则令 rd[id[y]] 减去 1。若减到了 0,则把 id[y] 插入拓扑排序的队列末尾。
    (7) 重复步骤(2)~(6),直至堆为空。
  5. 重复步骤 4,直至队列为空,拓扑排序完成。

数组 d 即为所求。整个算法的时间复杂度为 O(T + P + R log T)。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
void topsort(int s);
void dijk(int s);

const int N=25005;
const int inf=0x3f3f3f;
struct edge{
	int v,w;
};
vector<edge> e[N];
int n,m1,m2,s;
int d[N];
bool v[N];
int id[N];
vector<int> fk[N];
int rd[N];

void dfs(int u,int c){
	id[u]=c;
	fk[c].push_back(u);
	for(auto i:e[u]){
		if(!id[i.v])
		dfs(i.v,c);
	}
}

queue<int> q;
void topsort(int s,int c){
	memset(d,inf,sizeof d);
	d[s]=0;
	for(int i=1;i<=c;i++)
	if(rd[i]==0)
	q.push(i);
	while(q.size()){
		int f=q.front();
		q.pop();
		dijk(f);
	}
}

void dijk(int s){
	priority_queue<pii,vector<pii>,greater<pii>> pq;
	
	for(auto i:fk[s]) pq.push({d[i],i});
	
	while(pq.size()){
		pii f=pq.top();
		pq.pop();
		int u=f.se;
		if(v[u]) continue;
		v[u]=1;
		for(auto x:e[u]){
			int y=x.v,z=x.w;
			if(d[y]>d[u]+z){
				d[y]=d[u]+z;
				if(id[y]==s) pq.push({d[y],y});
			}
			if(id[y]!=s&&--rd[id[y]]==0)
			q.push(id[y]);
		}
	}
}

void slove(){
	cin>>n>>m1>>m2>>s;
	
	for(int i=1;i<=m1;i++){
		int a,b,c;cin>>a>>b>>c;
		e[a].push_back({b,c});
		e[b].push_back({a,c});
	}
	int cnt=1;
	for(int i=1;i<=n;i++){
		if(!id[i]){
			dfs(i,cnt);
			cnt++;
		}
	}
	cnt--;
	
	for(int i=1;i<=m2;i++){
		int a,b,c;cin>>a>>b>>c;
		e[a].push_back({b,c});
		rd[id[b]]++;
	}	
	
	topsort(s,cnt);
	
	for(int i=1;i<=n;i++){
		if(d[i]>=inf) cout<<"NO PATH"<<endl;
		else cout<<d[i]<<endl;
	}
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--)
	slove();
	return 0;
}

小结

问题特征识别

  • 混合图:同时存在正权双向边和负权单向边(如道路与航线)
  • 分层需求:需要记录额外状态(如免费次数、买卖操作)
  • 特殊限制:负权边不构成环、边权有特殊性质等

核心方法选择

场景适用算法关键优化时间复杂度
纯正权图Dijkstra(堆优化)优先队列O(m log n)
含负权边SPFA队列松弛O(km)~O(nm)
分层图拓扑排序+Dijkstra连通块划分O(T + P + R log T)
0-1权值双端队列BFS队首/队尾插入O(n + m)
动态决策动态规划+最短路状态转移设计依赖状态数

解题框架

  1. 建图阶段
    • 区分边类型(双向/单向、正权/负权)
    • 按需构建分层图或附加状态
  2. 算法选择
    • 存在拓扑序时优先用拓扑排序
    • 0-1权值用双端队列BFS
    • 大规模正权图用Dijkstra
    • 严格约束时结合动态规划
  3. 验证优化
    • 二分答案转化判定问题
    • 反向图处理双向搜索
    • 缩点降低复杂度

经典模型

  • 通信线路:二分答案+0-1BFS
  • 最优贸易:正反SPFA/分层图
  • 道路与航线:拓扑排序+Dijkstra

注意事项

  1. 优先分析图的特殊性质
  2. 大规模数据慎用SPFA
  3. 分层图注意空间开销
  4. 动态规划状态设计需满足无后效性

### 关于分层图短路算法的推荐练习题 #### 背景介绍 分层图短路径问题是基于图论的一种扩展模型,通常用于处理带有额外约束条件或者动态变化的情况。例如,在某些场景下,边的权重可能随时间或其他因素改变,此时可以通过构建多层图来模拟这些变化并求解短路径。 #### 推荐题目及其解析 1. **LeetCode 787. Cheapest Flights Within K Stops** 这道题目是一个典型的分层图应用案例。给定一张航班网络图以及多允许经过 `K` 次中转的情况下,找到从起点到终点的低价格路线。此问题可以转化为在一个具有 `(n * (k+1))` 层次的分层图上运行 Dijkstra 或 Bellman-Ford 算法[^4]。 ```python import heapq def findCheapestPrice(n, flights, src, dst, k): graph = [[] for _ in range(n)] for u, v, w in flights: graph[u].append((v, w)) dist = [[float('inf')] * (k + 2) for _ in range(n)] pq = [(0, src, 0)] # cost, node, stops while pq: cost, city, stop = heapq.heappop(pq) if city == dst: return cost if stop <= k: for neighbor, price in graph[city]: new_cost = cost + price if new_cost < dist[neighbor][stop + 1]: dist[neighbor][stop + 1] = new_cost heapq.heappush(pq, (new_cost, neighbor, stop + 1)) return -1 ``` 2. **Codeforces Problem: Shortest Path with Edge Costs Changing Over Time** 此类问题涉及随着时间推移,边的成本会发生变化的情形。通过创建多个层次代表不同时间段的状态转移矩阵,并利用广度优先搜索(BFS)或改进版迪杰斯特拉算法完成计算[^5]。 3. **AtCoder Beginner Contest Example Problems on Layered Graphs** AtCoder 平台上的许多比赛也会涉及到类似的分层图建模技巧,比如某一天开放特定道路通行等问题都可以抽象成此类形式加以解答[^6]。 #### 总结说明 对于初学者而言,可以从上述提到的经典平台入手尝试解决一些基础性的分层图短路径问题;随着经验积累再逐步挑战更复杂的变种情况。值得注意的是,实际编码过程中需特别留意状态定义清晰准确以及边界条件妥善处理等方面事项[^7]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值