web-dev-qa-db-ja.com

値の単一のランダムな組み合わせを選択するアルゴリズム?

y個の異なる値があり、そのうちのxをランダムに選択したいとします。これを行うための効率的なアルゴリズムは何ですか? Rand()x回呼び出すこともできますが、xyが大きいと、パフォーマンスが低下します。

ここではcombinationsが必要であることに注意してください。各値は同じ確率で選択される必要がありますが、結果の順序は重要ではありません。確かに、 permutations を生成するアルゴリズムはすべて適格ですが、ランダムな順序を必要とせずにこれをより効率的に行うことができるかどうか疑問に思います。

から上限NまでのK個の非反復整数のリストを効率的に生成するにはどうすればよいですか この順列の場合をカバーします。

36
user259175

Robert Floydは、まさにそのような状況のためのサンプリングアルゴリズムを発明しました。 O(y)ストレージを必要としないため、シャッフルしてから最初のx要素を取得するよりも一般的に優れています。最初に記述されたように、1..Nからの値を想定していますが、 0..Nを生成したり、生成した値をベクトル/配列などの添え字として処理するだけで、連続していない値を使用したりするのは簡単です。

Pseuocodeでは、アルゴリズムは次のように実行されます(Jon BentleyのProgramming Pearls列「AsampleofBrilliance」から盗む)。

initialize set S to empty
for J := N-M + 1 to N do
    T := RandInt(1, J)
    if T is not in S then
        insert T in S
    else
        insert J in S

その最後のビット(TがすでにSにある場合はJを挿入する)は注意が必要な部分です。肝心なのは Jを挿入する正しい数学的確率を保証する であるため、偏りのない結果が生成されます。

それはO(x)1 およびO(1)yに関して、O(x)ストレージ。

問題の combinations タグに従って、アルゴリズムは、結果に発生する各要素の確率が等しいことのみを保証し、その相対的な順序は保証しないことに注意してください。


1O(x2関係するハッシュマップの最悪の場合、すべての値が同じハッシュを持つ事実上存在しない病的なケースであるため、無視できます。

60
Jerry Coffin

順序もランダムにしたい(またはランダムであってもかまわない)と仮定すると、切り捨てられたフィッシャー-イェーツシャッフルを使用します。シャッフルアルゴリズムを開始しますが、すべてのxを「ランダムに選択」するのではなく、最初のy値を選択したら停止します。

フィッシャー-イェーツは次のように機能します。

  • 要素をランダムに選択し、配列の最後にある要素と交換します。
  • 最後の要素を除いて、配列の残りの部分を繰り返します(または繰り返す可能性が高くなります)。

最初のステップの後のステップは、配列の最後の要素を変更しません。最初の2つの後の手順は、最後の2つの要素には影響しません。最初のxの後のステップは、最後のx要素には影響しません。したがって、その時点で停止できます。配列の上部には、均一にランダムに選択されたデータが含まれています。配列の下部にはややランダム化された要素が含まれていますが、それらから得られる順列は均一に分散されていません。

もちろん、これは入力配列をゴミ箱に捨てたことを意味します-開始する前にそのコピーをとる必要があり、xがyに比べて小さい場合、配列全体をコピーすることはあまり効率的ではありません。ただし、今後使用するのがさらに選択するだけの場合は、多少ランダムな順序であるという事実は問題ではなく、もう一度使用することができます。したがって、選択を複数回行う場合は、最初に1つのコピーしか実行できず、コストを償却できる可能性があります。

11
Steve Jessop

本当に生成する必要があるのが combinations -要素の順序が重要でない場合 combinadics をそのまま使用できます ここでJames McCaffreyによって実装されています

これを k-permutations と比較してください。ここでは、要素の順序が重要です。

最初のケースでは(1,2,3)(1,3,2)(2,1,3)(2,3,1)(3,1,2)(3,2,1)は同じと見なされます-後者では、同じ要素が含まれていますが、別個のものと見なされます。

組み合わせが必要な場合は、実際には1つの乱数を生成するだけで済みます(少し大きくなる可能性があります)。これを直接使用して、m番目の組み合わせを見つけることができます。この乱数は特定の組み合わせのインデックスを表すため、乱数は0から C(n、k) の間にある必要があります。コンビナディックの計算にも時間がかかる場合があります。

トラブルの価値がないかもしれません ジェリーとフェデリコの答え 以外に、コンビナディックを実装するよりも確かに簡単です。ただし、本当に組み合わせだけが必要で、必要なランダムビットの正確な数を生成することにバグがあり、それ以上はない場合... ;-)

組み合わせとk順列のどちらが必要かは明確ではありませんが、後者のC#コードを次に示します(はい、x> y/2の場合にのみ補集合を生成できますが、必要な組み合わせが残っているはずです。実際のk順列を取得するためにシャッフルされます):

static class TakeHelper
{
    public static IEnumerable<T> TakeRandom<T>(
        this IEnumerable<T> source, Random rng, int count)
    {
        T[] items = source.ToArray();

        count = count < items.Length ? count : items.Length;

        for (int i = items.Length - 1 ; count-- > 0; i--)
        {
            int p = rng.Next(i + 1);
            yield return items[p];
            items[p] = items[i];
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Random rnd = new Random(Environment.TickCount);
        int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7 };
        foreach (int number in numbers.TakeRandom(rnd, 3))
        {
            Console.WriteLine(number);
        }
    }
}

k-permutationsを生成する別のより複雑な実装は、私が横になっていたものであり、結果を反復するだけでよい場合は、ある意味で既存のアルゴリズムよりも改善されていると思います。また、x乱数を生成する必要がありますが、O(min(y/2、x))メモリのみを使用します。プロセス:

    /// <summary>
    /// Generates unique random numbers
    /// <remarks>
    /// Worst case memory usage is O(min((emax-imin)/2, num))
    /// </remarks>
    /// </summary>
    /// <param name="random">Random source</param>
    /// <param name="imin">Inclusive lower bound</param>
    /// <param name="emax">Exclusive upper bound</param>
    /// <param name="num">Number of integers to generate</param>
    /// <returns>Sequence of unique random numbers</returns>
    public static IEnumerable<int> UniqueRandoms(
        Random random, int imin, int emax, int num)
    {
        int dictsize = num;
        long half = (emax - (long)imin + 1) / 2;
        if (half < dictsize)
            dictsize = (int)half;
        Dictionary<int, int> trans = new Dictionary<int, int>(dictsize);
        for (int i = 0; i < num; i++)
        {
            int current = imin + i;
            int r = random.Next(current, emax);
            int right;
            if (!trans.TryGetValue(r, out right))
            {
                right = r;
            }
            int left;
            if (trans.TryGetValue(current, out left))
            {
                trans.Remove(current);
            }
            else
            {
                left = current;
            }
            if (r > current)
            {
                trans[r] = left;
            }
            yield return right;
        }
    }

一般的な考え方は、 フィッシャー-イェーツシャッフル および 順列の転置を記憶する を実行することです。それはどこにも公開されておらず、ピアレビューも受けていません。実用的な価値というよりは好奇心だと思います。それにもかかわらず、私は非常に批判を受け入れており、一般的に、何か問題があるかどうかを知りたいのですが、これを検討してください(そして反対票を投じる前にコメントを追加してください)。

2
Andras Vass

ちょっとした提案:x >> y/2の場合、ランダムにy-x要素を選択してから、相補セットを選択する方がおそらく良いでしょう。

1

これを行う簡単な方法は、YXよりもはるかに大きい場合にのみ非効率的です。

void randomly_select_subset(
    int X, int Y,
    const int * inputs, int X, int * outputs
) {
    int i, r;
    for( i = 0; i < X; ++i ) outputs[i] = inputs[i];
    for( i = X; i < Y; ++i ) {
        r = Rand_inclusive( 0, i+1 );
        if( r < i ) outputs[r] = inputs[i];
    }
}

基本的に、個別の値の最初のXを出力配列にコピーし、残りの値ごとに、その値を含めるかどうかをランダムに決定します。

乱数はさらに、置き換える(可変)出力配列の要素を選択するために使用されます。

0
BenGoldberg

秘訣は、 shuffle のバリエーション、つまり部分的なシャッフルを使用することです。

_function random_pick( a, n ) 
{
  N = len(a);
  n = min(n, N);
  picked = array_fill(0, n, 0); backup = array_fill(0, n, 0);
  // partially shuffle the array, and generate unbiased selection simultaneously
  // this is a variation on fisher-yates-knuth shuffle
  for (i=0; i<n; i++) // O(n) times
  { 
    selected = Rand( 0, --N ); // unbiased sampling N * N-1 * N-2 * .. * N-n+1
    value = a[ selected ];
    a[ selected ] = a[ N ];
    a[ N ] = value;
    backup[ i ] = selected;
    picked[ i ] = value;
  }
  // restore partially shuffled input array from backup
  // optional step, if needed it can be ignored
  for (i=n-1; i>=0; i--) // O(n) times
  { 
    selected = backup[ i ];
    value = a[ N ];
    a[ N ] = a[ selected ];
    a[ selected ] = value;
    N++;
  }
  return picked;
}
_

[〜#〜] note [〜#〜]アルゴリズムは厳密にO(n)時間と空間の両方、生成されます偏りのない選択(これは部分的な偏りのないシャッフル)および非破壊的(部分的なシャッフルのように)入力配列ですが、これはオプションです。

ここ から適応

更新

PRNG(疑似乱数ジェネレーター)への1回の呼び出しのみ in _[0,1]_ by IVAN STOJMENOVIC、 "ON RANDOM AND ADAPTIVE PARALLEL GENERATION OF組み合わせオブジェクト " (セクション)、O(N)(最悪の場合)の複雑さ

enter image description here

0
Nikos M.

たとえば、2 ^ 64の異なる値がある場合、対称鍵アルゴリズム(64ビットブロックを使用)を使用して、すべての組み合わせをすばやく再シャッフルできます。 (たとえば、Blowfish)。

for(i=0; i<x; i++)
   e[i] = encrypt(key, i)

これは純粋な意味でランダムではありませんが、目的に役立つ場合があります。暗号化技術に従って任意の数の個別の値を処理する場合は可能ですが、より複雑です。

0
sw.