把每条可用边贡献为源点随机权值,维护全图哈希和判断是否所有点出度为 1。
OJ: luogu
题目 ID: P8819
难度:提高+/省选-
标签:图论哈希模拟
日期: 2026-07-06 08:46
题意
给定一张有向图,初始所有边都可用。每次操作会删除或恢复一条边,或者删除、恢复某个点的所有入边。每次操作后,需要判断当前是否是一个可以反攻的时刻。
题目中的两个条件合起来,可以转化为一个很简洁的图论条件:每个点当前恰好有一条可用出边。
如果每个点出度都是 1,那么从任意点出发,每一步都能沿唯一出边继续走。图是有限的,所以一定会进入环,从而可以无限穿梭。反过来,如果某个点出度不是 1,它就不满足“恰好一条可用虫洞”的连续穿梭条件。
思路
小数据可以直接维护每条边是否可用,每次操作后重新统计所有点出度:
// brute.cpp:小数据暴力解,直接维护每条边是否可用并检查每个点当前出度。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 35;
int n, m, q;
bool exist_edge[MAXN][MAXN];
bool active_edge[MAXN][MAXN];
bool can_counterattack() {
for (int u = 1; u <= n; u++) {
int out_degree = 0;
for (int v = 1; v <= n; v++) {
if (active_edge[u][v]) {
out_degree++;
}
}
if (out_degree != 1) {
return false;
}
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
exist_edge[u][v] = true;
active_edge[u][v] = true;
}
cin >> q;
while (q--) {
int type;
cin >> type;
if (type == 1) {
int u, v;
cin >> u >> v;
active_edge[u][v] = false;
} else if (type == 2) {
int v;
cin >> v;
for (int u = 1; u <= n; u++) {
if (exist_edge[u][v]) {
active_edge[u][v] = false;
}
}
} else if (type == 3) {
int u, v;
cin >> u >> v;
if (exist_edge[u][v]) {
active_edge[u][v] = true;
}
} else {
int v;
cin >> v;
for (int u = 1; u <= n; u++) {
if (exist_edge[u][v]) {
active_edge[u][v] = true;
}
}
}
cout << (can_counterattack() ? "YES" : "NO") << '\n';
}
return 0;
}暴力的瓶颈在于:n,m,q 都可以达到 5 * 10^5,而“删除某点所有入边”会影响很多源点的出度,不能每次暴力扫描。
我们给每个点 u 一个固定的 64 位权值 w[u]。一条当前可用边 u -> v 对全局贡献 w[u]。于是全图当前贡献为:
current = sum(w[u] * 当前 u 的出度)如果每个点出度都恰好为 1,那么:
current = sum(w[u])这就是目标值 target。
为了快速处理“按终点删除/恢复所有入边”,再维护两个数组:
full_in_sum[v]:原图中所有终点为v的边贡献和;cur_in_sum[v]:当前仍可用、且终点为v的边贡献和。
操作就变成:
- 删除单边
u -> v:cur_in_sum[v] -= w[u],current -= w[u]; - 恢复单边
u -> v:对应加回; - 删除所有入边到
v:从current中减去cur_in_sum[v],再把它清零; - 恢复所有入边到
v:把cur_in_sum[v]恢复成full_in_sum[v],并补上差值。
每次操作后判断 current == target,即可得到答案。这里使用 64 位哈希,发生碰撞的概率极低;竞赛中这种随机哈希维护集合/计数状态是常见写法。
代码
// main.cpp:用源点随机权值的哈希和维护“每个点出度是否都为 1”。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500005;
int n, m, q;
unsigned long long value_of_node[MAXN];
unsigned long long full_in_sum[MAXN]; // 终点为 v 的所有原始边的源点权值和
unsigned long long cur_in_sum[MAXN]; // 终点为 v 的当前可用边的源点权值和
unsigned long long target_sum;
unsigned long long current_sum;
unsigned long long splitmix64(unsigned long long x) {
x += 0x9e3779b97f4a7c15ULL;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL;
x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL;
return x ^ (x >> 31);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
value_of_node[i] = splitmix64((unsigned long long)i);
target_sum += value_of_node[i];
}
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
full_in_sum[v] += value_of_node[u];
cur_in_sum[v] += value_of_node[u];
current_sum += value_of_node[u];
}
cin >> q;
while (q--) {
int type;
cin >> type;
if (type == 1) {
int u, v;
cin >> u >> v;
cur_in_sum[v] -= value_of_node[u];
current_sum -= value_of_node[u];
} else if (type == 2) {
int v;
cin >> v;
current_sum -= cur_in_sum[v];
cur_in_sum[v] = 0;
} else if (type == 3) {
int u, v;
cin >> u >> v;
cur_in_sum[v] += value_of_node[u];
current_sum += value_of_node[u];
} else {
int v;
cin >> v;
current_sum += full_in_sum[v] - cur_in_sum[v];
cur_in_sum[v] = full_in_sum[v];
}
if (current_sum == target_sum) {
cout << "YES\n";
} else {
cout << "NO\n";
}
}
return 0;
}复杂度
初始化需要读入所有边,复杂度
每次操作只做常数次加减,时间复杂度
总结
本题难点是把复杂题意压缩成“每个点当前出度恰好为 1”。真正的优化点在于:不要维护每个出度的精确修改过程,而是用源点权值的全局贡献和来代表所有出度状态。
当操作天然按“终点的所有入边”成组变化时,维护每个终点的当前入边贡献和,就能把批量修改降成一次加减。
