必要な実行時の保証に応じて、1 回のパスで数値のストリームから k 個のランダムな要素を選択する有名な O(n) アルゴリズムがあります。アルゴリズムを理解するために、最初にセットから 1 つの要素だけを選択する場合を見てみましょう。次に、k 要素を選択するために機能するように一般化します。このアプローチの利点は、入力セットのサイズに関する事前の知識を必要とせず、要素の確実に均一なサンプリングを保証することです。これは常に非常に優れています。
セットから 1 つの要素を選びたいとします。これを行うには、セット内のすべての要素をパスし、各ポイントで返す予定の候補要素を維持します。要素のリスト全体を反復処理すると、最終的に均一な確率で単一の要素を選択するまで、ある程度の確率で推測を更新します。各ポイントで、次の不変条件を維持します。
k 個の要素を見た後、最初の k 個の要素のいずれかが候補要素として現在選択されている確率は 1 / k です。
配列全体でこの不変条件を維持すると、n 個の要素をすべて確認した後、それぞれが候補要素になる可能性が 1 / n になります。したがって、候補要素は一様にランダムな確率でサンプリングされています。
アルゴリズムがどのように機能するかを見るために、不変条件を維持するために何をしなければならないかを考えてみましょう。最初の要素を見たとします。上記の不変条件を維持するには、確率 1 でそれを選択する必要があるため、候補要素の初期推定を最初の要素に設定します。
さて、2 番目の要素については、2 つの要素を見たので、各要素が 1/2 の確率で選択されるという不変条件を保持する必要があります。では、確率 1/2 で 2 番目の要素を選択するとします。次に、次のことがわかります。
- 2 番目の要素を選択した確率は 1/2 です。
- 最初の要素を選択した確率は、最初にそれを選択した確率 (1) に、2 番目の要素を選択しなかった確率 (1/2) を掛けたものです。これも 1/2 になります。
したがって、この時点ではまだ不変条件が維持されています。3 番目の要素になるとどうなるか見てみましょう。この時点で、各要素が 1/3 の確率で選択されるようにする必要があります。さて、確率 1/3 で最後の要素を選択するとします。それから私たちはそれを知っています
- 3 番目の要素を選択した確率は 1/3 です。
- 最初の 2 つの要素のいずれかを選択した確率は、最初の 2 つのステップの後にそれが選択された確率 (1/2) に、3 番目の要素を選択しなかった確率 (2/3) を掛けたものです。これは 1/3 になります。
繰り返しますが、不変式が成り立ちます!
ここでの一般的なパターンは次のようになります: k 個の要素を見た後、各要素は 1/k の確率で選択されます。(k + 1) 番目の要素を見ると、1 / (k + 1) の確率でそれを選択します。これは、1 / (k + 1) の確率で選択され、それより前のすべての要素が、(1 / k) の前に選択して (k + 1) を選択しなかった確率に等しい確率で選択されることを意味します。 )今回は (k / (k + 1)) 番目の要素であり、これらの要素が選択される確率はそれぞれ 1 / (k + 1) になります。これにより、各ステップで不変条件が維持されるため、優れたアルゴリズムが得られます。
- 最初の要素が表示されたら候補として選択します。
- 連続する要素ごとに、候補要素をその要素に確率 1 / k で置き換えます。ここで、k はこれまでに検出された要素の数です。
これは O(n) 時間で実行され、O(1) スペースが必要で、データ ストリームから一様にランダムな要素を返します。
では、セットから 1 つだけでなくk 個の要素を選択したい場合に、これを機能するようにスケールアップする方法を見てみましょう。この考え方は、前のアルゴリズムと非常によく似ています (実際には、より一般的なアルゴリズムの特殊なケースになります)。1 つの候補を維持する代わりに、k 個の異なる候補を維持し、1、2、...、k の番号を付けた配列に格納します。各ポイントで、この不変条件を維持します。
m > k 個の要素を見た後、最初の m 個の要素のいずれかが選択される確率は k / m です。
配列全体をスキャンすると、完了時に各要素が選択される確率が k / n になることを意味します。k 個の異なる要素を選択しているため、配列から要素を一様にランダムにサンプリングすることを意味します。
アルゴリズムは以前と同様です。最初に、セットから最初の k 個の要素を確率 1 で選択します。これは、k 個の要素を見たときに、それらのいずれかが選択された確率が 1 = k / k であり、不変式が成り立つことを意味します。ここで、帰納的に m 回の反復後に不変式が成り立つと仮定し、(m + 1) 回目の反復を考えます。1 から (m + 1) までの乱数を選択します。1 から k (両端を含む) の間の数を選択した場合、その候補要素を次の要素に置き換えます。それ以外の場合は、次の要素を選択しないでください。これは、必要に応じて確率 k / (m + 1) で次の要素を選択することを意味します。最初の m 個の要素のいずれかが選択される確率は、その要素を含むスロットが選択されなかった確率 (m / (m + 1)) の (k / m) 倍の前に選択された確率です。これにより、必要に応じて k / (m + 1) が選択される合計確率が得られます。帰納法により、これは、アルゴリズムがセットから k 個の要素を完全に均一かつランダムにサンプリングすることを証明します!
さらに、実行時間は O(n) であり、これはセットのサイズに比例し、選択する要素の数とは完全に無関係です。また、O(k) メモリのみを使用し、格納される要素の型について何の仮定も行いません。
あなたはこれを C++ でやろうとしているので、恥知らずな自己宣伝として、このアルゴリズムの実装 (STL アルゴリズムとして書かれたもの)を私の個人的な Web サイトで入手できます。お気軽にご利用ください!
お役に立てれば!