The Double Dixie Cup Problemと期待値DP

The Double Dixie Cup Problem
典型的な「クーポンコレクター問題」は、「N種類のクーポンが1枚ずつ入った箱がある。箱からクーポンを1枚引いて、そのたびに同じ種類のクーポンが箱に戻される試行を繰り返した時、N種類全て1枚以上引くまでの試行回数の期待値はいくつか」という問題で、この回答は有名です。
The Double Dixie Cup Problemとは、これを「N種類全て2枚以上引くまでの試行回数」や「N種類全てM枚以上引くまでの試行回数」に一般化した問題です。
例えば「保存用に2枚ずつ欲しい」とか、「必要に応じてトレードすることで、友だち2人でコンプリートしたい」みたいなシチュエーションはよくありますね。
本スクラップでは、これを動的計画法(期待値DP)を使って求めます。
また、クーポンコレクター問題の別の拡張として
- N種類のクーポンが出る確率がそれぞれ違う(A賞が5%, E賞が55%出るクジなど)
- 期待値でなく「90%以上コンプできる回数」などが知りたい
に言及します。
期待値DP以外について
閉じた式
あるのですが、大変です。
Deep Researchが教えてくれた参考文献
自明な上界
例えばN=7, M=1の時は18.15回なので、N=7, M=2の場合は明らかに36.3回以下ですね。
実際はN=2, M=1の時は29.42回なので、そこそこ乖離はありますが。
シミュレーション
適当に1万回くらいシミュレーションすればそこそこ収束すると思うので、それでもいいです。

例
7種類で等確率のクーポンを2枚ずつ揃えたい場合の試行回数の期待値。
- 定義より、
dp[7][0] = 0 - 0枚揃ったクーポンを引いて
に遷移できる確率dp[a][b+1] はp_0 (7-a-b)/7 - 1枚揃ったクーポンを引いて
に遷移できる確率dp[a+1][b-1] はp_1 b/7 - 2枚揃ったクーポンを引いて
に留まる確率dp[a][b] はp_2 a/7 -
が求めたい期待値dp[0][0]
である。
これをそのまま漸化式で表すと、
となる。 式変形して右辺からdp[a][b]を消すと、
これをそのままプログラムに落として実行すれば求まる(29.4247回)
#include <bits/stdc++.h>
using namespace std;
int main() {
vector dp(8, vector<double>(8));
for (int a = 7; a >= 0; a--) {
for (int b = 7 - a; b >= 0; b--) {
if (a == 7 && b == 0) {
dp[7][0] = 0;
continue;
}
double p0 = (7 - a - b) / 7., p1 = b / 7., p2 = a / 7.;
dp[a][b] = 1;
// 0個揃ったものが1個揃う
if (b < 7) dp[a][b] += p0 * dp[a][b + 1];
// 1個揃ったものが2個揃う
if (a < 7 && b >= 1) dp[a][b] += p1 * dp[a + 1][b - 1];
// 2個揃ったものが被る
dp[a][b] /= 1 - p2;
}
}
cout << dp[0][0] << endl;
return 0;
}
実装上はメモ化再帰の方が簡単かも。
DPが分からない人向け
期待値DPが分からない人向け
期待値DPの自己ループ除去が分からない人向け
状態の持ち方が天才だと思った人向け

N種類のクーポンが出る確率がそれぞれ違う場合
状態をまとめられないので、計算量は悪化するが別の次元に持てばDP自体は簡単。
例えば「A賞が2種類でそれぞれ1%、B賞が3種類でそれぞれ2%……」のような問題で1個ずつ揃えたい場合は、
dp[a][b][c][d][e] := A賞がa枚、B賞がb枚、C賞がc枚、D賞がd枚、E賞がe枚ある状態からコンプリートまでに引く枚数の期待値
にして、同じA賞でも既に持っているやつを引いたかまだ持っていないやつを引いたかで分岐すればそれなりにまとまる。
さらに2個ずつ揃えたい場合はもっと次元が増える。

期待値でなく「90%以上コンプできる回数」などが知りたい
こっちの方が簡単。全状態の確率を持って、普通にDPする。
dp[i][a][b] := i回引いて、2枚引いたのがa種類、1枚引いたのがb種類の確率
(いわゆるnext dpで1次元目は潰せるが)