每想到一些天地都容纳不下的说法
心里就烧起烟霞
没去过心上人流浪的白发天涯
哪里懂镜月水花
最短路练习
在“动态规划”的学习中,所谓“无后效性”其实告诉我们,动态规划对状态空间的遍历构成一张有向无环图,遍历顺序就是该有向无环图的一个拓扑序。有向无环图中的节点对应问题中的“状态”,图中的边则对应状态之间的“转移”,转移的选取就是动态规划中的“决策”。
通信线路
简单来说,本题是在无向图上求出一条从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 N∗K个点, P ∗ K P*K P∗K条边的广义最短路问题,被称为分层图最短路。对于非特殊构造的数据,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;
}
最优贸易
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)
本题是一道明显的单源最短路问题,但图中带有负权边,不能使用 Dijkstra 算法。若直接用 SPFA 算法求解,因为测试数据经过了特殊构造,所以程序无法在规定时限内输出答案。题目中有一个特殊条件——双向边都是非负的,只有单向边可能是负的,并月单向边不构成环。我们应该利用这个性质来解答本题。
如果只把双向边(也就是题目中的“道路”)添加到图里,那么会形成若干个连通块。若把每个连通块整体看作一个“点”,再把单向边(也就是题目中的“航线”)添加到图里,会得到一张有向无环图。在有向无环图上,无论边权正负,都可以按照拓扑序进行扫描,在线性时间内求出单源最短路。这启发我们用拓扑序的框架处理整个图, 但在双向边构成的每个连通块内部使用堆优化的 Dijkstra 算法快速计算该块内的最短路信息。
详细的算法流程如下:
- 只把双向边加入到图中,用深度优先遍历划分图中的连通块,记 id[x] 为节点 x 所属的连通块的编号。
- 统计每个连通块的总入度(有多少条边从连通块之外指向连通块之内),记 rd[i] 为第 i 个连通块的总入度。
- 建立一个队列 q(存储连通块编号,用于拓扑排序),最初队列中包含所有总入度为零的连通块编号(当然其中也包括 id[S])。设 d[S] = 0,其余点的 d 值为 +∞。
- 取出队头的连通块 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),直至堆为空。 - 重复步骤 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) |
动态决策 | 动态规划+最短路 | 状态转移设计 | 依赖状态数 |
解题框架
- 建图阶段:
- 区分边类型(双向/单向、正权/负权)
- 按需构建分层图或附加状态
- 算法选择:
- 存在拓扑序时优先用拓扑排序
- 0-1权值用双端队列BFS
- 大规模正权图用Dijkstra
- 严格约束时结合动态规划
- 验证优化:
- 二分答案转化判定问题
- 反向图处理双向搜索
- 缩点降低复杂度
经典模型
- 通信线路:二分答案+0-1BFS
- 最优贸易:正反SPFA/分层图
- 道路与航线:拓扑排序+Dijkstra
注意事项
- 优先分析图的特殊性质
- 大规模数据慎用SPFA
- 分层图注意空间开销
- 动态规划状态设计需满足无后效性