给每个据点一个 64 位权值,用全图可用虫洞的权值和维护出度状态,判断是否所有据点出度恰好为 1。
OJ: luogu
题目 ID: P8819
难度:提高+/省选-
标签:图论哈希模拟
创建: 2026-07-06 08:46
更新: 2026-10-07 10:45
形式化题目
给定一张
依次执行
:摧毁虫洞 ; :摧毁据点 的全部虫洞(即所有终点为 的虫洞); :修复虫洞 ; :修复据点 的全部虫洞。
每次指令执行后,判断当前时刻能否反攻。
反攻要求两件事同时成立:
- 实现反击:从任一据点出发都能无限穿梭(可重复经过同一据点或虫洞);
- 实现连续穿梭:每个据点恰好只有一条可用虫洞从它出发。
条件 2 明确给出“每个据点的可用出度恰好为
因此判定条件可以压缩成一句话:
暴力解法
思路
用二维数组记录每条虫洞是否存在、是否可用,每次指令后扫描所有据点统计出度,判断是否全部等于
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 22:28
* update_at: 2026-10-01 22:28
*/
// brute.cpp:小数据暴力解,逐条维护虫洞是否可用,每次指令后重新统计各据点出度。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 35; // 暴力只服务小数据,n 取到 30 左右
int n, m, q;
bool exist_edge[MAXN][MAXN]; // exist_edge[u][v]:原图是否存在虫洞 u -> v
bool active_edge[MAXN][MAXN]; // active_edge[u][v]:虫洞 u -> v 当前是否可用
// 反攻时刻要求每个据点恰好有一条可用出边。
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) {
// 摧毁虫洞 u -> v。
int u, v;
cin >> u >> v;
active_edge[u][v] = false;
} else if (type == 2) {
// 摧毁据点 v 的全部虫洞,即终点为 v 的那些。
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) {
// 修复虫洞 u -> v。
int u, v;
cin >> u >> v;
if (exist_edge[u][v]) {
active_edge[u][v] = true;
}
} else {
// 修复据点 v 的全部虫洞。
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;
}复杂度
每次指令后重新统计出度是
瓶颈
所以必须换一种表示:不再记录每个据点出度的精确数值,而是用一个全局聚合量间接判断“是否所有出度都等于
正解
思路
用权值和代表出度向量
给每个据点
其中
如果所有出度都恰好为
反过来,若某个据点出度不是
是一个非零整数线性组合。只要
代码用 splitmix64 把编号打散成权值,并刻意选用 unsigned long long:哈希依赖
为什么还要维护每终点的入边和
current_sum,就没法一次把这批虫洞的贡献扣掉。于是再维护两个数组:
:原图中所有终点为 的虫洞的源点权值和; :当前可用、终点为 的虫洞的源点权值和。
四条指令就都变成常数次加减:
| 指令 | 对 current_sum 的改动 |
对 cur_in_sum 的改动 |
|---|---|---|
每次指令后只需判断
与代码的对应
| 行内变量 | 含义 |
|---|---|
value_of_node[u] |
据点 |
full_in_sum[v] |
|
cur_in_sum[v] |
|
target_sum |
|
current_sum |
当前全图贡献和 |
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 22:28
* update_at: 2026-10-01 22:28
*/
// main.cpp:给每个据点一个 64 位权值,用全图可用边的权值和判断是否所有据点出度恰好为 1。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 500005; // n <= 5e5
int n, m, q;
// 权值数组用 unsigned long long,因为哈希要依赖 2^64 的自然回绕做模运算。
// 这里刻意选无符号:有符号溢出是未定义行为,无法当作取模。
unsigned long long value_of_node[MAXN]; // value_of_node[u]:据点 u 的固定权值
unsigned long long full_in_sum[MAXN]; // full_in_sum[v]:原图中所有终点为 v 的虫洞的源点权值和
unsigned long long cur_in_sum[MAXN]; // cur_in_sum[v]:当前可用、终点为 v 的虫洞的源点权值和
unsigned long long target_sum; // 所有据点出度都恰好为 1 时的全图贡献和
unsigned long long current_sum; // 当前可用虫洞的全图贡献和
// splitmix64:把一个整数打散成 64 位权值,只是为了让不同据点的权值互不相关。
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 (ll i = 1; i <= n; i++) {
value_of_node[i] = splitmix64(i);
target_sum += value_of_node[i];
}
// 一条可用虫洞 u -> v 贡献 value_of_node[u],于是
// current_sum = sum( value_of_node[u] * 据点 u 当前的出度 )。
for (ll i = 1; i <= m; i++) {
ll 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--) {
ll type;
cin >> type;
if (type == 1) {
// 摧毁虫洞 u -> v,它对全图贡献 value_of_node[u]。
ll u, v;
cin >> u >> v;
cur_in_sum[v] -= value_of_node[u];
current_sum -= value_of_node[u];
} else if (type == 2) {
// 摧毁据点 v 的全部虫洞,也就是终点为 v 的所有可用虫洞。
ll v;
cin >> v;
current_sum -= cur_in_sum[v];
cur_in_sum[v] = 0;
} else if (type == 3) {
// 修复虫洞 u -> v。
ll u, v;
cin >> u >> v;
cur_in_sum[v] += value_of_node[u];
current_sum += value_of_node[u];
} else {
// 修复据点 v 的全部虫洞,恢复成原图中终点为 v 的那批。
ll 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;
}复杂度
- 时间复杂度:初始化
,每次指令 ,总计 。 - 空间复杂度:
,只存三个权值数组和几个全局量。
总结
这题的题意可以先压缩成一句话:每个据点当前可用出度恰好为
优化的真正难点不在判定,而在于
- 全局
current_sum代表; - 每终点的
cur_in_sum[v]让批量修改降成一次加减。
这类“用随机权值把集合/计数状态压成哈希和”的技巧,在需要支持批量增删、只问整体状态是否符合某个模式的题目里非常通用。代价是答案带极小的碰撞概率,属于随机化算法。
本文的 main.cpp 已用逐条统计出度的 brute.cpp 对拍
