鹿島建設プログラミングコンテスト2020 (AtCoder Regular Contest 110 - ARC110) の E - Shorten ABC の解法です.
経緯
解説AC です.公式解説も皆さんの解説もたくさん参考にしています.
問題概要
A, B, C からなる,長さ N の文字列がある. 隣り合った異なる文字を,どちらとも違う文字で置き換える操作ができる. 操作を0回以上行ってできる文字列の数を mod $10^9 + 7$ で求めよ.
制約: $N \leq 10^6$
解法
Python 流に,文字列 s と t の連結を s+t で, 文字列sの位置p(含む)から位置q(含まない)の部分文字列をs[p:q]で表す. 文字列長1の文字列と文字を同一視する.
与えられた文字列が1種類の文字からなるときは,答は 1 である. 以下,そうでないとする.
A,B,Cからなる文字列 s に対する e(s) を定義する.
- e(“A”) = 1
- e(“B”) = 2
- e(“C”) = 3
- e(s + c) = e(s) XOR e(c) (len(c) = 1)
次が成り立つ.
- 文字列 s を文字 c に変換できるための必要十分条件は,以下の
両方が成り立つことである.
- e(s) = e(c)
- len(s) = 1 であるか,または,s が2種類以上の文字を含む.
帰納法で簡単に証明できる.次も成り立つ.
- 長さ a の文字列 s が2種類以上の文字を含むとする. s を長さ b の文字列 t に変換できる必要十分条件は, $0 = p_0 < p_1 < … < p_{b} = a$ なる列がとれて, $e(s[p_i:p_{i+1}]) = e(t[i])$ が成り立つことである.
必要性は明らかなので,十分性を示す. $s[p_i:p_{i+1}]$ が1種類の文字 X しか含まない場合が問題となる. $p_{i+1} - p_i$ は奇数で,t[i] = X である. このブロックより右か左に X 以外の文字が現れるところがある. どちらでも同じなので,右に現れるとしよう. $s[p_j:p_{j+1}]$ に X 以外の文字が現れ,$i < k < j$ なる $s[p_k:p_{k+1}] $ には,X しか現れないとする. この場合,$p_{k+1} - p_k$ は奇数で,t[k] = X である. そこで,切り方を変えて,$s[p_i], \ldots, s[p_i + (j - i - 1)]$ の各 1文字を t[i], …, t[j-1] に対応させ, $s[p_i + (j-i-1):p_{j+1}]$ を t[j] に対応させる.t[j] に対応させる文字列は,元の対応文字列に 偶数個の X を追加したものであるから,XOR の値は変わらない. したがって,t に変換することが可能である.(終)
s を与えられた文字列とする. 上で示したことから,s から変換可能な長さ b の文字列 t に対して, 「左から貪欲に切っていく分割」を対応させることができる.正確には, $0 = p_0 < p_1 < … < p_{b} \leq N$ であって,以下を満たすものを対応させる.
- $e(s[p_i:p_{i+1}]) = e(t[i]) \qquad (i = 0, \ldots, b-1)$
- $e(s[p_i:q] \neq e(t[i]) \qquad (i = 0, …, b-1; p_i < q < p_{i+1})$
- $e(s[p_b:N]) = 0$
このような分割を数えれば良い.$p_b = i$ となるような分割の数 dp[i] を,配るDPを用いて数えよう. t = 0, 1, 2, 3 と,$0 \leq i < N$ に対して,
- $\textrm{nxt}(i, t) := \min \{ j \mid e(s[p:j]) = t, i < j \}$
とするとき,t = 1, 2, 3 に対して,
dp[nxt(i, t)] += dp[i]
なる計算を行えば良い.求める答は, $\sum \{ dp[i] \mid 1 \leq i \leq N; e(s[i:N]) = 0 \}$ である.
$e(s[i:N]) = 0$ を満たす i は,後ろから順に $O(N)$ で決定できる. nxt(i, t) も,t = 0 も含めて後ろから順に,
nxt(i, e(s[i])) = i + 1
nxt(i, (t XOR e(s[i])) % 4) = nxt(i + 1, t)
と計算すれば,やはり $O(N)$ で求められる. 全体として $O(N)$ で問題を解くことができた.
ACコード
Fp は,mod $10^9 + 7$ を取るためのクラスです.
int main(/* int argc, char *argv[] */) {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout << setprecision(20);
ll N; cin >> N;
string S; cin >> S;
if (all_of(S.begin(), S.end(), [&](char t) -> bool {return t == S[0];})) {
cout << 1 << endl;
return 0;
}
vector<ll> E(N);
for (ll i = 0; i < N; i++) E[i] = S[i] - 'A' + 1;
vector nxt(4, vector<ll>(N + 1));
for (ll t = 0; t < 4; t++) nxt[t][N] = N + 1;
for (ll i = N-1; i >= 0; i--) {
nxt[E[i]][i] = i + 1;
for (ll t = 1; t < 4; t++) nxt[E[i] ^ t][i] = nxt[t][i + 1];
}
vector<Fp> tbl(N + 2);
tbl[0] = 1;
for (ll i = 0; i < N; i++) {
for (ll t = 1; t < 4; t++) tbl[nxt[t][i]] += tbl[i];
}
vector<ll> accE(N + 1);
for (ll i = N-1; i >= 0; i--) accE[i] = E[i] ^ accE[i + 1];
Fp ans = 0;
for (ll i = 1; i <= N; i++) if (accE[i] == 0) ans += tbl[i];
cout << ans << endl;
return 0;
}