web-dev-qa-db-ja.com

範囲[0..n-1]のm個の異なる乱数を生成する

[0..n-1]の範囲でm個の異なる乱数を生成する2つの方法があります

方法1:

//C++-ish pseudocode
int result[m];
for(i = 0; i < m; ++i)
{
   int r;
   do
   {
      r = Rand()%n;
   }while(r is found in result array at indices from 0 to i)
   result[i] = r;   
}

方法2:

//C++-ish pseudocode
int arr[n];
for(int i = 0; i < n; ++i)
    arr[i] = i;
random_shuffle(arr, arr+n);
result = first m elements in arr;

最初の方法は、nがmよりはるかに大きい場合により効率的ですが、2番目の方法はそれ以外の場合により効率的です。しかし、「はるかに大きい」というのはそれほど厳密な概念ではありませんか。 :)

質問:method1とmethod2のどちらがより効率的かを判断するには、nとmのどの式を使用すればよいですか? (実行時間の数学的期待値に関して)

32
Armen Tsirunyan

純粋な数学:
両方のケースでRand()関数呼び出しの量を計算して、結果を比較しましょう。

ケース1:すでにk個の数値を選択している場合の、ステップ_i = k_での呼び出しの数学的期待値を見てみましょう。 1つのRand()呼び出しで数値を取得する確率は、p = (n-k)/nと同じです。そのような呼び出しの数の数学的期待値を知る必要があります。これは、まだ持っていない数を取得することにつながります。

_1_呼び出しを使用して取得できる確率はpです。 _2_呼び出しの使用-_q * p_、ここで_q = 1 - p_。一般的に、n呼び出しの直後に取得される確率は_(q^(n-1))*p_です。したがって、数学的期待値は
Sum[ n * q^(n-1) * p ], n = 1 --> INF。この合計は_1/p_(wolfram alphaによって提供される)と同じです。

したがって、ステップ_i = k_では、1/p = n/(n-k)関数のRand()呼び出しを実行します。

全体をまとめてみましょう:

Sum[ n/(n - k) ], k = 0 --> m - 1 = n * T-メソッド1のRand呼び出しの数。
ここにT = Sum[ 1/(n - k) ], k = 0 --> m - 1

ケース2:

ここでRand()は_random_shuffle_ _n - 1_の内部で呼び出されます(ほとんどの実装)。

次に、メソッドを選択するために、これらの2つの値を比較する必要があります:_n * T ? n - 1_。
したがって、適切な方法を選択するには、上記のようにTを計算します。 T < (n - 1)/nの場合、最初のメソッドを使用することをお勧めします。それ以外の場合は、2番目の方法を使用します。

15

ウィキペディアの 元のフィッシャーイェーツアルゴリズム の説明を確認してください。それは本質的にあなたの方法1をn/2まで使用し、残りの方法2を使用することを提唱しています。

9
Mark Ransom

O(n)メモリおよびO(n)時間(nは返された結果の数であり、結果セットの場合は、選択元のセット)。ハッシュテーブルを使用するので、便宜上、Python)にあります。

def random_elements(num_elements, set_size):
    state = {}
    for i in range(num_elements):
        # Swap state[i] with a random element
        swap_with = random.randint(i, set_size - 1)
        state[i], state[swap_with] = state.get(swap_with, swap_with), state.get(i, i)
    return [state[i] for i in range(num_elements) # effectively state[:num_elements] if it were a list/array.

これは一部のfisher-yatesシャッフルであり、シャッフルされる配列はスパースハッシュテーブルとして実装されます-存在しない要素はすべてそのインデックスに等しくなります。最初のnum_elementsインデックスをシャッフルし、それらの値を返します。 set_size = 1,の場合、これは範囲内の乱数を選択することと同等であり、num_elements = set_sizeの場合、これは標準のfisher-yatesシャッフルと同等です。

これがO(n)時間であることに注意してください。ループの各反復でハッシュテーブルの最大2つの新しいインデックスが初期化されるため、O(n)スペースも。

6
Nick Johnson

個人的には、方法1を使用し、M> N/2の場合はN-M値を選択してから、配列を反転します(選択されなかった数値を返します)。したがって、たとえば、Nが1000で、そのうちの950が必要な場合は、方法1を使用して50個の値を選択してから、残りの950を返します。

編集:ただし、一貫したパフォーマンスが目標である場合は、変更された方法2を使用します。これは完全なシャッフルを実行せず、N長の配列の最初のM要素のみをシャッフルします。

int arr[n];
for(int i = 0; i < n; ++i)
    arr[i] = i;

for (int i =0; i < m; ++i) {
   int j = Rand(n-i); // Pick random number from 0 <= r < n-i.  Pick favorite method
   // j == 0 means don't swap, otherwise swap with the element j away
   if (j != 0) { 
      std::swap(arr[i], arr[i+j]);
   }
}
result = first m elements in arr;
6
Dave S

3番目の方法はどうですか?

int result[m];
for(i = 0; i < m; ++i)
{
   int r;
   r = Rand()%(n-i);
   r += (number of items in result <= r)
   result[i] = r;   
}

Edit<=である必要があります。衝突を回避するために実際には追加のロジックになります。

これは、Fisher-Yatesの Modern Method を使用した例の方が優れています。

//C++-ish pseudocode
int arr[n];
for(int i = 0; i < n; ++i)
    arr[i] = i;

for(i = 0; i < m; ++i)
    swap(arr, n-i, Rand()%(n-i) );

result = last m elements in arr;
3
Jacob Eggers

数学的な期待について言えば、それはかなり役に立たないですが、とにかくそれを投稿します:D

シャッフルは単純なO(m)です。

今、他のアルゴリズムはもう少し複雑です。次の数を生成するために必要なステップ数は、試行回数の期待値であり、試行の長さの確率は幾何分布です。そう...

p=1          E[X1]=1            = 1           = 1
p=1-1/n      E[x2]=1/(1-1/n)    = 1 + 1/(n-1) = 1 + 1/(n-1) 
p=1-2/n      E[x3]=1/(1-1/n)    = 1 + 2/(n-2) = 1 + 1/(n-2) + 1/(n-2)
p=1-3/n      E[X4]=1/(1-2/n)    = 1 + 3/(n-3) = 1 + 1/(n-3) + 1/(n-3) + 1(n-3)
....
p=1-(m-1)/n) E[Xm]=1/(1-(m-1)/n))

合計は三角形に分割できることに注意してください。右側を参照してください。

調和級数の公式を使用してみましょう:H_n = Sum k = 0-> n(1/k)=およそln(k)

Sum(E[Xk]) = m + ln(n-1)-ln(n-m-1) + ln(n-2)-ln(n-m-1) + ... = m + ln(n-1) + ln(n-2) + ... - (m-1)*ln(n-m-1) ..

そして、あなたがまだに興味があるなら、調和系列の和のためのいくつかのフォーラムがあり、私はそれを調べます...

更新:実際にはかなりいい式です(見事なコンクリート数学の本のおかげです)

Sum(H_k) k=0->n = n*H_n - n

したがって、予想されるステップ数:

Sum(E[Xk]) = m + (n-1)*ln(n-1) - (n-1) - (n-m-1)*ln(n-m-1) - (n-m-1)) - (m-1)*ln(n-m-1).

注:まだ確認していません。

2
Karoly Horvath

これは少し長いショットですが、システムによってはうまくいくかもしれません。

  1. 0.5のような妥当な比率から始めます。
  2. リクエストが届いたら、しきい値比率の現在の値から取得した方法のいずれかでリクエストを処理します。
  3. 所要時間を記録し、「空」の時間がある場合は、他の方法で同じタスクを実行します。
  4. 代替ソリューションが元のソリューションよりもはるかに速い場合は、しきい値を上下に調整します。

この方法の明らかな欠点は、非常に変動する負荷システムでは、「オフライン」テストの信頼性が高すぎないことです。

1
biziclop

配列の代わりに set を使用するのはどうですか、配列よりもはるかに簡単だと思います

set<int> Numbers;
while (Numbers.size() < m) {
   Numbers.insert(Rand() % n);
}
1
Hani Shams

フィッシャーイェイツのシャッフルが提案されました。次のコードが均等に分散された整数を生成するかどうかはわかりませんが、少なくともコンパクトで1パスです:

std::random_device rd;
std::mt19937 g(rd());
for (size_type i = 1; i < std::size(v); ++i) {
    v[i] = std::exchange(v[g() % i], i);
}
0