これはマジで天才やと思うやが...
いや本当にどこから Grundy 数を導けるのか、わからぬ...
問題概要
頂点数 のグラフが 3 つ与えられる。それらのグラフのカルテシアン積を考える。この頂点数 のカルテシアングラフの独立集合のうち、以下の値の最大値を求め、それを 998244353 で割ったあまりを求めよ。
- 独立集合に含まれる各頂点 (a, b, c) (それぞれのグラフの頂点番号に相当, 1-indexed) について、重みとして を加算する
制約
- 各グラフの辺数は 以下
考えたこと
まずグラフが 1 個の場合を考えてみると、単純に、頂点番号が大きい順に Greedy にとっていけばよいことがわかる。その際に
- 今見ている頂点から行ける頂点 (番号が大きい方向へ) のうち、
- 1 つでも採用済みの頂点があったら採用できない
- すべて採用していない頂点であれば採用する
という風にすれば OK。そしてこの営みが、なんと Game DP にそっくりだというのだ。言われてみれば確かに...
- 「採用済み」を「負け」
- 「採用しない」を「勝ち」
と言い換えると、ピッタリ対応する手続きになっている。そしてカルテシアングラフ上のこの手続きは、「3 つの山に関する Nim」に対応するという。
Grundy 数
というわけで、それぞれのグラフのそれぞれの頂点について、「採用する」を 0 として、Grundy 数を求めてあげる。
そうすると、頂点 (a, b, c) が採用されるかどうかは、Grundy 数の XOR 和が 0 かどうかで判定できるのだ。
...どうしたらこんな発想が生まれるのかわからないけど、とりあえず「頂点 (a, b, c) が採用されるための必要十分条件をじっくり考察する」というのをやるといいのかもしれない。そうするともしかしたら自然に Grundy 数と似ていることに思い至るか、Grundy 数と同等の概念を自力で再発明できるのかもしれない。
Grundy 数は小さい
そしてさらに、Grundy 数は辺数を として、 程度の大きさにしかならないという。それはそうだと思う。よってこの問題は、
- 3 つのグラフそれぞれについて、各頂点の Grundy 数を求め
- それを Grundy 数の値ごとに集計した配列を作る (そのサイズは 程度で十分)
- 3 つのうちの 2 つについて全探索して、計算量は
というわけだ。
#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; }