ABC341E - Alternating Stringを遅延セグ木で解く
過去問精進している際にハマったので整理します。
誤植や誤りを見つけた場合、コメント欄などで指摘していただけるとありがたいです。
想定する読者は
- 自分
- ACLの遅延セグ木の仕組みを完全理解しているわけじゃないが、チートシートなどを見ながらだと遅延セグ木問題が解けたりもする。
問題:
問題概要
01010101
や10101010101
といった、0と1からなる文字列で隣り合う2文字が異なっているものを良い文字列という。L文字目からR文字目までの01を反転させたり、L文字目からR文字目までを文字列として見たときに良い文字列かどうかを問うクエリをいっぱい出すから高速に処理してね。
最初に考えた解法(WA)
-
良い文字列かどうかという判定は、文字列の奇数文字目を01反転させた後は、「1種類の文字からなる文字列かどうか」という判定になって、考えやすくなる。(ので、そうする。) [1]
-
1つ目のクエリの区間に対して01を反転させる処理は、区間に対してxorで1を足す操作だと考えられる
-
2つ目のクエリの区間が良い文字列かどうかを判定する処理は、区間の和が0もしくは(R-L+1)かどうかで判定できる
-
じゃあ遅延セグメント木でいけるじゃん!
Sは整数、fは0か1の整数として、opは普通に整数の和で、mappingは整数に整数のxorを取ったものを返せばいいし、compositionもf^gでいいな。idはxorの単位元と一緒で0を返せばいいね。
ll sum (ll a, ll b) {
return a + b;
}
ll e() {
return 0;
}
ll mapping(ll f, ll x) {
assert(0 <= f && f<=1);
return x ^ f;
}
ll composition(ll f, ll g) {
assert(0 <= f && f<=1);
assert(0 <= g && g<=1);
return f^g;
}
ll id() {
return 0;
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
ll N, Q; cin >> N >> Q;
string S; cin >> S;
vector<ll>A(N);
for(int i=0; i<N; i++) {
A[i] = S[i] - '0';
if (i%2 == 1) A[i] ^= 1;
}
atcoder::lazy_segtree<ll, sum, e, ll, mapping, composition, id> seg(A);
for(int q=0; q<Q; q++) {
int qi, L, R; cin >> qi >> L >> R;
L--;
if (qi == 1) {
seg.apply(L, R, 1);
} else if (qi == 2) {
ll sum = seg.prod(L, R);
if (sum == R - L || sum == 0) cout << "Yes" << endl;
else cout << "No" << endl;
}
}
return 0;
}
として、サンプルもあったし、提出w! → WA[2]
上の解法のどこがダメだったのか
ほんとうは遅延セグ木の仕組みを完全理解したうえで図解とかしながら書いていきたいんですが、理解していないために、遅延セグ木の仕組みを理解していなくてもわかることだけ書きます。
F はACLの遅延セグメント木が求める性質を満たしていない
上のACLの遅延セグメント木が求める性質というのは、ドキュメント[3] に記載されている以下の条件のことです。
-
がモノイドであり、S はF からS への写像の集合であるS -
は恒等写像F を含む。id -
は写像の合成について閉じている。F - 任意の
に対して、f \in F, x, y \in S を満たすf(x \cdot y) = f(x) \cdot f(y)
先程の解法はこれらを満たしているでしょうか?確認していきましょう。
S がモノイドであり、F はS からS への写像の集合である
F は恒等写像id を含む。
F は写像の合成について閉じている。
これも満たしていそうです。
f \in F, x, y \in S に対して、f(x \cdot y) = f(x) \cdot f(y) を満たす
任意のこれはパッとはわからないですね。計算してみましょう。先ほども書いた通り、
右辺は、
左辺は、
これはすべての
この条件は遅延セグメント木の鍵とも言える条件なので、これが成立しない以上上の解法は間違っているということがわかりました。
撃墜ケース
1つ目の提出は以下のようなケースで撃墜できます。
4 2
0010
1 3 4
2 1 4
正解: No
ACできる解法
解説などを見ると遅延でないセグメント木の解法が紹介されていますが、遅延セグメント木でもACできます。
WAとなった解法では
struct S {
int sum, size;
};
S mapping(F f, S x) {
if (f == 0) return f;
else return {x.size - x.sum, x.size};
}
S op (S a, S b){
return {a.sum + b.sum, a.size + b.size};
}
などすると、上の解法では満たされていなかった条件が満たされることになりますね。
一応、「任意の
右辺は
左辺は
なんだかんだでこの実装をすると、ACできました[5]。
おまけ: ランダムケースをするコードと愚直解のコード
import random
N = random.randint(4, 200)
Q = random.randint(1, 100)
print(N, Q)
S = ""
for _ in range(N):
S += str(random.randint(0,1))
print(S)
for i in range(Q):
L = random.randint(1,N)
R = random.randint(L,N)
print(random.randint(1,2), L, R)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned int uint;
template<class T> inline bool chmax(T& a, const T& b) {if (a<b) {a=b; return true;} return false;}
template<class T> inline bool chmin(T& a, const T& b) {if (b<a) {a=b; return true;} return false;}
const int INTINF = 1000001000;
const int INTMAX = 2147483647;
const ll LLMAX = 9223372036854775807;
const ll LLINF = 1000000000000000000;
// [l, r) が良い数列か?
//
bool is_good(const vector<int>& S, int L, int R) {
bool ret = true;
for (int i=L; i+1<=R-1; i++) {
if (S[i] == S[i+1]) {
ret = false;
break;
}
}
return ret;
}
// TLELTLELETLELTLELTELT
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
ll N, Q; cin >> N >> Q;
string S; cin >> S;
vector<int>A(N);
for(int i=0; i<N; i++) {
A[i] = S[i] - '0';
}
for(int q=0; q<Q; q++) {
int qi, L, R; cin >> qi >> L >> R;
L--;
if (qi == 1) {
for (int i=L; i<R; i++) A[i] ^= 1;
} else if (qi == 2) {
bool res = is_good(A,L,R);
if (res) cout << "Yes" << endl;
else cout << "No" << endl;
}
}
return 0;
}
online-judge-tools[6]を導入している場合、
oj g/i "python3 generate.py"
g++ tle.cpp -o tle
oj g/o -c "./tle"
oj t
などすると、1行目でランダムケースを自動生成し、
3行目で愚直解で想定される出力を生成し、4行目でランダムケースに対してテストを実行することができます。
実際、200ケースでテストを回してみたところしっかり落ちるケースが見つかりました。
-
この考え方はこの問題より後のコンテストの問題 https://atcoder.jp/contests/abc346/tasks/abc346_d の解説放送で覚えたもの ↩︎
-
提出したコード: https://atcoder.jp/contests/abc341/submissions/52707947 ↩︎
-
参照: https://atcoder.github.io/ac-library/master/document_ja/lazysegtree.html ↩︎
-
逆に、これを満たすような
はない気がします。(x,y 証明は考えてませんがプログラムで探索した限り見つかりません)→ の偶奇で場合分けして4通りについて計算すれば確かめられますね。 ↩︎x,y -
提出したコード: https://atcoder.jp/contests/abc341/submissions/52725254 ↩︎
-
参照: https://github.com/online-judge-tools/oj/blob/master/docs/getting-started.ja.md ↩︎
Discussion