获得加权随机项目


51

我有这个桌子

+ ----------------- +
| 水果 重量
+ ----------------- +
| 苹果| 4 |
| 橙色| 2 |
| 柠檬| 1 |
+ ----------------- +

我需要归还随机水果。但是苹果的采摘频率应该是柠檬的 4倍和橙子的 2倍。

在更一般的情况下,它应该f(weight)经常出现。

什么是实现此行为的良好通用算法?

也许在Ruby上有一些现成的宝石?:)

PS
我已经在Ruby https://github.com/fl00r/pickup中实现了当前算法


11
那应该与在暗黑破坏神中获得随机战利品的公式相同:-)
Jalayn

1
@Jalayn:实际上,下面我回答中的间隔解决方案的想法来自我对《魔兽世界》中战斗表的记忆。:-D
本杰明·克洛斯特



我已经实现了几种简单的加权随机算法。如果您有任何问题,请告诉我。
Leonid Ganeline,2015年

Answers:


50

从概念上讲,最简单的解决方案是创建一个列表,其中每个元素的出现次数与其权重相同,因此

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

然后使用您可以使用的任何功能从该列表中选择一个随机元素(例如,在适当范围内生成一个随机索引)。当然,这不是很有效的存储,需要整数权重。


另一种稍微复杂一些的方法如下所示:

  1. 计算权重的累加和:

    intervals = [4, 6, 7]

    低于4的指数代表苹果,4至6以下的橘子和6至7以下的柠檬

  2. 生成一个n介于0到的随机数sum(weights)

  3. 查找最后一个项,其总和大于n。相应的水果就是您的结果。

与第一种方法相比,此方法需要更复杂的代码,但需要较少的内存和计算量,并且支持浮点权重。

对于这两种算法,对于任意数量的随机选择,设置步骤都可以执行一次。


2
间隔解决方案看起来不错
Jalayn

1
这是我的第一个想法:)。但是,如果我有100个水果的桌子并且重量可能是1万左右,该怎么办?这将是非常大的数组,并且效率不如我想要的那样。这是第一个解决方案。第二种解决方案看起来不错
fl00r 2012年


1
别名方法是处理此问题的事实上的方法,我真的惊讶于一遍又一遍地重复相同代码的帖子的数量,而忽略了别名方法看在上帝的份上,您将获得恒定的性能!
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. 
}

这是基于以下推理:让我们选择序列的第一个元素作为“当前结果”;然后,在每次迭代中,保留它或丢弃它,然后选择新元素作为当前元素。我们可以计算出最终将要选择的任何给定元素的概率,作为该元素不会在后续步骤中丢弃的所有概率的乘积,再乘以该元素首先被选择的概率。如果进行数学计算,您会发现该乘积简化为(元素的权重)/(所有权重的总和),这正是我们所需要的!

由于此方法仅对输入序列进行一次迭代,因此即使权重之和适合一个int(或您可以为该计数器选择更大的类型),它也适用于非常大的序列


2
我会先做基准测试,然后再假设它只迭代一次就更好了。生成同样多的随机值也不是很快。
Jean-Bernard Pellerin 2013年

1
@ Jean-Bernard Pellerin我做到了,在大型列表中,它实际上更快。除非您使用加密强度高的随机生成器(-8
Nevermind 2013年

应该是imo接受的答案。我比“间隔”和“重复进入”方法更喜欢这种方法。
Vivin Paliath,2015年

2
我只是想说在过去的几年中我已经回到该线程3或4次以使用此方法。这种方法已经多次成功地为我的目的提供了我需要的足够快的答案。我希望我每次回来使用该答案时都可以投票赞成。
Jim Yarbro

1
如果您真的只需要选择一次,则是不错的解决方案。否则,为第一个答案中的解决方案做一次准备工作效率会大大提高。
Deduplicator

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];
}

还有一个有关更新权重的故事。在最坏的情况下,更新一个元素的权重会导致更新所有元素的累积和,从而将更新复杂度增加到O(n)。也可以使用二进制索引树将其缩减为O(log n)


关于二进制搜索的要点
fl00r 2012年

Nevermind的答案不需要额外的空间,因此它是O(1),但是通过重复生成随机数并评估权重函数(取决于潜在的问题,可能会很昂贵)而增加了运行时复杂性。
本杰明·克洛斯特

1
实际上,您声称不是我的代码的“可读性更高”的版本。您的代码需要提前知道权重的总和和累计和。我的不是。
没关系

@Benjamin Kloster我的代码仅对每个元素调用一次权重函数-您不能做得更好。不过,您对随机数是正确的。
没关系

@Nevermind:每次调用pick函数仅调用一次,因此,如果用户调用两次,则每个元素都会再次调用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论文/网站都使用了它,但我不知道作者是谁。
manlio

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,这是给定数组的“ 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

2
程序员是关于概念性问题的,答案应能解释问题。抛出代码转储而不是进行解释就像将代码从IDE复制到白板一样:它看起来很熟悉,甚至有时是可以理解的,但是感觉很奇怪……只是很奇怪。白板没有编译器
2015年

没错,我只专注于代码,所以我忘了告诉它如何工作。我将添加有关其工作原理的说明。
Ramazan Polat 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.