
配点: 425 点 / 実行時間制限: 2 sec / メモリ制限: 1024 MB / Difficulty: 821 / NoviSteps: 1Q
問題概要
0
, 1
からなる空でない文字列 が次の条件を満たすとき、
を「美しい文字列」と呼ぶ。
- 次の一連の操作を
の長さが
になるまで行い、
に残った唯一の文字を
1
にすることができる。を満たす整数
を自由に選ぶ。
- 整数
を次のように定義する。
0
かつ0
である場合、とする。
0
かつ1
である場合、とする。
1
かつ0
である場合、とする。
1
かつ1
である場合、とする。
と
を取り除き、それらがあった場所に
を数字とみなしたものを
個挿入する。
0
, 1
からなる長さ の文字列
が与えられるので、
の連続部分文字列である美しい文字列の個数を求めよ。ただし、2つの連続部分文字列が文字列として同じでも、取り出す位置が異なるならば別々に数える。
制約
は整数
考察
「美しい文字列」となるための条件の言い換え
最後の状態である1
を頂点として、操作を逆から辿るような木構造を考えることもできるが、1文字から2通りの遷移が考えられるため、状態数は と爆発的に増えていってしまう。
このような状態遷移を伴うような操作に関わる問題は、操作の前後で変化しない「不変量」に注目することが有効な場合が多い。
最終的に0
を消したいので、0
の個数に注目すると、
00
1
: 2個減少01
0
: 変わらない10
0
: 変わらない11
1
: 変わらない
となるから、この問題の不変量は「0
の個数の偶奇」 ということになる。
美しい文字列は、最終的に0
の個数が偶数である1
となる文字列のことだから、 の空でない連続部分文字列
について、
が美しい文字列
に含まれる
0
の個数が偶数
という同値関係が成り立つ。
したがって、0
の個数が偶数となるような を数え上げればよい。
「美しい文字列」の数え上げ
の長さは最大
となるので、B問題のように全ての連続部分文字列を愚直に試すことはできない。
そこで、以下のような2次元 DP テーブルを考える。なお、 の
文字目から
文字目までの連続部分文字列を
] と表す
。
(末尾が
の
文字目である連続部分文字列) のうち、
0
を偶数個含むものの個数。のうち、
0
を奇数個含むものの個数。
なお、初期値は全て である。
遷移は、末尾が の
文字目である連続部分文字列は
- 長さ
の連続部分文字列
の末尾に
を追加したもの
のみからなる長さ
の文字列
の2種類からなることを踏まえると、以下のように書くことができる。
答えは、 で計算することができる。
計算量は となるので、本問の制約下では十分高速。
実装時は答えのオーバーフローに注意すること。
実装例
#include <bits/stdc++.h> using namespace std; #define rep(i, start, end) for (auto i = (start); (i) < (end); (i)++) using ll = long long; // ======================================== // int main() { int N; string T; cin >> N >> T; vector<vector<int>> dp(N + 1, vector<int>(2, 0)); rep(j, 0, N) { if (T[j] == '0') { dp[j + 1][0] = dp[j][1]; dp[j + 1][1] = dp[j][0] + 1; } else { dp[j + 1][0] = dp[j][0] + 1; dp[j + 1][1] = dp[j][1]; } } ll ans = 0; rep(j, 0, N + 1) { ans += dp[j][0]; } cout << ans << endl; return 0; }
実装時間: 40分
コメント
不変量に注目するのは鉄則本にも載っていたので、解法の引き出しに入れておきたい。
ちなみに、数え上げ部分を DP で行わないような方針もとれる。詳細は以下の記事を参照のこと。これは連続部分文字列の末尾を固定して先頭を動かしている。