D. a-Good String

Codeforces Round #656 (Div. 3)

https://codeforces.com/contest/1385/problem/D

問題

長さ 2^kの文字列 Sが与えられるので、 i番目の文字を任意の文字に置き換える操作を何度か行うことでa-goodな文字列にできる最小の操作回数を求める。
ここで、c-goodであるとは次の条件の内どれかを満たす事である。

  • 長さ1の文字列
  • 前半部が文字cのみで構成され、後半部は(c + 1)-goodである。
  • 後半部が文字cのみで構成され、前半部は(c + 1)-goodである。

解法

c-goodの条件を眺めると再帰的定義であることに気がつく。
これは、半分をaに統一すると、もう半分はb-goodにならなければならない。今度は、半分の半分をbに統一するともう半分は、c-goodに....という風に1文字になって分割できなくなるときまで続くことからわかる。
次に制約を眺めてみると再帰木の葉の総数は高々 10^5程度であることがわかるので、全探索をすれば良い事がわかる。

実装についてだが、葉に達したときにansをcostで更新するようにした。
再帰関数を呼び出すときは、左半分を文字cで統一したときにかかるコストをdiff1として、右半分に対して潜るような実装をしている。右半分を統一したときはその逆である。
また、文字cで統一したときのコストは、累積和を計算しておき、(区間の長さ)から(区間に存在するcの総数)の差をとることで高速に求めることが出来る。
今回、区間の分割の仕方は、(左半分) = [left, mid], (右半分) = [mid+1, right]のようにした。また、累積和は、左端が閉じていて、右端が開いているという実装にした。

コード

https://codeforces.com/contest/1385/submission/94952181

int n;
string s;
vector<vector<int>> c;
int ans = INF;

void rec(char x, int left, int right, int cost) {
    if (left == right) {
        chmin(ans, cost + (s[left] != x));
        return;
    }

    int mid = left + (right - left) / 2;
    
    int diff1 = (mid - left + 1) - (c[x - 'a'][mid + 1] - c[x - 'a'][left]);
    int diff2 = (right - mid) - (c[x - 'a'][right + 1] - c[x - 'a'][mid + 1]);

    
    rec(x+1, left, mid, cost + diff2);    
    rec(x+1, mid + 1, right, cost + diff1);
}



void solve() {
    cin >> n >> s;

    ans = INF;
    c = vector<vector<int>>(26, vector<int>(n+1));

    rep(i, n) {
        int t = s[i] - 'a';
        c[t][i + 1]++;
    }
    rep(i, 26) {
        rep(j, n) c[i][j + 1] += c[i][j];
    }

    rec('a', 0, n - 1, 0);

    cout << ans << endl;
}

int main() {
    int t;
    cin >> t;
    rep(i, t) solve();
}