G - Substring Game Editorial
by
shinchan
UnionFindを用いた実装
解説
実装例
まずは実装例をご覧ください。他プログラムと比べて非常にシンプルで、かつ高速なプログラムとなりました。
実際、少し修正することで shortest を大幅に更新できました。
https://atcoder.jp/contests/abc433/submissions/76408628 (shortest)
#include <bits/stdc++.h>
using namespace std;
#include<atcoder/all>
using namespace atcoder;
int f[1 << 20];
int main() {
int t; cin >> t;
while(t--) {
string s; cin >> s;
auto sa = suffix_array(s);
auto lc = lcp_array(s, sa);
int n = s.size();
for(int i = 0; i < n; i ++) f[i] = (n - sa[i]) & 1;
dsu uf(n);
vector<vector<int>> task(n);
for(int i = 0; i < n - 1; i ++) task[lc[i]].push_back(i);
for(int i = n - 1; i >= 0; i --) {
for(int j : task[i]) {
int x = uf.leader(j), y = uf.leader(j + 1);
if(i & 1) f[x] &= f[y];
else f[x] |= f[y];
f[y] = f[x];
uf.merge(x, y);
}
}
cout << (f[uf.leader(0)] ? "Alice" : "Bob") << endl;
}
}
概要
部分文字列全体を頂点とする Trie を考えると、このゲームは Trie 上のゲームになります。しかし、部分文字列は \(O(N^2)\) 個存在するため、そのまま Trie を構築することはできません。
そこで、Trie を陽に作る代わりに Suffix Array と LCP を用いて暗黙的に処理します。
Suffix Array 上では、ある文字列を prefix に持つ suffix は必ず連続区間になります。また、その区間は LCP の値を大きい方から見ていくことで順に構築できます。
初期状態
各 suffix を単独で考えると、対応する Trie は一本道になります。
一本道では葉まで残っている手数の偶奇だけで勝敗が決まるため、各 suffix の勝敗はその suffix の長さの偶奇だけで初期化できます。
LCP の大きい順に処理する
LCP が \(x\) の隣接する 2 つの suffix は、長さ \(x\) の prefix を共有しています。
そこで、LCP の値ごとに対応する隣接ペアをまとめておき、LCP の大きい方から順に処理します。
ある長さで複数の suffix が同じ prefix を共有する場合、それらを 1 つの連結成分として扱います。連結成分の勝敗は、ゲーム DP の遷移に従って統合します。
深さ(現在見ている文字列長)の偶奇によって遷移は
- 偶数長なら OR
- 奇数長なら AND
となるため、その規則に従って連結成分の勝敗を更新していきます。
すべての長さについて処理を終えると、空文字列に対応する連結成分が得られ、その勝敗が答えになります。
UnionFind の適用
UnionFind を用いれば、「現在属している連結成分を取得する」「2 つの連結成分を結合する」という操作だけで区間管理ができます。勝敗も各連結成分の代表に持たせればよいため、実装を非常に簡潔にできます。
このような 区間や集合を順次マージしていく問題では UnionFind が非常に有効です。
計算量
Suffix Array と LCP の構築が \(O(N)\)、UnionFind の操作がならし \(O(N\alpha(N))\) なので、全体の計算量は
\[ O(N\alpha(N)) \]
となります。
posted:
last update:
