28

この質問は、必要なすべてのデータを提供します。指定された間隔[0,N-1]内でK個の非反復整数のシーケンスを生成する効率的なアルゴリズムは何ですか。Kが大きく、 Nに十分近い場合、簡単なアルゴリズム (乱数を生成し、それらをシーケンスに追加する前にそれらが既に存在するかどうかを調べます) は非常にコストがかかります。

リンクされたリストから一連のランダムな要素を効率的に選択するで提供されているアルゴリズムは、必要以上に複雑に見え、いくつかの実装が必要です。関連するすべてのパラメーターを1回のパスで知っている限り、問題なく機能するように見える別のアルゴリズムを見つけました。

4

13 に答える 13

13

The Art of Computer Programming, Volume 2: Seminumerical Algorithms, Third Editionで、Knuth は次の選択サンプリング アルゴリズムについて説明しています。

アルゴリズム S (選択サンプリング手法)。0 < n ≤ N である N 個のセットから n 個のレコードをランダムに選択します。

S1。[初期化] t ← 0、m ← 0 に設定します (このアルゴリズムでは、m はこれまでに選択されたレコードの数を表し、t は処理した入力レコードの総数です)。

S2. [U を生成する] 0 と 1 の間で一様に分布する乱数 U を生成します。

S3。[検定] (N – t)U ≧ n – m ならばステップ S5 へ。

S4. [選択] サンプルの次のレコードを選択し、m と t を 1 ずつ増やします。m < n の場合、ステップ S2 に進みます。それ以外の場合、サンプルは完全であり、アルゴリズムは終了します。

S5. [Skip.] 次のレコードをスキップして (サンプルに含めず)、t を 1 増やして、ステップ S2 に戻ります。

実装は、説明よりも従う方が簡単な場合があります。リストから n 個のランダムなメンバーを選択する Common Lisp の実装を次に示します。

(defun sample-list (n list &optional (length (length list)) result)
  (cond ((= length 0) result)
        ((< (* length (random 1.0)) n)
         (sample-list (1- n) (cdr list) (1- length)
                      (cons (car list) result)))
        (t (sample-list n (cdr list) (1- length) result))))

そして、再帰を使用せず、あらゆる種類のシーケンスで機能する実装を次に示します。

(defun sample (n sequence)
  (let ((length (length sequence))
        (result (subseq sequence 0 n)))
    (loop
       with m = 0
       for i from 0 and u = (random 1.0)
       do (when (< (* (- length i) u) 
                   (- n m))
            (setf (elt result m) (elt sequence i))
            (incf m))
       until (= m n))
    result))
于 2008-10-09T16:02:12.517 に答える
12

Python ライブラリのrandom モジュールを使用すると、非常に簡単で効果的です。

from random import sample
print sample(xrange(N), K)

sample関数は、指定されたシーケンスから選択された K 個の一意の要素のリストを返します。
xrangeは「リスト エミュレーター」です。つまり、メモリ内に作成せずに連続した数字のリストのように動作するため、このようなタスクが非常に高速になります。

于 2008-10-01T17:47:01.573 に答える
5

実際には、選択しているセット全体の割合に関係なく、選択しているセットのサイズではなく、選択した要素の数に比例するスペースでこれを行うことができます。これを行うには、ランダムな順列を生成してから、次のように選択します。

TEAや XTEAなどのブロック暗号を選択します。XOR フォールディングを使用して、選択しているセットよりも大きい最小の 2 の累乗にブロック サイズを縮小します。ランダム シードを暗号の鍵として使用します。置換で要素 n を生成するには、n を暗号で暗号化します。出力番号がセットにない場合は、それを暗号化します。数がセット内に収まるまで繰り返します。平均して、生成された数値ごとに 2 回未満の暗号化を行う必要があります。これには、シードが暗号的に安全である場合、順列全体も安全であるという追加の利点があります。

これについては、こちらで詳しく説明ています。

于 2008-10-02T09:50:34.970 に答える
3

次のコード (C 言語、出所不明) は、問題を非常にうまく解決しているようです。

 /* generate N sorted, non-duplicate integers in [0, max[ */
 int *generate(int n, int max) {
    int i, m, a;    
    int *g = (int *)calloc(n, sizeof(int));
    if ( ! g) return 0;

    m = 0;
    for (i=0; i<max; i++) {
        a = random_in_between(0, max - i);
        if (a < n - m) {
            g[m] = i;
            m ++;
        }
    }
    return g;
 }

このような他の宝石をどこで見つけることができるか知っている人はいますか?

于 2008-10-01T17:27:50.043 に答える
2

0...N-1で満たされた配列を生成しa[i] = iます。

K次に、最初のアイテムをシャッフルします。

シャッフリング:

  • 始めるJ = N-1
  • 乱数を選択します0...J (たとえば、R)
  • a[R]と交換a[J]
    • Rと等しくなる可能性があるためJ、要素はそれ自体と交換される可能性があります
  • 1から引いてJ繰り返す。

最後に、K最後の要素を取ります。

これは基本的に、リストからランダムな要素を選択し、それを移動してから、残りのリストからランダムな要素を選択する、というように続きます。

O (K)およびO(N)時間で動作し、 O(N)ストレージが必要です。

シャッフル部分は、 The Art of Computer Programming の第 2 巻で説明されているように、Fisher-Yates shuffleまたはKnuth's shuffleと呼ばれます。

于 2008-10-01T17:29:27.530 に答える
1

K 個の数値をハッシュ ストアに格納することで、自明なアルゴリズムを高速化します。始める前に K を知っていれば、ハッシュ マップへの挿入の非効率性をすべて取り除くことができ、高速ルックアップの利点も得られます。

于 2008-10-01T17:25:36.353 に答える
1

私のソリューションは C++ 指向ですが、非常に単純なので、他の言語に翻訳できると確信しています。

  • 最初に、0 から K までの K 個の要素を持つ連結リストを生成します。
  • 次に、リストが空でない限り、0 とベクトルのサイズの間の乱数を生成します
  • その要素を別のベクトルにプッシュし、元のリストから削除します

このソリューションには、2 回のループ反復のみが含まれ、ハッシュ テーブルの検索などは含まれません。したがって、実際のコードでは:

// Assume K is the highest number in the list
std::vector<int> sorted_list;
std::vector<int> random_list;

for(int i = 0; i < K; ++i) {
    sorted_list.push_back(i);
}

// Loop to K - 1 elements, as this will cause problems when trying to erase
// the first element
while(!sorted_list.size() > 1) {
    int rand_index = rand() % sorted_list.size();
    random_list.push_back(sorted_list.at(rand_index));
    sorted_list.erase(sorted_list.begin() + rand_index);
}                 

// Finally push back the last remaining element to the random list
// The if() statement here is just a sanity check, in case K == 0
if(!sorted_list.empty()) {
    random_list.push_back(sorted_list.at(0));
}
于 2008-10-01T18:17:38.600 に答える
1

ステップ 1: 整数のリストを生成します。
ステップ 2:クヌース シャッフルを実行します。

リスト全体をシャッフルする必要はないことに注意してください。これは、Knuth シャッフル アルゴリズムでは n 回のシャッフルのみを適用できるためです (n は返す要素の数)。リストの生成には、リストのサイズに比例した時間がかかりますが、シャッフル アルゴリズムを再起動する前に、部分的にシャッフルされたリストを事前にシャッフルする必要がなく、将来のシャッフルの必要に応じて (サイズが同じであると仮定して) 既存のリストを再利用できます。

Knuth Shuffle の基本的なアルゴリズムは、整数のリストから始めることです。次に、最初の整数をリスト内の任意の数値と交換し、現在の (新しい) 最初の整数を返します。次に、2 番目の整数をリスト内の任意の数 (最初の整数を除く) と交換し、現在の (新しい) 2 番目の整数を返します。そしたら…など…

これはとてつもなく単純なアルゴリズムですが、スワップを実行するときにリストに現在のアイテムを含めるように注意してください。そうしないと、アルゴリズムが壊れます。

于 2010-03-22T19:06:47.587 に答える
0

たとえば、リストが並べ替えられている場合、NからK個の要素を抽出したいが、それらの相対的な順序は気にしない場合は、効率的なアルゴリズムが論文An Efficient Algorithm for Sequential Random Sampling(Jeffrey Scott Vitter、ACM Transactions on Mathematical Software、Vol。13、No. 1、March 1987、Pages 56-67。)

Boostを使用してC++でコードを追加するように編集しました。入力したばかりで、エラーが多い可能性があります。乱数は、愚かなシードを持つBoostライブラリから取得されるため、これで深刻なことは何もしないでください。

/* Sampling according to [Vitter87].
 * 
 * Bibliography
 * [Vitter 87]
 *   Jeffrey Scott Vitter, 
 *   An Efficient Algorithm for Sequential Random Sampling
 *   ACM Transactions on MAthematical Software, 13 (1), 58 (1987).
 */

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <string>
#include <iostream>

#include <iomanip>

#include <boost/random/linear_congruential.hpp>
#include <boost/random/variate_generator.hpp>
#include <boost/random/uniform_real.hpp>

using namespace std;

// This is a typedef for a random number generator.
// Try boost::mt19937 or boost::ecuyer1988 instead of boost::minstd_rand
typedef boost::minstd_rand base_generator_type;

    // Define a random number generator and initialize it with a reproducible
    // seed.
    // (The seed is unsigned, otherwise the wrong overload may be selected
    // when using mt19937 as the base_generator_type.)
    base_generator_type generator(0xBB84u);
    //TODO : change the seed above !
    // Defines the suitable uniform ditribution.
    boost::uniform_real<> uni_dist(0,1);
    boost::variate_generator<base_generator_type&, boost::uniform_real<> > uni(generator, uni_dist);



void SequentialSamplesMethodA(int K, int N) 
// Outputs K sorted random integers out of 0..N, taken according to 
// [Vitter87], method A.
    {
    int top=N-K, S, curr=0, currsample=-1;
    double Nreal=N, quot=1., V;

    while (K>=2)
        {
        V=uni();
        S=0;
        quot=top/Nreal;
        while (quot > V)
            {
            S++; top--; Nreal--;
            quot *= top/Nreal;
            }
        currsample+=1+S;
        cout << curr << " : " << currsample << "\n";
        Nreal--; K--;curr++;
        }
    // special case K=1 to avoid overflow
    S=floor(round(Nreal)*uni());
    currsample+=1+S;
    cout << curr << " : " << currsample << "\n";
    }

void SequentialSamplesMethodD(int K, int N)
// Outputs K sorted random integers out of 0..N, taken according to 
// [Vitter87], method D. 
    {
    const int negalphainv=-13; //between -20 and -7 according to [Vitter87]
    //optimized for an implementation in 1987 !!!
    int curr=0, currsample=0;
    int threshold=-negalphainv*K;
    double Kreal=K, Kinv=1./Kreal, Nreal=N;
    double Vprime=exp(log(uni())*Kinv);
    int qu1=N+1-K; double qu1real=qu1;
    double Kmin1inv, X, U, negSreal, y1, y2, top, bottom;
    int S, limit;
    while ((K>1)&&(threshold<N))
        {
        Kmin1inv=1./(Kreal-1.);
        while(1)
            {//Step D2: generate X and U
            while(1)
                {
                X=Nreal*(1-Vprime);
                S=floor(X);
                if (S<qu1) {break;}
                Vprime=exp(log(uni())*Kinv);
                }
            U=uni();
            negSreal=-S;
            //step D3: Accept ?
            y1=exp(log(U*Nreal/qu1real)*Kmin1inv);
            Vprime=y1*(1. - X/Nreal)*(qu1real/(negSreal+qu1real));
            if (Vprime <=1.) {break;} //Accept ! Test [Vitter87](2.8) is true
            //step D4 Accept ?
            y2=0; top=Nreal-1.;
            if (K-1 > S)
                {bottom=Nreal-Kreal; limit=N-S;}
            else {bottom=Nreal+negSreal-1.; limit=qu1;}
            for(int t=N-1;t>=limit;t--)
                {y2*=top/bottom;top--; bottom--;}
            if (Nreal/(Nreal-X)>=y1*exp(log(y2)*Kmin1inv))
                {//Accept !
                Vprime=exp(log(uni())*Kmin1inv);
                break;
                }
            Vprime=exp(log(uni())*Kmin1inv);
            }
        // Step D5: Select the (S+1)th record
        currsample+=1+S;
        cout << curr << " : " << currsample << "\n";
        curr++;
        N-=S+1; Nreal+=negSreal-1.;
        K-=1; Kreal-=1; Kinv=Kmin1inv;
        qu1-=S; qu1real+=negSreal;
        threshold+=negalphainv;
        }
    if (K>1) {SequentialSamplesMethodA(K, N);}
    else {
        S=floor(N*Vprime);
        currsample+=1+S;
        cout << curr << " : " << currsample << "\n";
        }
    }


int main(void)
    {
    int Ntest=10000000, Ktest=Ntest/100;
    SequentialSamplesMethodD(Ktest,Ntest);
    return 0;
    }

$ time ./sampling|tail

私のラップトップで次の出力を与えます

99990 : 9998882
99991 : 9998885
99992 : 9999021
99993 : 9999058
99994 : 9999339
99995 : 9999359
99996 : 9999411
99997 : 9999427
99998 : 9999584
99999 : 9999745

real    0m0.075s
user    0m0.060s
sys 0m0.000s
于 2010-03-21T18:26:22.500 に答える
0

この Ruby コードは、貯水池サンプリング、アルゴリズム Rメソッドを示しています。各サイクルで、範囲n=5から一意のランダムな整数を選択します。[0,N=10)

t=0
m=0
N=10
n=5
s=0
distrib=Array.new(N,0)
for i in 1..500000 do
 t=0
 m=0
 s=0
 while m<n do

  u=rand()
  if (N-t)*u>=n-m then
   t=t+1
  else 
   distrib[s]+=1
   m=m+1
   t=t+1
  end #if
  s=s+1
 end #while
 if (i % 100000)==0 then puts i.to_s + ". cycle..." end
end #for
puts "--------------"
puts distrib

出力:

100000. cycle...
200000. cycle...
300000. cycle...
400000. cycle...
500000. cycle...
--------------
250272
249924
249628
249894
250193
250202
249647
249606
250600
250034

0 ~ 9 のすべての整数がほぼ同じ確率で選択されました。

これは本質的に、任意のシーケンスに適用されるクヌースのアルゴリズムです(実際、その答えにはこれの LISP バージョンがあります)。@MichaelCramer's answerに示されているように、アルゴリズムは時間的にO(N)であり、シーケンスがそれにストリーミングされている場合はメモリ内でO(1)になる可能性があります。

于 2012-09-04T15:09:38.383 に答える
0

Reservoir Sampling バージョンは非常に単純です。

my $N = 20;
my $k;
my @r;

while(<>) {
  if(++$k <= $N) {
    push @r, $_;
  } elsif(rand(1) <= ($N/$k)) {
    $r[rand(@r)] = $_;
  }
}

print @r;

これは、STDIN からランダムに選択された $N 行です。ファイルの行を使用していない場合は、 <>/$_ を別のものに置き換えますが、これは非常に簡単なアルゴリズムです。

于 2008-10-01T18:01:56.703 に答える
-1

これは、追加のストレージなしで O(N) で行う方法です。これは純粋にランダムな分布ではないと確信していますが、おそらく多くの用途には十分近いでしょう。

/* generate N sorted, non-duplicate integers in [0, max[  in O(N))*/
 int *generate(int n, int max) {
    float step,a,v=0;
    int i;    
    int *g = (int *)calloc(n, sizeof(int));
    if ( ! g) return 0;

    for (i=0; i<n; i++) {
        step = (max-v)/(float)(n-i);
        v+ = floating_pt_random_in_between(0.0, step*2.0);
        if ((int)v == g[i-1]){
          v=(int)v+1;             //avoid collisions
        }
        g[i]=v;
    }
    while (g[i]>max) {
      g[i]=max;                   //fix up overflow
      max=g[i--]-1;
    }
    return g;
 }
于 2008-10-01T18:16:33.437 に答える
-3

これはPerlコードです。grep はフィルターです。いつものように、このコードはテストしていません。

@list = grep ($_ % I) == 0, (0..N);
  • I = インターバル
  • N = 上限

モジュラス演算子を使用して、間隔に一致する数値のみを取得します。

@list = grep ($_ % 3) == 0, (0..30);

0、3、6、... 30 を返します

これは疑似 Perl コードです。コンパイルするには、微調整が必​​要になる場合があります。

于 2008-10-01T17:31:23.047 に答える