Luogu P8819 [CSP-S 2022] 星战 题解 [ 蓝 ] [ 基环树 ] [ 哈希 ]
星战:唐题,感觉 *2100 顶天了。
容易将题意转化为:维护一个数据结构,支持删除 / 添加有向图中以 \(u\) 为出点的所有边,删除 / 添加某一条单独的边,查询有向图是否为内向基环树森林。
考虑内向基环树森林的充分必要条件:
- 有向图中的边数为 \(\bm n\)。
- 有向图中每个节点的出度都是 \(1\)。
维护边数是简单的,修改的时候多记录几个值即可。考虑如何判断出度是否都为 \(1\),因为第一个条件已经保证了总边数为 \(n\),所以可以直接 Xor Hashing,给每个节点随机赋权,当前有向图的哈希值即为所有边的入点的哈希异或和。内向基环树森林的哈希值一定是所有点的哈希值的异或和。
该做法的正确性是基于恰好有 \(\bm n\) 条边的条件而言的,因为有了 \(n\) 条边,所以一旦有一个节点有两条出边,则一定对应着另一个节点一条出边都没有,此时该点不会被计入哈希值中,自然能被判断出来。
当同时满足边数为 \(n\)、哈希值相等时,说明是内向基环树森林。直接维护就能做到时间复杂度 \(O(n + m + q)\)。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
mt19937_64 rnd((unsigned) time(NULL));
ll rd(ll l, ll r)
{
return uniform_int_distribution<ll> (l, r) (rnd);
}
const int N = 500005;
const ll MXV = 1e18;
int n, m, q, nowe;
ll w[N], smhs, orihs[N], nowhs[N], cur, lste[N], orie[N];
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
w[i] = rd(1, MXV);
smhs ^= w[i];
}
for(int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
nowe++;
orihs[v] ^= w[u];
nowhs[v] ^= w[u];
lste[v]++;
orie[v]++;
cur ^= w[u];
}
cin >> q;
while(q--)
{
int op, u, v;
cin >> op;
if(op == 1)
{
cin >> u >> v;
nowhs[v] ^= w[u];
cur ^= w[u];
nowe--;
lste[v]--;
}
else if(op == 2)
{
cin >> u;
cur ^= nowhs[u];
nowhs[u] = 0;
nowe -= lste[u];
lste[u] = 0;
}
else if(op == 3)
{
cin >> u >> v;
nowhs[v] ^= w[u];
cur ^= w[u];
nowe++;
lste[v]++;
}
else
{
cin >> u;
cur ^= (nowhs[u] ^ orihs[u]);
nowhs[u] = orihs[u];
nowe += orie[u] - lste[u];
lste[u] = orie[u];
}
if(nowe == n && cur == smhs) cout << "YES\n";
else cout << "NO\n";
}
return 0;
}