web-dev-qa-db-ja.com

加重ランダムアイテムを取得する

たとえば、私はこのテーブルを持っています

 + ----------------- + 
 |フルーツ|重量| 
 + ----------------- + 
 | Apple | 4 | 
 |オレンジ| 2 | 
 |レモン| 1 | 
 + ----------- ------ + 

ランダムな果物を返す必要があります。ただし、AppleLemonの4倍の頻度で、orange

より一般的なケースでは、それは頻繁にf(weight)倍になるはずです。

この動作を実装するための優れた一般的なアルゴリズムは何ですか?

それともRubyにいくつかの準備ができている宝石がありますか? :)

[〜#〜] ps [〜#〜]
Ruby https://github.com/fl00r/pickup に現在のアルゴリズムを実装しました

50
fl00r

概念的に最も簡単な解決策は、各要素がその重みと同じ回数発生するリストを作成することです。

_fruits = [Apple, Apple, Apple, Apple, orange, orange, lemon]
_

次に、自由に使用できる関数を使用して、そのリストからランダムな要素を選択します(たとえば、適切な範囲内でランダムなインデックスを生成します)。もちろん、これはあまりメモリ効率が良くなく、整数の重みが必要です。


別の少し複雑なアプローチは次のようになります。

  1. 重みの累積合計を計算します。

    _intervals = [4, 6, 7]
    _

    4未満のインデックスはAppleを表し、4から6未満はorangeを表し、6から7未満はlemonを表します。

  2. _0_からsum(weights)の範囲の乱数nを生成します。

  3. 累積合計がnを超える最後のアイテムを見つけます。対応する果物はあなたの結果です。

このアプローチは、最初のものよりも複雑なコードを必要としますが、メモリと計算は少なく、浮動小数点の重みをサポートします。

どちらのアルゴリズムでも、任意の数のランダム選択に対してセットアップ手順を1回実行できます。

49

これは、任意のシーケンスからランダムに重み付けされた要素を選択できるアルゴリズム(C#で)です。

public static T Random<T>(this IEnumerable<T> enumerable, Func<T, int> weightFunc)
{
    int totalWeight = 0; // this stores sum of weights of all elements before current
    T selected = default(T); // currently selected element
    foreach (var data in enumerable)
    {
        int weight = weightFunc(data); // weight of current element
        int r = Random.Next(totalWeight + weight); // random value
        if (r >= totalWeight) // probability of this is weight/(totalWeight+weight)
            selected = data; // it is the probability of discarding last selected element and selecting current one instead
        totalWeight += weight; // increase weight sum
    }

    return selected; // when iterations end, selected is some element of sequence. 
}

これは、次の理由に基づいています。シーケンスの最初の要素を「現在の結果」として選択しましょう。次に、各反復で、それを保持するか、破棄して新しい要素を現在のものとして選択します。すべての確率の結果として最終的に選択される特定の要素の確率を計算できますされない後続のステップで破棄され、最初に選択される確率を掛けます。計算を行うと、この要素は(要素の重み)/(すべての重みの合計)に単純化されることがわかります。これはまさに私たちが必要としていることです!

このメソッドは入力シーケンスを1回だけ反復するので、重みの合計がintに収まる場合は、わいせつに大きなシーケンスでも機能します(または、このカウンターにより大きいタイプを選択できます)。

30
Nevermind

すでに存在する答えは良いので、少し詳しく説明します。

ベンジャミンが示唆したように、このような問題では通常、累積合計が使用されます。

+------------------------+
| fruit  | weight | csum |
+------------------------+
| Apple  |   4    |   4  |
| orange |   2    |   6  |
| lemon  |   1    |   7  |
+------------------------+

この構造でアイテムを見つけるには、Nevermindのコードのようなものを使用できます。私が通常使用するこのC#コードの一部:

double r = Random.Next() * totalSum;
for(int i = 0; i < fruit.Count; i++)
{
    if (csum[i] > r)
        return fruit[i];
}

次に、興味深い部分について説明します。このアプローチはどの程度効率的ですか、最も効率的なソリューションは何ですか?私のコードはO(n)メモリを必要とし、O(n)時間で実行されます。 O(n)未満のスペースでは実行できないと思いますが、時間の複雑さはO(log n) 実際には。トリックは、通常のforループの代わりにバイナリ検索を使用することです。

double r = Random.Next() * totalSum;
int lowGuess = 0;
int highGuess = fruit.Count - 1;

while (highGuess >= lowGuess)
{
    int guess = (lowGuess + highGuess) / 2;
    if ( csum[guess] < r)
        lowGuess = guess + 1;
    else if ( csum[guess] - weight[guess] > r)
        highGuess = guess - 1;
    else
        return fruit[guess];
}

重みの更新についてのストーリーもあります。最悪の場合、1つの要素の重みを更新すると、すべての要素の累積合計が更新され、更新の複雑さがO(n)に増加します。それも バイナリインデックスツリー を使用してO(log n)に削減できます。

21
Emperor Orionii

これは単純なPython実装です:

from random import random

def select(container, weights):
    total_weight = float(sum(weights))
    rel_weight = [w / total_weight for w in weights]

    # Probability for each element
    probs = [sum(rel_weight[:i + 1]) for i in range(len(rel_weight))]

    slot = random()
    for (i, element) in enumerate(container):
        if slot <= probs[i]:
            break

    return element

そして

population = ['Apple','orange','lemon']
weights = [4, 2, 1]

print select(population, weights)

遺伝的アルゴリズムでは、この選択手順は フィットネス比例選択 または ルーレットホイール選択 と呼ばれます。

  • ホイールの比率は、ウェイト値に基づいて可能な選択のそれぞれに割り当てられます。これは、セレクションの重みをすべてのセレクションの合計の重みで割ることにより達成でき、それによりそれらを1に正規化します。
  • 次に、ルーレットホイールを回転させる方法と同じようにランダムに選択します。

Roulette wheel selection

典型的なアルゴリズムはO(N)またはO(log N)の複雑さを持っていますが、O(1)(eg Roulette-wheel確率的受け入れによる選択 )。

8
manlio

この要点 は、まさにあなたが求めていることを実行しています。

public static Random random = new Random(DateTime.Now.Millisecond);
public int chooseWithChance(params int[] args)
    {
        /*
         * This method takes number of chances and randomly chooses
         * one of them considering their chance to be choosen.    
         * e.g. 
         *   chooseWithChance(0,99) will most probably (%99) return 1
         *   chooseWithChance(99,1) will most probably (%99) return 0
         *   chooseWithChance(0,100) will always return 1.
         *   chooseWithChance(100,0) will always return 0.
         *   chooseWithChance(67,0) will always return 0.
         */
        int argCount = args.Length;
        int sumOfChances = 0;

        for (int i = 0; i < argCount; i++) {
            sumOfChances += args[i];
        }

        double randomDouble = random.NextDouble() * sumOfChances;

        while (sumOfChances > randomDouble)
        {
            sumOfChances -= args[argCount -1];
            argCount--;
        }

        return argCount-1;
    }

あなたはそれをそのように使うことができます:

string[] fruits = new string[] { "Apple", "orange", "lemon" };
int choosenOne = chooseWithChance(98,1,1);
Console.WriteLine(fruits[choosenOne]);

上記のコードは、おそらく(%98)が0を返します。これは、指定された配列の「Apple」のインデックスです。

また、このコードは上記のメソッドをテストします。

Console.WriteLine("Start...");
int flipCount = 100;
int headCount = 0;
int tailsCount = 0;

for (int i=0; i< flipCount; i++) {
    if (chooseWithChance(50,50) == 0)
        headCount++;
    else
        tailsCount++;
}

Console.WriteLine("Head count:"+ headCount);
Console.WriteLine("Tails count:"+ tailsCount);

次のような出力が得られます。

Start...
Head count:52
Tails count:48
0
Ramazan Polat