問題
ダーツをN回投げる。毎回short rangeとlong rangeを選ぶことができる。short rangeで投げて的に当たったら2点、long rangeで投げて当たれば3点もらえる。的に当たらなかった場合は0点である。short rangeで命中させられる確率はp1、long rangeで命中させられる確率はp2である。N回投げ終わった後にW点以上得点していればプレイヤーの勝ちとする。プレイヤーが勝つ確率を求めよ。(1 <= N <= 1000, 1 <= W <= 1000)
方針
とりあえず、DPっぽいのはすぐ分かった。毎回shortにするか、longにするかプレイヤーが最適な選択をできるというのが厄介。これをどうするか。状態をdp[残りのゲーム数][今の点数]で表すと、以下の漸化式が成り立つことに気付く。
dp[N][W] = max(shortで投げた場合ゲームに勝つ確率、longで投げた場合ゲームに勝つ確率)
= max(p1*dp[N-1][W+2] + (1-p1)*dp[N-1][W], p2*dp[N-1][W+3] + (1-p2)*dp[N-1][W])
= max(p1*dp[N-1][W+2] + (1-p1)*dp[N-1][W], p2*dp[N-1][W+3] + (1-p2)*dp[N-1][W])
これでやってみる。
メモ化再帰で実装
まずメモ化で書いてみた。AC。(下のソースではやってませんが、WとP1とP2は呼び出し毎に変わる変数じゃないのでメンバー変数に入れておいた方がいいです。)
double dp[1001][3001];
class SimplifiedDarts {
double solve(int n, int p, int w, int p1, int p2) {
if (dp[n][p] >= 0) return dp[n][p];
if (n == 0) {
if (p >= w) return dp[n][p] = 1.0;
else return dp[n][p] = 0.0;
}
double ret = p1 / 100.0 * solve(n-1, p+2, w, p1, p2) + (100-p1) / 100.0 * solve(n-1, p, w, p1, p2);
ret = max(ret, p2 / 100.0 * solve(n-1, p+3, w, p1, p2) + (100-p2) / 100.0 * solve(n-1, p, w, p1, p2));
return dp[n][p] = ret;
}
public:
double tryToWin(int W, int N, int P1, int P2) {
memset(dp, -1, sizeof(dp));
return solve(N, 0, W, P1, P2) * 100;
}
};
DPで実装
こういう時系列でみて後ろから前に詰まっていくDPは、すぐに書けないのでDPで書き直してみる。すっきりしました。
double tryToWin(int W, int N, int P1, int P2) {
memset(dp, 0, sizeof(dp));
FOR (i, W, 3001) dp[0][i] = 1;
FOR (i, 1, N+1) REP(j, 3*N+1) {
dp[i][j] = max(
P1/100.0 * dp[i-1][j+2] + (100-P1)/100.0 * dp[i-1][j], // short
P2/100.0 * dp[i-1][j+3] + (100-P2)/100.0 * dp[i-1][j] // long
);
}
return dp[N][0]*100;
}
まとめ
- 確率のDPで戦略的に次の手を選ぶことができるような問題に対して、こういうやり方はある種の定石っぽい気がする。
- DPを書くときは、base casesとSubproblem-DAGの枝の向きを意識するようにする。
0 件のコメント:
コメントを投稿