どのカードが特定のドローの対象となるかを事前に知らないというコメントに追加した制約を考えると、2パスアルゴリズムが最善の方法である可能性が高いと思います.
狡猾な「単一パスで未知のサイズのリストからランダムに選択する」アルゴリズムを試すことができます。
int sofar = 0;
int selected = -1;
for (i = 0; i < 52; ++i) {
if (used[i]) continue;
++sofar;
if ((rand() % sofar) == 0) selected = i;
}
if (selected == -1) panic; // there were no usable cards
else used[selected] = 1; // we have selected a card
次に、(コメントで言うように)異なるドローに異なる基準がある場合used[i]
、実際の基準が何であれ置き換えることができます。
それが機能する方法は、最初のカードを選択することです。次に、1/2 の確率で 2 枚目のカードに置き換えます。その結果を 1/3 の確率で 3 枚目のカードに置き換えます。n ステップ後、前のカードのそれぞれが選択されたものである確率が 1/n であることを帰納法によって証明するのは簡単です。
この方法では多くの乱数が使用されるため、各アイテムの取得が遅くなったり、条件の評価が遅くなったりしない限り、おそらく 2 パス バージョンよりも遅くなります。これは通常、ファイルからランダムな行を選択する場合などに使用されますが、実際にはデータを 2 回実行したくない場合に使用されます。また、乱数の偏りにも敏感です。
シンプルでいいですけどね。
[編集:証拠
ステップ k の後にカード番号 j が現在選択されているカードである確率を p(j,k) とします。
証明する必要があります: すべての n に対して、p(j,n) = 1/n すべての 1 <= j <= n に対して
n = 1 の場合、最初のステップで最初のカードが確率 1/1 = 1 で選択されるため、明らかに p(1,1) = 1 です。
すべての 1 <= j <= k に対して p(j,k) = 1/k であるとします。
次に、ステップ (k+1) で確率 1/(k+1)、つまり p(k+1,k+1) = 1/(k+1) で (k+1) 番目のカードを選択します。
確率 k/(k+1) で既存の選択を保持するため、任意の j < k+1 について:
p(j,k+1) = p(j,k) * k/(k+1)
= 1/k * k/(k+1) // by the inductive hypothesis
= 1/(k+1)
したがって、すべての 1 <= k <= k+1 に対して p(j,k+1) = 1/(k+1)
したがって、帰納法により、すべての n について: p(j,n) = 1/n for all 1 <= j <= n]