TDPC D - サイコロ
TDPC D - サイコロ に関連する、計算量を落とす工夫についてのメモを残します。
参考
例題
コインを 1 枚投げて、表が出たら 1 点、裏が出たら 2 点を獲得する。これを 4 回行うとき、合計得点が 6 点以上となる確率はいくらか。
求める確率は
となる。
これを動的計画法 (DP) で求めてみる。
実装例 1 - 素朴な実装
DP テーブルは
コインを 回投げて、合計得点が 点となる確率
とし、テーブルの列は 9 列 用意する。
#include <bits/stdc++.h> using namespace std; int main() { vector<vector<double>> dp(5, vector<double>(9)); dp[0][0] = 1.0; for (int i = 0; i < 4; i++) { for (int j = 0; j < 9; j++) { if (dp[i][j] > 0) { dp[i+1][j+1] += dp[i][j] / 2.0; dp[i+1][j+2] += dp[i][j] / 2.0; } } } double ans = 0.0; for (int j = 6; j <= 8; j++) ans += dp[4][j]; cout << ans << endl; }
実装例 2
TDPC D - サイコロ で利用する考え方。
DP テーブルの列は 7 列 しか用意せず、 の列は 6 点以上となる確率とする。
#include <bits/stdc++.h> using namespace std; int main() { vector<vector<double>> dp(5, vector<double>(7)); dp[0][0] = 1.0; for (int i = 0; i < 4; i++) { for (int j = 0; j < 7; j++) { dp[i+1][min(j+1, 6)] += dp[i][j] / 2.0; dp[i+1][min(j+2, 6)] += dp[i][j] / 2.0; } } cout << dp[4][6] << endl; }
検証
実装例 1 では 7 点、8 点の列も用意したが、実装例 2 ではそれらを 6 点の列にまとめた。
ここで実装例 2 について、モヤッとしないように表にしてみる。
下に示すのは、完成した DP テーブル。気になるのは右下の端っこ。
DP テーブルの列をわざと までしか用意していないため、下に示す通り、当然行き場のない矢印が発生してしまう。そこで、 や へ向かう矢印については、min 関数を用いて行き先を に変更する。
行き先を変更して、 や に加算しようとした値を に加算することで、 の列は 6 点以上を獲得する確率を表すことになる。
テーブルのサイズを節約できただけでなく、答えは の値を出力するだけでよくなった。
このような工夫を TDPC D - サイコロ に活かしてみる。
TDPC D - サイコロ
- 問題 : TDPC D - サイコロ
サイコロを 回投げて、出た目の積が の倍数となる確率を求める問題。
制約 : かつ
サイコロを 回投げて、出た目の積を とすると、必ず と表せる。
が の倍数となるには、 は 2, 3, 5 以外の素因数を含まない、かつ とするとき、 かつ かつ であることを満たせばよい。
次に、サイコロを 1 回投げて、どのように状態が推移するかを整理する。
出た目の積が の状態で、サイコロを 1 回投げる。すると
- 1 の目が出たとき、 は変化なし
- 2 の目が出たとき、 が 1 増加
- 3 の目が出たとき、 が 1 増加
- 4 の目が出たとき、 が 2 増加
- 5 の目が出たとき、 が 1 増加
- 6 の目が出たとき、 がそれぞれ 1 増加
以上の推移となる。
次に、DP テーブルのサイズを考える。
サイコロを 回投げて、出た目の積が となる確率
とする。
、、 であるから、 の 4 次元テーブルが思い浮かぶ。これでも AC できたが、上の例題でみた考え方を利用する。
のとき、 かつ かつ となる確率を求めればよいので、サイズは で十分で、 には、 回投げて、2 が 個以上、3 が 個以上、5 が 個以上となる確率を記録するとよい。計算量は となる。
確認しておくと、 であるから、大きくても は 60 近く、 は 40 近く、 は 20 近くに収まる。
ソースコードは、Typical DP Contest D - サイコロ - すいバカ日誌 が大変参考になりました。この方は 4 次元テーブルではなく、
回目の試行を行う前の状態
回目の試行を行った後の状態
この 2 つの 3 次元テーブルを用意されています。とても勉強になります。
ちなみに 4 次元テーブルで実装したソースコードは以下の通り。
#include <bits/stdc++.h> using namespace std; using vi = vector<int>; using vd = vector<double>; using vvd = vector<vd>; using vvvd = vector<vvd>; using vvvvd = vector<vvvd>; int main() { long long N, D; cin >> N >> D; vi pf(3); vi div = { 2, 3, 5 }; for (int i = 0; i < 3; i++) { while (D % div[i] == 0) { pf[i]++; D /= div[i]; } } if (D != 1) { cout << 0 << endl; return 0; } vvvvd dp(N+1, vvvd(pf[0]+1, vvd(pf[1]+1, vd(pf[2]+1)))); dp[0][0][0][0] = 1.0; vi di = { 0, 1, 0, 2, 0, 1 }; vi dj = { 0, 0, 1, 0, 0, 1 }; vi dk = { 0, 0, 0, 0, 1, 0 }; for (int n = 0; n < N; n++) { for (int i = 0; i <= pf[0]; i++) { for (int j = 0; j <= pf[1]; j++) { for (int k = 0; k <= pf[2]; k++) { for (int d = 0; d < 6; d++) { int ni = min(pf[0], i+di[d]); int nj = min(pf[1], j+dj[d]); int nk = min(pf[2], k+dk[d]); dp[n+1][ni][nj][nk] += dp[n][i][j][k] / 6.0; } } } } } cout << fixed << setprecision(10); cout << dp[N][pf[0]][pf[1]][pf[2]] << endl; }