AOJ 1382 Black or White

解法

いかにも DP の高速化をする見た目をしている。
DP を考える前に、操作を観察する。
色々実験などして考えてみると、

  • ある位置を3回以上塗るのは最適ではなさそう
  • 2つの区間を違う色で塗る時、それらは互いに全く交わらないか、一方がもう一方を完全に内包している

ことがわかる。特に2つ目が本質である。
例えば [0, 5] を B で、[2, 7] を W で塗る操作を考えてみれば、これは [0, 1] を B でぬり、[2, 7] を W で塗る操作と同値である。

つまり、考える必要があるのは、ある区間を一色で塗ったときに、その区間を目的の色にするのに最小何手かかるか、ということである。
結論を言えば、B を W が交互にあらわれる(つまり B と W の境界)個数を2で割って切り上げたものが最小手数である。
例えば、WWWWWW を WBWBWB にしたいとする。仮に全体を B で塗って BBBBBB としよう。すると、ここから目的の色にするためには、独立して現れる W の数だけ塗るのが最適である。この数が、"WB" または "BW" となる位置の数(今回なら、5 箇所ある)を2で割って切り上げた数に等しい。

こうして、DPを考えるに必要な情報が揃った。
今、
dp[i] := i 文字目までみて最小手数はいくらか
sum[i] := i までみて、W と B の境界は何回現れるか
と定義する。sum は累積和で予め計算しておく。
すると遷移は

  • s[i - 1] == t[i - 1] のとき:dp[i] = dp[i - 1]
  • s[i - 1] != t[i - 1] のとき: dp[i] = min(dp[j] + ceil((sum[i] - sum[j + 1]) / 2) + 1) (i - k <= j < i)

となる。2つ目の遷移で +1 しているのは、注目区間を t[i - 1] 一色で塗る操作の分である。

この状態だと O(nk) なので、高速化する。2つ目の遷移式を整理すると

  • dp[i] = ceil(min((2 * dp[j] + sum[i] - sum[j + 1]) / 2) + 1) (i - k <= j < i)

となり、min の j に依存している部分は 2 * dp[j] - sum[j + 1] だけであるから、この部分だけ取り出して、適切なデータ構造 (deque や segment tree) で管理すると良い。

実装例は deque でスライド最小値しており、 O(n) である。

ソースコード

#include <bits/stdc++.h>
using namespace std;

using pii = pair<int, int>;

int main() {
    int n, k; cin >> n >> k;
    string s, t; cin >> s >> t;

    vector<int> sum(n + 1);
    for(int i = 0; i < n; ++i) {
        sum[i + 1] = sum[i] + (i == 0 || t[i - 1] != t[i]);
    }

    vector<int> dp(n + 1);
    deque<pii> deq;
    deq.emplace_back(0, -1);
    for(int i = 1; i <= n; ++i) {
        if(deq.front().first < i - k) deq.pop_front();
        if(s[i - 1] == t[i - 1]) {
            dp[i] = dp[i - 1];
        } else {
            dp[i] = (deq.front().second + sum[i] + 1) / 2 + 1;
        }
        if(i != n) {
            const int added = 2 * dp[i] - sum[i + 1];
            while(!deq.empty() && deq.back().second >= added) deq.pop_back();
            deq.emplace_back(i, added);
        }
    }

    cout << dp[n] << endl;
}

感想

これめっちゃ嘘が生えそうな形をしていて本番だと怖い。サンプルも強くないし。