信息学奥赛一本通 1505:【例 2】双调路径 | 洛谷 P5530 [BalticOI 2002] 双调路径
【题目链接】
ybt 1505:【例 2】双调路径
洛谷 P5530 [BalticOI 2002] 双调路径
【题目考点】
1. 图论:最短路
- Dijkstra算法
- SPFA算法
2. 动态规划:图上动规
3. 树状数组
【解题思路】
无向图,图中每条边的权值包括两种:费用c和时间t。
根据题意,一条路径比另一条路径更“小”的定义如下:
路径1上各边的费用加和为 C 1 C_1 C1,时间加和为 T 1 T_1 T1
路径2上各边的费用加和为 C 2 C_2 C2,时间加和为 T 2 T_2 T2
如果 C 1 < C 2 C_1 < C_2 C1<C2同时 T 1 < T 2 T_1 < T_2 T1<T2,或 C 1 = C 2 C_1 = C_2 C1=C2同时 T 1 < T 2 T_1 < T_2 T1<T2,或 C 1 < C 2 C_1 < C_2 C1<C2同时 T 1 = T 2 T_1 = T_2 T1=T2,那么路径1优于路径2。
与该逻辑等价的逻辑表达式为 ( C 1 < C 2 ∧ T 1 ≤ T 2 ) ∨ ( C 1 ≤ C 2 ∧ T 1 < T 2 ) (C_1<C_2\land T_1\le T_2) \lor (C_1 \le C_2 \land T_1<T_2) (C1<C2∧T1≤T2)∨(C1≤C2∧T1<T2)( ∧ \land ∧表示与, ∨ \lor ∨表示或)
如不满足条件,就不能对路径1与路径2哪条路径更小做出判定。
该概念即为帕累托最优。
本题求从源点s出发到终点e的最“小”的路径的数量。也就是说,从源点s到终点e的所有其它路径中,找不到比最“小”路径更“小”的路径。
由于每条边的时间或费用最大为100,总顶点数为100,则最小路径总边数不超过100,路径的时间或费用最大不超过 100 ∗ 100 = 10000 100*100=10000 100∗100=10000,大小不超过 10000 10000 10000的整数可以作为数组下标。
以下考虑动态规划
状态定义
- 阶段:到达顶点编号i,路径费用j
(或者此处也可以选择使用取路径时间,本题中费用和时间是等价的) - 决策:从某顶点出发下一步到哪个顶点
- 策略:路径
- 策略集合:从源点s出发到顶点i的路径费用为j的所有路径
- 条件:时间最小
在路径费用固定的情况下,只有时间最小的路径才可能是最“小”路径,时间更大的路径没有必要统计。因此只需要统计费用固定的所有路径中时间最小的路径的最小时间。 - 统计量:时间
状态定义 d p i , j dp_{i,j} dpi,j:从源点s出发到顶点i的路径费用为j的所有路径中,时间最小的路径的时间。
初始状态:
源点s到任意顶点的路径是否存在还未知,将dp数组初值都设为无穷大INF,表示先认为源点s到其它顶点的路径都不存在。
源点s到s存在路径费用为0,时间为0的一条路径,所以 d p s , 0 = 0 dp_{s,0} = 0 dps,0=0。
状态转移方程
- 策略集合:从源点s出发到顶点u的路径费用为uc的所有路径
- 分割策略集合:根据有哪些顶点u通过一条边可以到达顶点v来分割策略集合
对于所有到顶点v有边的顶点u,到达顶点u路径费用为uc的路径中时间最小的路径的时间为 d p u , u c dp_{u, uc} dpu,uc,经过一条费用为c,时间为t的边后到达顶点v,到达顶点v时路径的费用为 v c = u c + c vc=uc+c vc=uc+c,时间为 d p u , u c + t dp_{u, uc}+t dpu,uc+t。
如果该时间 d p u , u c + t dp_{u, uc}+t dpu,uc+t比原来到达顶点v路径费用为 v c vc vc的时间最小的路径的时间更小,则更新该到达顶点v费用为 v c vc vc的最小的路径的时间 d p v , v c dp_{v, vc} dpv,vc为 d p u , u c + t dp_{u, uc}+t dpu,uc+t。
由于顶点v的状态值发生更新了,表示存在一条从源点到顶点v的费用为 v c vc vc的时间更小的路径,接下来还要从顶点v出发更新它的邻接点。这一过程可以使用SPFA算法,或Dijkstra堆优化算法(将顶点v加入优先队列)完成。队列或优先队列中保存路径到达顶点u,以及路径费用c。
解法1:SPFA算法
只要顶点v的状态发生更新,就将顶点v,以及到顶点v的路径的费用vc合成一个路径对象加入队列。
inQue
设为二维数组,inQue[i][j]
表示到达顶点i费用为j的路径是否在队列内。
解法2:Dijkstra算法
路径对象包括到达顶点u,路径费用uc,路径时间ut。路径时间t小的路径更优先。
如果出队的路径的时间t已经大于 d p u , u c dp_{u, uc} dpu,uc,那么就没有必要尝试更新顶点u的连接点的状态。
最后根据dp数组统计出最“小”路径的数量。如果已经存在一条到达终点e,费用为ec,时间为et的最小路径,那么对于到达顶点e费用大于ec的路径,其时间必须小于et,这样的路径才是最小路径。因此当路径费用不断增大时,路径的时间必须小于上一次选出的最小路径的时间,该路径才是一个最小路径。
设minTime为上一次选择的路径的最小时间,初值设为INF。 j j j表示路径费用,从0到10000循环。ans表示最小路径的数量。
如果发现 d p e , j < m i n T i m e dp_{e,j} < minTime dpe,j<minTime,则到达顶点e,费用为j,时间为 d p e , j dp_{e,j} dpe,j的路径也是一条最小路径,最小路径数量ans增加1。
最后输出最小路径数量ans。
以上逻辑写出的代码已经可以通过本题,不过还可以进行进一步优化。
【优化】树状数组优化
对于一个从源点s出发,通过顶点u到达顶点v,费用为 v c vc vc,时间为 d p u , u c + t dp_{u, uc}+t dpu,uc+t的路径,根据上述找最小路径的思路,如果该路径的时间大于等于到达顶点v的费用小于等于 v c vc vc的任意路径的时间,则该路径一定不是最优路径。因此只有该路径时间 d p u , u c + t dp_{u,uc}+t dpu,uc+t满足小于所有 顶点费用小于等于 v c vc vc的每个路径的最小时间 的最小值,也就是 m i n 0 ≤ j ≤ v c { d p v , j } min_{0\le j\le vc}\{dp_{v,j}\} min0≤j≤vc{dpv,j},才需要更新状态 d p v , v c dp_{v, vc} dpv,vc。
遍历求最小值一定会超时,此处是求一个数组前j个元素的最小值,该最小值可以使用树状数组维护,每次更新和查询都是 O ( l o g n ) O(logn) O(logn)复杂度。
注意:树状数组下标从1开始,因此传入的值需要先加1后再进行更新或查询。
【题解代码】
解法1:图上动规 基本做法
- SPFA算法
#include<bits/stdc++.h>
using namespace std;
#define N 105
#define M 10005
#define INF 0x3f3f3f3f
struct Edge
{int v, c, t;//到顶点v,费用c,时间t
};
struct Path
{int u, c;
};
vector<Edge> edge[N];
int n, m, s, e, ans, dp[N][M], minTime = INF;
bool inQue[N][M];
void spfa(int sv)
{memset(dp, 0x3f, sizeof(dp));queue<Path> que;dp[sv][0] = 0;//dp[i][j]:从源点到顶点i使用费用为j的路径的最小时间que.push(Path{sv, 0});inQue[sv][0] = true;while(!que.empty()){ int u = que.front().u, uc = que.front().c;//存在一条从源点到u的花费为uc的路径。que.pop();inQue[u][uc] = false;for(Edge e : edge[u]){int v = e.v, c = e.c, t = e.t, vc = uc+c;if(vc <= 10000 && dp[v][vc] > dp[u][uc]+t){dp[v][vc] = dp[u][uc]+t;if(!inQue[v][vc]){inQue[v][vc] = true;que.push(Path{v, vc});}}}}
}
int main()
{int p, r, c, t;cin >> n >> m >> s >> e;for(int i = 1; i <= m; ++i){cin >> p >> r >> c >> t;edge[p].push_back(Edge{r, c, t});edge[r].push_back(Edge{p, c, t});}spfa(s);for(int j = 0; j <= 10000; ++j) if(dp[e][j] < minTime){ans++;minTime = dp[e][j];}cout << ans;return 0;
}
- Dijkstra堆优化算法
#include<bits/stdc++.h>
using namespace std;
#define N 105
#define M 10005
#define INF 0x3f3f3f3f
struct Edge
{int v, c, t;//到顶点v,费用c,时间t
};
struct Path
{int u, c, t;bool operator < (const Path &b) const{return b.t < t;}
};
vector<Edge> edge[N];
int n, m, s, e, ans, dp[N][M], minTime = INF;
void dijkstra(int sv)
{memset(dp, 0x3f, sizeof(dp));priority_queue<Path> que;dp[sv][0] = 0;//dp[i][j]:从源点到顶点i使用费用为j的路径的最小时间que.push(Path{sv, 0, 0});while(!que.empty()){int u = que.top().u, uc = que.top().c, ut = que.top().t;que.pop();if(dp[u][uc] < ut)//该路径已经不是最优的,则跳过 continue;for(Edge e : edge[u]){int v = e.v, c = e.c, t = e.t, vc = uc+c;if(vc <= 10000 && dp[v][vc] > dp[u][uc]+t)//注意:vc=uc+c可能超过10000,导致数组越界 {dp[v][vc] = dp[u][uc]+t;que.push(Path{v, vc, dp[v][vc]});}}}
}
int main()
{int p, r, c, t;cin >> n >> m >> s >> e;for(int i = 1; i <= m; ++i){cin >> p >> r >> c >> t;edge[p].push_back(Edge{r, c, t});edge[r].push_back(Edge{p, c, t});}dijkstra(s);for(int j = 0; j <= 10000; ++j) if(dp[e][j] < minTime){ans++;minTime = dp[e][j];}cout << ans;return 0;
}
解法2:图上动规+树状数组优化
- SPFA算法
#include<bits/stdc++.h>
using namespace std;
#define N 105
#define M 10005
#define INF 0x3f3f3f3f
struct Edge
{int v, c, t;//到顶点v,费用c,时间t
};
struct Path
{int u, c;
};
vector<Edge> edge[N];
int n, m, s, e, ans, dp[N][M], minTime = INF, tree[N][M];
bool inQue[N][M];
int lowbit(int x)
{return x & -x;
}
void update(int i, int j, int v)//dp[i][j] = v
{for(int x = j+1; x < M; x += lowbit(x))tree[i][x] = min(tree[i][x], v);
}
int query(int i, int j)//min{dp[i][0]~dp[i][j]}
{int res = INF;for(int x = j+1; x > 0; x -= lowbit(x))res = min(res, tree[i][x]);return res;
}
void spfa(int sv)
{memset(dp, 0x3f, sizeof(dp));memset(tree, 0x3f, sizeof(dp));queue<Path> que;dp[sv][0] = 0;//dp[i][j]:从源点到顶点i使用费用为j的路径的最小时间que.push(Path{sv, 0});inQue[sv][0] = true;while(!que.empty()){ int u = que.front().u, uc = que.front().c;//存在一条从源点到u的花费为uc的路径。que.pop();inQue[u][uc] = false;for(Edge e : edge[u]){int v = e.v, c = e.c, t = e.t, vc = uc+c;if(vc <= 10000 && query(v, vc) > dp[u][uc]+t){dp[v][vc] = dp[u][uc]+t;update(v, vc, dp[u][uc]+t);if(!inQue[v][vc]){inQue[v][vc] = true;que.push(Path{v, vc});}}}}
}
int main()
{int p, r, c, t;cin >> n >> m >> s >> e;for(int i = 1; i <= m; ++i){cin >> p >> r >> c >> t;edge[p].push_back(Edge{r, c, t});edge[r].push_back(Edge{p, c, t});}spfa(s);for(int j = 0; j <= 10000; ++j) if(dp[e][j] < minTime){ans++;minTime = dp[e][j];}cout << ans;return 0;
}
- Dijkstra堆优化算法
#include<bits/stdc++.h>
using namespace std;
#define N 105
#define M 10005
#define INF 0x3f3f3f3f
struct Edge
{int v, c, t;//到顶点v,费用c,时间t
};
struct Path
{int u, c, t;bool operator < (const Path &b) const{return b.t < t;}
};
vector<Edge> edge[N];
int n, m, s, e, ans, dp[N][M], tree[N][M], minTime = INF;
int lowbit(int x)
{return x & -x;
}
void update(int i, int j, int v)//dp[i][j] = v
{for(int x = j+1; x < M; x += lowbit(x)) tree[i][x] = min(tree[i][x], v);
}
int query(int i, int j)//min{dp[i][0]~dp[i][j]}
{int res = INF;for(int x = j+1; x > 0; x -= lowbit(x))res = min(res, tree[i][x]);return res;
}
void dijkstra(int sv)
{memset(dp, 0x3f, sizeof(dp)); memset(tree, 0x3f, sizeof(dp));priority_queue<Path> que;dp[sv][0] = 0;//dp[i][j]:从源点到顶点i使用费用为j的路径的最小时间que.push(Path{sv, 0, 0});while(!que.empty()){int u = que.top().u, uc = que.top().c, ut = que.top().t;que.pop();if(dp[u][uc] < ut)//该路径已经不是最优的,则跳过 continue;for(Edge e : edge[u]){int v = e.v, c = e.c, t = e.t, vc = uc+c;if(vc <= 10000 && query(v, vc) > dp[u][uc]+t)//注意:vc=uc+c可能超过10000,导致数组越界 {dp[v][vc] = dp[u][uc]+t;update(v, vc, dp[u][uc]+t);que.push(Path{v, vc, dp[v][vc]});}}}
}
int main()
{int p, r, c, t;cin >> n >> m >> s >> e;for(int i = 1; i <= m; ++i){cin >> p >> r >> c >> t;edge[p].push_back(Edge{r, c, t});edge[r].push_back(Edge{p, c, t});}dijkstra(s);for(int j = 0; j <= 10000; ++j) if(dp[e][j] < minTime){ans++;minTime = dp[e][j];}cout << ans;return 0;
}