月赛题 P8860 题解
先说一点题外话. 原本以为开放题解之后就会有人来提交简单题解, 没想到一个星期过去了还是各种主席树云云. 明明这题没那么麻烦. 或者说有简单的做法, 虽然正确性还是需要一点思考.
题意
有一张
你需要对每次修改输出是否删掉了这条边.
分析与解答
我们是一定要离线做的 (不然就真的是动态图连通性了, 约等于不能做).
首先我们发现, 如果一条边被多次试图删除, 那么后面那次肯定是没有用的.
因为如果第一次删除它就成功删除, 那么第二次删除的时候会什么都不做.
如果第一次删除它发现没法删除, 那说明它是从
综上所述, 我们可以假设每条边只会被删除一次. 设第
我们来考虑所有修改都执行完毕之后, 还剩下的从
这条路径应该满足什么性质呢?
首先, 比较容易看出来的是, 这条路径上
其次, 在所有这样的路径中,
依此类推, 我们可以把比较的规则总结为: 把每条路径上的边的
我们设
如果我们截止到这一步, 那么可以把每条路径的边权, 或者说“排列”, 用主席树存储来比较, 详见其他题解. 不过事实上我们有更容易的方法.
如果你不想看证明或者想自己思考证明, 请跳到下面加粗的综上所述的位置.
为了叙述方便, 我们想象, 这个问题相当于把每条边的边权定为
如果我们经过了
d_i = 2, 3 的边, 路径长就是110 .如果我们经过了
d_i = 2, 4 的边, 路径长就是101 , 比前面那个短.
我们使用 Dijkstra 算法, 设
假设 Dijkstra 进行到某个时刻.
我们要考虑那些起点已经被扩展出来, 终点还没有被扩展出来的边,
选取其中
由于这个原因, 我们需要比较两条边的
- 如果
s_1 = s_2 , 显然我们只需要比较d_1 和d_2 . - 否则, 我们假设
dis_{s_1} < dis_{s_2} (反过来也一样, 我就不写两遍了). - 由于 Dijkstra 是按
dis 从小到大扩展点, 而s_2 已经扩展出来了,dis_{s_1} + 10^{q-d_1} 那个点还没扩展出来, 我们一定有dis_{s_1} + 10^{q-d_1} > dis_{s_2} . - 由于每条边的
d 都是唯一的,dis_{s_1} 和dis_{s_2} 中都肯定没有10^{q-d_1} 这一位 (因为有这一位的路径都经过了d_1 对应的边, 但是d_1 对应的这条边的终点我们还没扩展出来呢).
综上,
其中三个红色的 xxx 都是相同的数字, 而 aaa 和 bbb 位数相同, aaa 小于 bbb;
蓝色的 0/1 就是
因此不难看出, 如果我们把 1 如果落在绿色位置, 也就是
综上所述, 我们在 Dijkstra 的时候只需要比较最后走出的这一步的
事实上,
(真的要比较也不难, 因为我们不需要实际求出
我们记录每个点的最短路的前驱, 最后就可以找出
最初的想法
事实上, 这个解法是我在手玩了自己随便写的几个数据之后想到的, 并且最开始也不会有这些复杂度证明, 只是一个简单的贪心想法.
如果我们有了这个想法, 试图证明它也不是困难的, 读者也可以自己试着证一下. 核心思路就是假设有一条道路比这样求出的更优, 然后想一想 Dijkstra 中哪一步走不通. 如果没有想法, 也可以自己画点小数据手玩一下.
代码
#include <algorithm>
#include <queue>
#include <vector>
#include <functional>
#include <utility>
#include <cctype>
#include <cstdio>
#include <cstring>
typedef long long LL;
const int N = 200050;
int pre[N], nxt[N], fr[N], to[N], a[N], cnt;
bool rm[N];
int read() {
int ans = 0, c;
while (!isdigit(c = getchar()));
do ans = ans * 10 + c - '0';
while (isdigit(c = getchar()));
return ans;
}
typedef std::pair<int, int> E;
#define mp std::make_pair
std::priority_queue<E> Q;
int fa[N];
int main() {
int n = read(), m = read(), q = read();
for (int i = 1; i <= m; ++i) {
int x = fr[i] = read();
to[i] = read();
nxt[i] = pre[x];
pre[x] = i;
a[i] = q + 1;
}
for (int i = 0; i < q; ++i) {
int x = read();
if (a[x] > i) rm[a[x] = i] = true;
else rm[i] = false;
}
to[m + 1] = 1;
Q.push(mp(0, m + 1));
while (!Q.empty()) {
E x = Q.top(); Q.pop();
int u = x.second, t = to[u];
if (fa[t]) continue;
fa[t] = u;
for (int i = pre[t]; i; i = nxt[i])
Q.push(mp(a[i], i));
}
for (int i = n; i != 1; i = fr[fa[i]]) {
// printf("%d %d %d\n", i, fa[i], a[fa[i]]);
rm[a[fa[i]]] = false;
}
for (int i = 0; i < q; ++i)
printf("%d\n", rm[i] ? 1 : 0);
}
一些吐槽
我不是很懂出题人为什么把这题放到月赛第三题, 我觉得得出这个贪心并证明它并不是困难的. 更不懂为什么出题人没有在题解上给出更简单的做法. 大约是其真的没想到吧.
更奇怪的是居然比赛时少有人 AC. 是不是放在 C 题就把大家吓跑了, 想到贪心也觉得是假的呢?