[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のどの式を使用すればよいですか? (実行時間の数学的期待値に関して)
純粋な数学:
両方のケースで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番目の方法を使用します。
ウィキペディアの 元のフィッシャーイェーツアルゴリズム の説明を確認してください。それは本質的にあなたの方法1をn/2まで使用し、残りの方法2を使用することを提唱しています。
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)スペースも。
個人的には、方法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;
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;
数学的な期待について言えば、それはかなり役に立たないですが、とにかくそれを投稿します: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).
注:まだ確認していません。
これは少し長いショットですが、システムによってはうまくいくかもしれません。
この方法の明らかな欠点は、非常に変動する負荷システムでは、「オフライン」テストの信頼性が高すぎないことです。
配列の代わりに set を使用するのはどうですか、配列よりもはるかに簡単だと思います
set<int> Numbers;
while (Numbers.size() < m) {
Numbers.insert(Rand() % n);
}
フィッシャーイェイツのシャッフルが提案されました。次のコードが均等に分散された整数を生成するかどうかはわかりませんが、少なくともコンパクトで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);
}