mayoko’s diary

プロコンとかいろいろ。

SRM 536 div1 med: RollingDiceDivOne

Laycrs さんがこどふぇすの時言ってた問題多分これですね。

解法

エイヤでサンプル合わせたら WA 食らったので editorial 見ました。
http://apps.topcoder.com/wiki/display/tc/SRM+536

まず様子を観察して見るためのコードを書いてみます。gnuplot を使ってたので僕も gnuplot で書きました。

int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    int n;
    cin >> n;
    vector<int> dice(n);
    for (int i = 0; i < n; i++) cin >> dice[i];
    vector<int> ways(100);
    ways[0] = 1;
    for (int i = 0; i < n; i++) {
        vector<int> _ways(100);
        for (int j = 0; j < 100; j++) {
            if (ways[j]) {
                for (int k = 0; k <= dice[i]; k++) _ways[j+k] += ways[j];
            }
        }
        ways = _ways;
    }
    int maxX = 0, maxY = 0;
    for (int i = 0; i < 100; i++) {
        if (ways[i] == 0) break;
        maxX = max(i, maxX);
        maxY = max(ways[i], maxY);
        cout << i << "  " << ways[i] << endl;
    }
    FILE *fp = popen("gnuplot -persist", "w");
    if (fp == NULL) return -1;
    fprintf(fp, "set xrange [0:%d]\n", maxX);
    fprintf(fp, "set yrange [0:%d]\n", maxY+1);
    fprintf(fp, "plot '-' with lines linetype 1 title \"ways\"\n");
    for (int i = 0; i < 100; i++) {
        if (ways[i] == 0) break;
        fprintf(fp, "%d\t%d\n", i, ways[i]);
    }
    pclose(fp);
    return 0;
}

見る前から知ってた, という人も多いかと思いますが一応説明しますと, 最終的なグラフは左右対称であり,
(狭義単調増加) -> (一定の値キープ) -> (狭義単調減少), というようになります。
一例として, 上のプログラムに 3 1 2 17 と入れたものを載せておきます。
f:id:mayokoex:20160325122958p:plain

このことから,

  • dice の和
  • (一定の値キープ)の幅(plateau(台地) とする)

がわかれば, 答えを求めることが出来ることがわかります。dice の値は 1〜d, というようにするよりも, 0〜d, とするほうが扱いやすいのでそのようにします。

plateau を求めましょう。これは今の確率分布から dice[i] を振った時, 一定のルールにしたがって変化します。あ, あと今までに振った dice の和 sum という変数も用意しておきます。

dice[i] < plateau の時
f:id:mayokoex:20160325125101j:plain
この場合, 図のように「plateau の中からのみ遷移する」という dice の和が存在し, それが最大になります。これによって, plateau の数は d 減少します。
dice[i] > sum の時
f:id:mayokoex:20160325125119j:plain
この場合分けに最初気づきませんでした(上の 1 2 17 という例を見ると明らかにそういう場合があることに気づく)。
この場合は, 今までにあったすべての事象(0, 1, ..., sum が出る確率)をすべて包含しながら派生する場合が dice[i]-sum+1 個存在します。なので, plateau = dice[i]-sum+1 です。
それ以外
ちょっと説明しづらいですが, 「左右から平等に遷移させる」のが最も確率を高くしそうです。そのため,

  • dice[i]-plateau が奇数の時は plateau = 1
  • dice[i]-plateau が偶数の時は plateau = 2

となります。

これを 各 dice について回せば答えが得られます。

class RollingDiceDivOne {
public:
    long long mostLikely(vector <int> dice) {
        ll sum = 0, plateau = 1;
        for (int d : dice) {
            d--;
            if (d < plateau) plateau -= d;
            else if (sum <= d) plateau = d-sum+1;
            else {
                ll tmp = d-plateau;
                if (tmp%2) plateau = 1;
                else plateau = 2;
            }
            sum += d;
        }
        ll ret = dice.size();
        cout << sum << "  " << plateau << endl;
        if (sum%2==0) {
            assert(plateau%2==1);
            ret += sum/2 - plateau/2;
        } else {
            assert(plateau%2==0);
            ret += (sum-plateau+1)/2;
        }
        return ret;
    }
};