Official

F - Reordering Editorial by penguinman


\(S\) の空でない、連続した部分列 \(S'\) を並び替えて得られる文字列の種類数は、\(S'\) における各アルファベットの出現数のみに依存します。

そのためこの問題を動的計画法に落とし込む際には

  • 何番目のアルファベットまでを使ったか
  • 合計で何文字を使ったか

\(2\) つを状態に持つことが重要であり、故に以下のような動的計画法を考えることが有効です。

  • \(\text{dp}_{i,j}:=(\)アルファベットの昇順で \(i\) 番目のアルファベットまでを使い、使った文字数の合計が \(j\) であるような \(S\) の部分列、およびその並び替えの個数\()\)

そしてその遷移は、\(S\) における a から z までのアルファベットの出現数をそれぞれ \(\text{freq}_1,\text{freq}_2,\ldots,\text{freq}_{26}\) として以下のようになります。

  • \(\text{dp}_{i,j} = \sum_{k=0}^{\min(j,\text{freq}_i)} \text{dp}_{i-1,j-k} \times \binom{j}{k}\)

計算量は一見すると \(σ=26\), \(N\)\(S\) の長さとして \(O(σN^2)\) かかっていそうですが、\(\text{dp}_{i,j}\) への遷移にかかる計算量が高々 \(O(\text{freq}_i)\) であることを踏まえると合計での計算量は高々 \(O(\sum_{i=1}^{26} \text{freq}_i \times N)=O((\sum_{i=1}^{26} \text{freq}_i) \times N)=O(N \times N)=O(N^2)\) となります。

求める答えは \(\sum_{i=1}^{N} \text{dp}_{26,i}\) です。

実装例 (C++)

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

using ll = long long;

const ll MAX = 5010, MOD = 998244353;

vector<ll> fac,finv,inv;

void binom_init() {
    fac.resize(MAX);
    finv.resize(MAX);
    inv.resize(MAX);
    fac[0] = fac[1] = 1;
    inv[1] = 1;
    finv[0] = finv[1] = 1;
    for(int i=2; i<MAX; i++){
        fac[i] = fac[i-1]*i%MOD;
        inv[i] = MOD-MOD/i*inv[MOD%i]%MOD;
        finv[i] = finv[i-1]*inv[i]%MOD;
    }
}

ll binom(ll n, ll r){
    if(n<r || n<0 || r<0) return 0;
    return fac[n]*finv[r]%MOD*finv[n-r]%MOD;
}

int main(){
    binom_init();
    string S; cin >> S;
    int N = S.size();
    vector<int> freq(26);
    for(auto el: S) freq[el-'a']++;
    vector<vector<ll>> dp(27,vector<ll>(N+1));
    dp[0][0] = 1;
    for(int i=0; i<26; i++){
        for(int j=0; j<=N; j++){
            for(int k=0; k<=min(j,freq[i]); k++){
                dp[i+1][j] += dp[i][j-k]*binom(j,k);
                dp[i+1][j] %= MOD;
            }
        }
    }
    ll ans = 0;
    for(int i=1; i<=N; i++){
        ans += dp[26][i];
        ans %= MOD;
    }
    cout << ans << endl;
}

posted:
last update: