遷移先を絞れる系の DP
問題概要
長さ の文字列 が与えられる。これを以下の条件を満たすように最小個数の区間に分割したい。最小個数を求めよ。
- どの区間についても、区間内の文字を適切に並び替えると回文になる
制約
考えたこと
まず「文字列 を並び替えると回文にできる」条件について考えておく。これは簡単で
- に登場する各文字について出現頻度を考えたとき、奇数個であるような文字がたかだか 1 個である
という風になる。よって、 を上手に区間分割して、各区間が上記の条件を満たすようにする問題といえる。最初、左からまたは右から Greedy にうまくできないかと考えていたけど、
- abpqrstuba
みたいなケースがある。これは結局すべての文字をバラバラにするしかないケースであって、Greedy にできるとは思えない。ちょっと Greedy な方向性で迷走しまくってしまった。
とりあえず O(N2) な DP に
とりあえず な DP はすぐに出る。こういう風に区間を分割していくタイプの DP はおなじみだ。
- dp[ i ] := 最初の i 文字について条件を満たすように区間分割する方法のうちの、区間の個数の最小値
とすれば
- chmin(dp[ i ], dp[ j ] + 1) (区間 [j, i) が条件を満たす場合のみ)
という感じの遷移で解くことができる。これで にはなる。この手の DP を高速化する手段は本当にめちゃくちゃたくさんある。今回は「遷移先が絞れる」というタイプの問題だった。
遷移先が 26 通りに限られる
具体的には、
- 各文字 c に対して、区間 [j, i) 内の文字 c の個数が奇数で他がすべて偶数となるような j
についてのみ調べればよいということになる。ただし、そのような j は一般には複数通り考えられることに注意する。そこで、
- memo[ bit ] := 今まで見てきた中での、各文字の頻度パリティベクトルが bit となるような場合についての最小値
といったものを用意することにする。
- 区間 [0, i) の頻度パリティベクトルを par とすると、区間 [j, i) 内の文字 c が奇数個で他がすべて偶数個になるような場合についての区間 [0, j) のパリティ par2 は自動的に決まるので、
- chmin(dp[ i ], memo[ par2 ] + 1) と更新すればよい
- さらにその結果をもとに chmin(memo[ par ], dp[ i ]) と更新しておく
#include <bits/stdc++.h> using namespace std; template<class T> inline bool chmax(T& a, T b) { if (a < b) { a = b; return 1; } return 0; } template<class T> inline bool chmin(T& a, T b) { if (a > b) { a = b; return 1; } return 0; } const int INF = 1<<29; string S; int rev(int bit, int i) { return bit ^ (1<<i); } long long solve() { int N = S.size(); vector<int> dp(N+1, INF); vector<int> mval(1<<26, INF); dp[0] = 0; mval[0] = 0; int par = 0; for (int i = 1; i <= N; ++i) { par = rev(par, S[i-1] - 'a'); chmin(dp[i], mval[par] + 1); for (int j = 0; j < 26; ++j) { par = rev(par, j); chmin(dp[i], mval[par] + 1); par = rev(par, j); } chmin(mval[par], dp[i]); } return dp[N]; } int main() { while (cin >> S) cout << solve() << endl; }