けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

AtCoder AGC 043 C - Giant Graph (900 点)

これはマジで天才やと思うやが...
いや本当にどこから Grundy 数を導けるのか、わからぬ...

問題へのリンク

問題概要

頂点数  N のグラフが 3 つ与えられる。それらのグラフのカルテシアン積を考える。この頂点数  N^{3} のカルテシアングラフの独立集合のうち、以下の値の最大値を求め、それを 998244353 で割ったあまりを求めよ。

  • 独立集合に含まれる各頂点 (a, b, c) (それぞれのグラフの頂点番号に相当, 1-indexed) について、重みとして  10^{18(a + b + c)} を加算する

制約

  •  2 \le N \le 10^{5}
  • 各グラフの辺数は  10^{5} 以下

考えたこと

まずグラフが 1 個の場合を考えてみると、単純に、頂点番号が大きい順に Greedy にとっていけばよいことがわかる。その際に

  • 今見ている頂点から行ける頂点 (番号が大きい方向へ) のうち、
    • 1 つでも採用済みの頂点があったら採用できない
    • すべて採用していない頂点であれば採用する

という風にすれば OK。そしてこの営みが、なんと Game DP にそっくりだというのだ。言われてみれば確かに...

  • 「採用済み」を「負け」
  • 「採用しない」を「勝ち」

と言い換えると、ピッタリ対応する手続きになっている。そしてカルテシアングラフ上のこの手続きは、「3 つの山に関する Nim」に対応するという。

Grundy

というわけで、それぞれのグラフのそれぞれの頂点について、「採用する」を 0 として、Grundy 数を求めてあげる。

そうすると、頂点 (a, b, c) が採用されるかどうかは、Grundy 数の XOR 和が 0 かどうかで判定できるのだ。

...どうしたらこんな発想が生まれるのかわからないけど、とりあえず「頂点 (a, b, c) が採用されるための必要十分条件をじっくり考察する」というのをやるといいのかもしれない。そうするともしかしたら自然に Grundy 数と似ていることに思い至るか、Grundy 数と同等の概念を自力で再発明できるのかもしれない。

Grundy 数は小さい

そしてさらに、Grundy 数は辺数を  M として、 O(\sqrt{M}) 程度の大きさにしかならないという。それはそうだと思う。よってこの問題は、

  • 3 つのグラフそれぞれについて、各頂点の Grundy 数を求め
  • それを Grundy 数の値ごとに集計した配列を作る (そのサイズは  O(\sqrt{M}) 程度で十分)
  • 3 つのうちの 2 つについて全探索して、計算量は  O(M)

というわけだ。

#include <bits/stdc++.h>
using namespace std;

template<int MOD> struct Fp {
    long long val;
    constexpr Fp(long long v = 0) noexcept : val(v % MOD) {
        if (val < 0) val += MOD;
    }
    constexpr int getmod() { return MOD; }
    constexpr Fp operator - () const noexcept {
        return val ? MOD - val : 0;
    }
    constexpr Fp operator + (const Fp& r) const noexcept { return Fp(*this) += r; }
    constexpr Fp operator - (const Fp& r) const noexcept { return Fp(*this) -= r; }
    constexpr Fp operator * (const Fp& r) const noexcept { return Fp(*this) *= r; }
    constexpr Fp operator / (const Fp& r) const noexcept { return Fp(*this) /= r; }
    constexpr Fp& operator += (const Fp& r) noexcept {
        val += r.val;
        if (val >= MOD) val -= MOD;
        return *this;
    }
    constexpr Fp& operator -= (const Fp& r) noexcept {
        val -= r.val;
        if (val < 0) val += MOD;
        return *this;
    }
    constexpr Fp& operator *= (const Fp& r) noexcept {
        val = val * r.val % MOD;
        return *this;
    }
    constexpr Fp& operator /= (const Fp& r) noexcept {
        long long a = r.val, b = MOD, u = 1, v = 0;
        while (b) {
            long long t = a / b;
            a -= t * b; swap(a, b);
            u -= t * v; swap(u, v);
        }
        val = val * u % MOD;
        if (val < 0) val += MOD;
        return *this;
    }
    constexpr bool operator == (const Fp& r) const noexcept {
        return this->val == r.val;
    }
    constexpr bool operator != (const Fp& r) const noexcept {
        return this->val != r.val;
    }
    friend constexpr ostream& operator << (ostream &os, const Fp<MOD>& x) noexcept {
        return os << x.val;
    }
    friend constexpr Fp<MOD> modpow(const Fp<MOD> &a, long long n) noexcept {
        if (n == 0) return 1;
        auto t = modpow(a, n / 2);
        t = t * t;
        if (n & 1) t = t * a;
        return t;
    }
};

const int MOD = 998244353;
using mint = Fp<MOD>;

using Graph = vector<vector<int>>;
int N;
vector<int> M;
vector<Graph> G;
vector<mint> ten;

void pre() {
    int MAX = 510000;
    ten.assign(MAX, 1);
    mint fac = modpow(mint(10), 18);
    for (int i = 1; i <= MAX; ++i) ten[i] = ten[i-1] * fac;
}

mint solve() {
    vector<vector<int>> grundy(3, vector<int>(N, -1));
    vector<vector<mint>> memo(3, vector<mint>(1000, 0));
    for (int iter = 0; iter < 3; ++iter) {
        for (int v = N-1; v >= 0; --v) {
            set<int> gse;
            for (auto e : G[iter][v]) gse.insert(grundy[iter][e]);
            int gres = 0;
            while (gse.count(gres)) ++gres;
            grundy[iter][v] = gres;
            memo[iter][gres] += ten[v+1];
        }
    }
    mint res = 0;
    for (int a = 0; a < 1000; ++a) {
        if (memo[0][a] == 0) break;
        for (int b = 0; b < 1000; ++b) {
            if (memo[1][b] == 0) break;
            int c = a^b;
            res += memo[0][a] * memo[1][b] * memo[2][c];
        }
    }
    return res;
}

int main() {
    pre();
    cin >> N;
    M.assign(3, 0);
    G.assign(3, Graph(N, vector<int>()));
    for (int iter = 0; iter < 3; ++iter) {
        cin >> M[iter];
        for (int i = 0; i < M[iter]; ++i) {
            int a, b; cin >> a >> b; --a, --b;
            G[iter][a].push_back(b);
            G[iter][b].push_back(a);
        }
    }
    cout << solve() << endl;
}