[CSP-S 2022] 星战

GitHub跳转原题关系图返回列表

把每条可用边贡献为源点随机权值,维护全图哈希和判断是否所有点出度为 1。

OJ: luogu

题目 ID: P8819

难度:提高+/省选-

标签:图论哈希模拟

日期: 2026-07-06 08:46

题意

给定一张有向图,初始所有边都可用。每次操作会删除或恢复一条边,或者删除、恢复某个点的所有入边。每次操作后,需要判断当前是否是一个可以反攻的时刻。

题目中的两个条件合起来,可以转化为一个很简洁的图论条件:每个点当前恰好有一条可用出边

如果每个点出度都是 1,那么从任意点出发,每一步都能沿唯一出边继续走。图是有限的,所以一定会进入环,从而可以无限穿梭。反过来,如果某个点出度不是 1,它就不满足“恰好一条可用虫洞”的连续穿梭条件。

思路

小数据可以直接维护每条边是否可用,每次操作后重新统计所有点出度:

cpp
// 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]。于是全图当前贡献为:

text
current = sum(w[u] * 当前 u 的出度)

如果每个点出度都恰好为 1,那么:

text
current = sum(w[u])

这就是目标值 target

为了快速处理“按终点删除/恢复所有入边”,再维护两个数组:

  • full_in_sum[v]:原图中所有终点为 v 的边贡献和;
  • cur_in_sum[v]:当前仍可用、且终点为 v 的边贡献和。

操作就变成:

  • 删除单边 u -> vcur_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 位哈希,发生碰撞的概率极低;竞赛中这种随机哈希维护集合/计数状态是常见写法。

代码

cpp
// 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;
}

复杂度

初始化需要读入所有边,复杂度 O(n+m)O(n + m)

每次操作只做常数次加减,时间复杂度 O(1)O(1)。总时间复杂度为 O(n+m+q)O(n + m + q),空间复杂度为 O(n)O(n)

总结

本题难点是把复杂题意压缩成“每个点当前出度恰好为 1”。真正的优化点在于:不要维护每个出度的精确修改过程,而是用源点权值的全局贡献和来代表所有出度状态。

当操作天然按“终点的所有入边”成组变化时,维护每个终点的当前入边贡献和,就能把批量修改降成一次加减。