当前位置: 首页 > news >正文

最短路练习

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

最短路练习

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

目录

  • 最短路练习
    • 通信线路
      • 思路一:分层图最短路
      • 思路二:二分,双端队列 0-1bfs
    • 最优贸易
      • 思路一:双向SPFA
      • 思路二:分层建图最短路
    • 道路与航线(拓扑排序+Dijkstra)
    • 小结

通信线路

340. 通信线路 - AcWing题库

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


思路一:分层图最短路

可以仿照动态规划的思想,用D˙[x,p]\dot{D}[x,p]D˙[x,p]表示从 1 号节点到达基站χ\chiχ,途中已经指定了ppp条电缆免费时,经过的路径上最贵的电缆的花费最小是多少(也就是选择一条从 1到xxx的路径,使路径上第p+1p+1p+1大的边权尽量小)。若有一条从xxxyyy长度为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∗KN*KNK个点,P∗KP*KPK条边的广义最短路问题,被称为分层图最短路。对于非特殊构造的数据,SPFA 算法的时间复杂度为 O(tNP)O(tNP)O(tNP),其中ttt为常数, 实际测试可以 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 到NNN的最短路是否不超过KKK即可。可以用双端队列 BFS 求解这种边权只有 0 和 1 的最短路问题。整个算法时间复杂度为O((N+P)log⁡MAX_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);elseq.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 到nnn的路径,使路径上能选出两个点p,qp,qp,q (先经过ppp后经过qqq),并且“节点qqq的权值减去节点ppp的权值”最大。

思路一:双向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. 动态规划状态设计需满足无后效性

http://www.dtcms.com/a/293109.html

相关文章:

  • Scrapyd与ScrapydAPI深度解析:企业级爬虫部署与管理解决方案
  • 面向对象分析与设计40讲(6)设计原则之开闭原则
  • Go语言初识--标识符 可见性
  • 数据库表介绍
  • ArcGIS地形起伏度计算
  • javaweb小案例1
  • Linux打开、读写一个文件内核做了啥?
  • python安装package和pycharm更改环境变量
  • MySQL:内置函数
  • 基于模拟的流程为灵巧机器人定制训练数据
  • 钢铁逆行者:Deepoc具身智能如何重塑消防机器人的“火场直觉”
  • CY3-NH2/amine 使用注意事项
  • 【nginx】隐藏服务器指纹:Nginx隐藏版本号配置修改与重启全攻略
  • Adaptive Graph Convolutional Network for Knowledge Graph Entity Alignment
  • 基于LangGraph的Open Deep Research架构全解析:从多Agent协作到企业级落地
  • 数据库设计mysql篇
  • 什么是检索增强生成(RAG)?
  • java调用周立功USBCAN SDK读取汽车总线数据
  • [3-02-02].第04节:开发应用 - RequestMapping注解的属性2
  • TCP头部
  • Kotlin伴生对象
  • Go后端配置文件教程
  • LeetCode|Day22|231. 2 的幂|Python刷题笔记
  • AI一周事件(2025年7月15日-7月21日)
  • 开发避坑短篇(4):跨域请求中Session数据丢失的排查与修复方案
  • Qt资源系统:如何有效管理图片和文件
  • 【黑马SpringCloud微服务开发与实战】(五)微服务保护
  • 【NLP舆情分析】基于python微博舆情分析可视化系统(flask+pandas+echarts) 视频教程 - 访问鉴权功能实现
  • MMDeploy模型转换与TensorRT推理遇到问题及解决方案
  • GRU模型