問題

TopCoder Statistics - Problem Statement

解法

暗号化する前の文字列を str0, 一回暗号化した後の文字列を str1, もう一回暗号化した後の文字列を str2 とします。
入力として与えられるのは str2 ですが, str2 から str1 を求めるのは, O(N^2) の dp で解けます。

for (int i = 0; i < N; i++) {
    for (int j = i+2; j < N; j++) {
        if (code[i] != code[j] && code[i+1] != 0) {
            dp[j] += dp[i];
        }
    }
}

こんな感じですね。

この問題では, この dp を使って str2 から str1 を作りながら, str1 から str0 も同時に作っていくような dp をします。

dp[cur][digit][flag] = (str2 の cur 番目までの文字列を考える。str1 で, 最後に確定した数字が digit で, str0 でまだ数字が残っているかどうかのフラグが flag であるような場合の数) とします。いや自分でも何言ってるかわからないです。以下の例を考えてみます。topcoder の解説からです。

STRING : 1124512344
INDEX : 0123456789

一番最初に書いた dp において, i = 2, j = 5 とします。すると, str2 の i と j の間の成分として, str1 に, 1 が 45 個並んだものが出来ます。これ以前に str1 には 2 が 11 個並んだものが出来ていますが, str0 の中で, この 11 個の 2 について考慮する必要がなくなっている(つまり 2222222222 個の 2 を並べている)場合は flag = 1 となります。一方で, 例えば まだ 22 個の 2 を並べただけで, 222222222*** 個の 1 を並べることを暗号化した str1 の成分が残っているなら, flag = 0 となります。digit は直前の数字なので, この場合は 2 です。
str1 に 1 を 45 個並んだものができるので, str0 にも 1 をいくつか並べたものが出来ます。
まず, str0 で 1 を並べようとせず, 222...2211...11 という数だけ別の数字を並ばせる, という選択肢があります(flag = 0 のまま別の数字を並ばせる)。
他の場合は, とにかく 1 を str0 にも並ばせようとします。仮に flag = 0 であるとすると, str0 には 「まだ str1 で消費されていない数字の分 + 45-1 個の 1 のうちいくつの 1 『1 が並んでいる数』として数えるか」という 1 の消費の仕方があります。flag = 1 の時は, str1 で消費されていない数字はないので, 45-1 個の数字の分をどう割り振るかです。例えば 1 個だけ 1 を並べて, 残り 45-1-1 個は後に残すとすると, flag = 0 ですが, 1111...1(1 が 44 個) の 1 を str0 に並ばせた場合は, 残っている数字はないので, flag = 1 です。

こんな感じ(???) のことを dp の遷移で書きます。簡単に場合分けすると,

  • i 〜 j で str1 に生成された桁は桁のままにしておく
  • i 〜 j で生成された桁で, code[j] をいくつか並べる
    • code[j] を flag = 1 になるまで並べるか, flag = 0 に並べるかで場合分け

となります。

const int MAXN = 555;
const ll MOD = 1e9+9;

ll dp[MAXN][11][2];

class StringDecryption {
public:
    int decrypt(vector <string> CODE) {
        string code;
        for (string s : CODE) code += s;
        int n = code.size();
        memset(dp, 0, sizeof(dp));
        dp[0][10][1] = 1;
        for (int i = 0; i < n; i++) {
            ll digitSum = 0;
            for (int j = i+2; j <= n; j++) {
                int num = code[j-1]-0;
                digitSum = digitSum * 10 + (code[j-2]-0);
                digitSum %= MOD;
                if (i != 0 && code[i-1] == code[j-1]) continue;
                if (code[i] == 0) continue;
                for (int digit = 0; digit <= 10; digit++) {
                    for (int flag = 0; flag < 2; flag++) {
                        if (num == 0 && flag == 1) continue;
                        // digit のまま続ける
                        (dp[j][digit][0] += dp[i][digit][flag]) %= MOD;
                        // digit から num に切り替える
                        if (digit == num) continue;
                        if (num > 0 && (digitSum > 1 || j-i > 2)) {
                            dp[j][num][0] += dp[i][digit][flag] * (digitSum-1-flag) % MOD;
                            (dp[j][num][0] += MOD) %= MOD;
                        }
                        if ((digitSum > 1 || j-i > 2) || flag == 0) {
                            (dp[j][num][1] += dp[i][digit][flag]) %= MOD;
                        }
                    }
                }
            }
        }
        return (int)(dp[n][code[n-1]-0][1]);
    }
}