1 条题解
-
0
P3238 [HNOI2014]道路堵塞
前言
基于打破题解区被 SPFA 算法霸凌的局面,对无人问津的 Dijkstra 思路的代码实现,即对
https://www.luogu.com.cn/user/124918 "LinkyChristian"顺便提供一个双倍经验:CF1163F Indecisive Taxi Fee。
题目
给定一个 个点 条单向边的有向图,还给定了 最短路径经过的边的编号。每次询问(询问间相互独立)删去最短路径上的一条边后,新的 最短路径是多少?
Solution
变量解释
原最短路径:题目中给定的最短路径。
原编号:题目输入的边的顺序的编号。
新编号:对原最短路径上的边重新编号,具体编号方式见下述。
-
: 的最短路径长度。
-
:原编号为 的边是否在最短路径上:若为 ,则代表不在;否则,由 的原最短路径经过的边依次从大到小 重新编号。记 的编号为新编号。
-
:从 点连出的最小的原最短路径上的边的新编号。
-
:从原最短路径上的边连入 点最大的新编号。
思路
首先有个很显然的结论,对于必须经过一条连接着 的单向边,权值为 , 的最短路径长度为:
此时就要使用到上述的变量 。根据变量的定义,必须经过 点的最短路径是不经过 这段新编号区间的边的,换句话说就是绕过了这一段原最短路径上的边。
那再转化到边上,对于一条非原最短路径上的边连接着 ,我们就需要更新 这段区间的权值为 。
翻译一下就是:连入 的原最短路径上的边和从 连出的原最短路径上的边,他们之间的原最短路径上的边是可以不用经过的,那不经过这段原最短路径上的边的权值就可以更新为 。
最后查询时就是每次查询不经过某一条原最短路径上的边,新的最短路径最小值。
解法
现在要求这些变量:
最短路径长度
: 到任意点的最短路径。这个好处理,直接沿着正边从 跑一遍 Dijkstra 最短路径就可以求出。
:任意点到 的最短路径。建立反图,沿着反边从 跑一遍 Dijkstra 最短路径就可以求出。
连入及连出某一点的原最短路径上的边的新编号
:因为要求最小值,初始化为极大值, 初始为 。从 出发,沿反图跑最短路径。设当前点为 ,其可以到达的节点为 ,然后从 和 更新:若 不是原最短路径上的边,只能从 更新;否则从 转移。
:因为要求最大值,初始化为极小值, 初始为 。从 出发,沿原图跑最短路径。设当前点为 ,其可以到达的节点为 ,然后从 和 更新:若 不是原最短路径上的边,只能从 更新;否则从 转移。
Code 参考:
处理 ,跑原图:
if (dis[v] == dis[u] + e[i].w) // 路径权值相同就取 max { if (I[i]) // 是原最短路径边直接转移 R[v] = max(R[v], I[i]); else // 否则只能继承 R[v] = max(R[v], R[u]); } if (dis[v] > dis[u] + e[i].w) // 路径更优直接覆盖 { if (I[i]) // 是原最短路径边直接转移 R[v] = I[i]; else // 否则只能继承 R[v] = R[u]; dis[v] = dis[u] + e[i].w; }处理 ,跑反图:
if (dis[v] == dis[u] + e[i].w) // 路径权值相同就取 min { if (I[i]) // 是原最短路径边直接转移 L[v] = min(L[v], I[i]); else // 否则只能继承 L[v] = min(L[v], L[u]); } if (dis[v] > dis[u] + e[i].w) // 路径更优直接覆盖 { if (I[i]) // 是原最短路径边直接转移 L[v] = I[i]; else // 否则只能继承 L[v] = L[u]; dis[v] = dis[u] + e[i].w; }不经过某条原最短路径上的边的新 最短路径最小值
已经求出了 和 ,只要枚举所有的正边,这条正边连接 ,把 这段区间的权值更新为 。也就是一个区间取 操作,用线段树维护最小值,区间修改,单点查询。
Code 参考:
显然用线段树维护最小值,区间修改,单点查询这段代码很简单,直接跳过就行了。
namespace Segment { #define lc(i) (i << 1) #define rc(i) (i << 1 | 1) #define lmid ((l + r) >> 1) #define rmid ((l + r + 2) >> 1) int tr[N << 2], tag[N << 2]; inline void change(const int &i, const int &val) { tr[i] = min(tr[i], val); tag[i] = min(tag[i], val); } inline void push_down(const int &i) { if (tag[i] != INF) { change(lc(i), tag[i]); change(rc(i), tag[i]); tag[i] = INF; } } inline void update(int i, int l, int r, int cl, int cr, int val) { if (l > cr || r < cl) return void(); if (l >= cl && r <= cr) return change(i, val); push_down(i); update(lc(i), l, lmid, cl, cr, val); update(rc(i), rmid, r, cl, cr, val); } inline int query(int i, int l, int r, int x) { if (l == r) return tr[i]; push_down(i); if (x <= lmid) return query(lc(i), l, lmid, x); if (x >= rmid) return query(rc(i), rmid, r, x); } } using namespace Segment;疑问
Q:但是为什么要 尽量小, 尽量大呢?
A: 越小 越大,更新的值域就越大,更新的越多,答案自然就会更优,因此我们让他尽量绕过更多的原最短路径上的边。
Code
删去了一些基础内容,保留了代码框架。完整代码:click here.
#include <bits/stdc++.h> using namespace std; const int INF = 0x3f3f3f3f; const int N = 1e5 + 5; namespace IO{/*Quick Read and Write*/} using namespace IO; namespace chain{/*链式前向星*/} using namespace chain; namespace Segment/*维护区间最小值*/ { inline void update(int i, int l, int r, int cl, int cr, int val){} inline int query(int i, int l, int r, int x){} } using namespace Segment; struct type { int x; int y; friend bool operator<(const type &A, const type &B){return A.y > B.y;} type(int x = 0, int y = 0) : x(x), y(y){} }; int n, m, k; int rode[N]; int I[N << 2], L[N], R[N]; int dis1[N], dis2[N]; bool vis[N]; inline void dij(int s, int *dis) // 代码核心,求 L,R { priority_queue<type> q; memset(vis, false, sizeof(vis)); dis[s] = 0; if (s == n) fill(L + 1, L + n + 1, k + 1), L[s] = 0; if (s == 1) fill(R + 1, R + n + 1, 0), R[s] = k + 1; q.push(type(s, 0)); while (!q.empty()) { int u = q.top().x; q.pop(); if (vis[u]) continue; vis[u] = true; for (int i = head[u]; i; i = e[i].nxt) { if ((i & 1) ^ (s == n)) // 判正反边 continue; int v = e[i].v, ID = I[i] ? I[i] : (k + 1) * (s == 1); /*这里为了减少些 if,所以这样写,其实和上面的参考代码里 if 分类讨论时等价的。*/ if (dis[v] == dis[u] + e[i].w) { if (R[v] < min(R[u], ID) && s == 1) R[v] = min(R[u], ID); if (L[v] > max(L[u], ID) && s == n) L[v] = max(L[u], ID); } if (dis[v] > dis[u] + e[i].w) { if (s == 1) R[v] = min(R[u], ID); if (s == n) L[v] = max(L[u], ID); dis[v] = dis[u] + e[i].w; q.push(type(v, dis[v])); } } } } signed main() { memset(dis1, 0x3f, sizeof(dis1)); memset(dis2, 0x3f, sizeof(dis2)); read(n, m, k); for (int i = 1, u, v, w; i <= m; i++) { read(u, v, w); add(u, v, w); add(v, u, w); } for (int i = 1; i <= k; i++) { read(rode[i]); I[(rode[i] << 1)] = I[(rode[i] << 1) ^ 1] = k - i + 1; // 从大到小编号。 } dij(1, dis1); //原图。 dij(n, dis2); //反图。 for (int i = 2; i <= ecnt; i += 2) // 只枚举正边。 { if (I[i]) continue; int u = e[i].u, v = e[i].v; if (L[v] + 1 <= R[u] - 1) update(1, 1, k, L[v] + 1, R[u] - 1, dis1[u] + e[i].w + dis2[v]); //更新区间最小值 } for (int i = 1; i <= k; i++) { int ans = query(1, 1, k, I[rode[i] << 1]); // 查询不走当前边的最短路径 write(ans == 0x3f3f3f3f ? -1 : ans, '\n'); } return 0; }后言
冒昧地指出
https://www.luogu.com.cn/user/124918 "LinkyChristian"一个小错误:本题为有向图,而 CF1163F Indecisive Taxi Fee 为无向图,所以枚举边时不能用反边更新,只能用正边更新。也就是不能用 更新 这段区间。否则,这题就Wrong了,成功 get 分。而且,这个做法跑了 左右。辛苦管理员的审核,有问题可以指出,极力改正。
-
- 1
信息
- ID
- 5240
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 2
- 已通过
- 2
- 上传者