重み付けされたランダムなアイテムを取得する


51

例えば、私はこの表を持っています

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

ランダムなフルーツを返す必要があります。ただし、リンゴレモンの 4倍、オレンジの 2倍の頻度で摘み取る必要があります。

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

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

または、Rubyにすぐに使える宝石がありますか?:)

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


11
これは、ディアブロでランダム戦利品を取得するための同じ式である必要があります:
ジャレイン

1
@Jalayn:実際、以下の私の答えの間隔ソリューションのアイデアは、World of Warcraftの戦闘テーブルについて覚えていることから来ています。:-D
ベンジャミン修道院



私はいくつかの単純な重み付けランダムアルゴリズムを実装しました。質問があれば教えてください。
レオニードガネリン

回答:


50

概念的に最も簡単な解決策は、各要素がその重みの数だけ出現するリストを作成することです。

fruits = [apple, apple, apple, apple, orange, orange, lemon]

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


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

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

    intervals = [4, 6, 7]

    4未満のインデックスはリンゴ、4〜6未満はオレンジ、6〜7未満はレモンを表します。

  2. nの範囲の乱数を生成0sum(weights)ます。

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

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

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


2
間隔の解決策はいいようです
-Jalayn

1
これは私の最初の考えでした:)。しかし、100個の果物を含むテーブルがあり、重量が約10kである場合はどうなりますか?それは非常に大きな配列になり、これは私が望むほど効率的ではありません。これが最初の解決策です。2番目の解決策は良さそうです
-fl00r


1
alias methodはこれ処理するための事実上の方法です。alias method無視しながら、同じコードを何度も繰り返す投稿の数に正直に驚いています。 神のために、あなたは一定時間のパフォーマンスを得ます!
opa

30

これは、任意のシーケンスからランダムに重み付けされた要素を選択できるアルゴリズム(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場合(または、このカウンターにもっと大きなタイプを選択できる場合)、わいせつに大きいシーケンスでも機能します


2
一度反復するからといってそれが良いと仮定する前に、これをベンチマークします。同じくらい多くのランダムな値を生成することも、正確ではありません。
ジャン=ベルナルドペラン

1
@ Jean-Bernard Pellerin私がやったが、実際には大きなリストの方が速い。暗号的に強力なランダムジェネレーターを使用しない限り(-8
Nevermind

受け入れられた答えでなければなりません。これは、「間隔」および「繰り返し入力」のアプローチよりも優れています。
ヴィヴィンパリアス

2
このメソッドを使用するために、ここ数年で3〜4回このスレッドに戻ってきたと言いたいだけです。この方法は、目的に応じて必要な答えを迅速に提供することに繰り返し成功しています。使用するために戻ってくるたびに、この答えに賛成できればと思います。
ジムヤーブロ16

1
本当に1回だけ選択する必要がある場合に最適なソリューションです。それ以外の場合、最初の回答でソリューションの準備作業を1回行うと、はるかに効率的です。
デュプリケータ

22

すでに存在する答えは良いです、そして私はそれらを少し拡張します。

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

+------------------------+
| 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)に削減できます。


バイナリ検索の良い点
-fl00r

Nevermindの答えは余分なスペースを必要としないため、O(1)ですが、乱数を繰り返し生成し、重み関数を評価することで実行時の複雑さを追加します(基になる問題によっては、コストがかかる可能性があります)。
ベンジャミンクロスター

1
私のコードの「読みやすいバージョン」であると主張するものは、実際にはそうではありません。コードでは、事前に重みの合計と累積合計を知る必要があります。私はしません。
ネヴァーマインド

@Benjamin Kloster私のコードは要素ごとに一度だけ重み関数を呼び出します-それ以上のことはできません。ただし、乱数については正しいです。
ネバーマインド

@Nevermind:pick-functionの呼び出しごとに1回だけ呼び出すため、ユーザーが2回呼び出すと、各要素に対してweight関数が再度呼び出されます。もちろん、キャッシュすることはできますが、スペースの複雑さのためにO(1)ではなくなります。
ベンジャミンクロスター

8

これは単純な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に正規化することで実現できます。
  • 次に、ルーレットのホイールが回転する方法と同様に、ランダムに選択されます。

ルーレットホイールの選択

典型的なアルゴリズムの複雑さはO(N)またはO(log N)ですが、O(1)も可能です(たとえば、確率的受け入れによるルーレットホイールの選択)。


この画像の元のソースが何であるか知っていますか?論文に使用したいのですが、帰属を確認する必要があります。
マルコムマクラウド

@MalcolmMacLeod申し訳ありませんが、多くのGAの論文/サイトで使用されていますが、著者が誰なのかわかりません。
マンリオ

0

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

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を返します。これは、指定された配列の「リンゴ」のインデックスです。

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

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

2
プログラマは、概念的な質問と回答は、物事を説明することが期待されています。説明の代わりにコードダンプをスローすることは、IDEからホワイトボードにコードをコピーするようなものです。見た目はよく、時には理解できるかもしれませんが、奇妙に感じます...ただ奇妙です。ホワイトボードにはコンパイラがありません
-gnat

あなたは正しい、私はコードに焦点を当てていたので、それがどのように機能するかを伝えるのを忘れていました。それがどのように機能するかについての説明を追加します。
ラマザンポラット
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.