コンテスト中は丁寧丁寧丁寧に解いたので雑に雑に雑に解説残しておきます。想定解と多分一緒です。
問題概要
https://atcoder.jp/contests/abc342/tasks/abc342_f
あなたとディーラー、面サイコロ、変数があります。あなたとディーラーがゲームをします。
まず、あなたが、以下の操作を好きな回数繰り返します。
- サイコロを一回振る。出た目をに加算する。
次に、ディーラーがの間以下を繰り返します。
- サイコロを一回振る。出た目をに加算する。
勝敗は次のように決められます。
- ならディーラーの勝ち
- そうでなくてならあなたの勝ち
- そうでなくてならあなたの勝ち
- そうでないならディーラーの勝ち
あなたが勝つために最適に行動した時、勝つ確率を求めてください。
制約
解法
dpを考えました。を「の初期値がだったとしてゲームをはじめてあなたが勝つ確率」
の値はゲームのルールよりです。(所謂dpの初期値)
そうで無い時、あなたはサイコロを振るか振らないかの二手があり、サイコロを振った場合の勝率と振らなかった場合の勝率が求まれば良いです(勝率が高い戦略を実際に選ぶことになる)。
サイコロを振った時の勝率は
サイコロを振らなかった時の勝率は、となる確率をとしてです。
- を計算するのに、以降のみが必要になることからが後ろから計算できることがわかります。
- サイコロを振った時の遷移は後ろ個のの和を別の変数で管理しておけばクリア
- サイコロの振らなかった時の遷移はさえ求まっていればその累積和を持ってクリア
サイコロを振らなかった時の遷移の計算に必要なを求めましょう。明らかにです*1。がになっている瞬間がある確率をとして、です。じゃあを求めないといけない!になるのですが、で、計算式がまったく一緒。
- dpの計算と同様、前からの和をよしなに変数で持っておけばこの計算も効率的にできる
実装
変数名等がこれまでの表記と噛み合ってないです。
int n, l, d; void calcD(std::vector<long double>& D) { D[0] = 1.0; long double sum{}; sum += D[0]; for (int i{1} ; i < l + d ; i++) { D[i] = sum / (long double)d; if (i < l) sum += D[i]; if (i - d >= 0) sum -= D[i - d]; } for (int i{} ; i < l ; i++) D[i] = 0; } int main() { SetFastIO(); std::cin >> n >> l >> d; std::vector<long double> D(std::max(n + 1, l + d)); calcD(D); std::vector<long double> acm(D.size() + 1); for (int i{} ; i < (int)D.size() ; i++) { acm[i + 1] = acm[i] + D[i]; } std::vector<long double> dp(n + 1); long double shake{}; for (int i{n} ; i >= 0 ; i--) { long double here{ acm.back() - acm[std::min((int)D.size(), n + 1)] + acm[std::min(i, (int)D.size())] }; dp[i] = std::max(here, shake / (long double)d); shake += dp[i]; if (i + d <= n) shake -= dp[i + d]; } SetPrecision(-8); std::cout << dp[0] << '\n'; }
感想
累積和の添字が難しかったです。
全人類Gに突撃したのが原因なのか、自分が思っているよりdiffが高くでている。
- 人類クエリ処理が上手すぎるぜ
*1:明らかとかかっこつけて言っていますが、私はこれを忘れて2ペナしました....