从阵列加权随机选择

时间:2010-12-16 17:20:44

标签: arrays algorithm random

我想从数组中随机选择一个元素,但每个元素都有一个已知的选择概率。

所有机会(在阵列中)总和为1。

您认为哪种算法最快,最适合大型计算?

示例:

id => chance
array[
    0 => 0.8
    1 => 0.2
]

对于此伪代码,有问题的算法应该在多个调用上统计返回id 0上的四个元素,用于id 1上的一个元素。

14 个答案:

答案 0 :(得分:67)

计算列表的离散累积密度函数(CDF) - 或者简单地计算权重的累积和数组。然后生成一个介于0和所有权重之和(在你的情况下可能是1)的范围内的随机数,进行二进制搜索以在离散CDF数组中找到该随机数并获得与该条目对应的值 - 这是你的加权随机数。

答案 1 :(得分:13)

算法很简单

rand_no = rand(0,1)
for each element in array 
     if(rand_num < element.probablity)
          select and break
     rand_num = rand_num - element.probability

答案 2 :(得分:8)

我发现this article对于完全理解这个问题最有用。 This stackoverflow question也可能是您正在寻找的内容。

我认为最佳解决方案是使用Alias Method (wikipedia)。 它需要 O(n)时间来初始化, O(1)时间来进行选择,以及 O(n)内存。

这是用于生成滚动加权 n - 侧面骰子的结果的算法(从这里,从长度 n 数组中选择一个元素是微不足道的)从this article开始。 作者假设您具有滚动公平骰子(floor(random() * n))和翻转有偏见硬币(random() < p)的功能。

  

算法:Vose的别名方法

     

初始化:

     
      
  1. 创建数组 Alias Prob ,每个 n
  2.   
  3. 创建两个工作清单,
  4.   
  5. 将每个概率乘以 n
  6.   
  7. 对于每个缩放概率 p i :      
        
    1. 如果 p i &lt; 1 ,将 i 添加到 Small
    2.   
    3. 否则( p i ≥1),将 i 添加到 Large
    4.   
  8.   
  9. 不为空:(可能先清空)      
        
    1. Small 中删除第一个元素;称之为 l
    2.   
    3. Large 中删除第一个元素;称之为 g
    4.   
    5. 设置 Prob [l] = p l
    6.   
    7. 设置别名[l] = g
    8.   
    9. 设置 p g :=(p g + p l ) - 1 。 (这是一个更加数字稳定的选择。)
    10.   
    11. 如果 p g &lt; 1 ,请将 g 添加到 Small
    12.   
    13. 否则( p g ≥1),将 g 添加到 Large
    14.   
  10.   
  11. Large 不为空时:      
        
    1. Large 中删除第一个元素;称之为 g
    2.   
    3. 设置 Prob [g] = 1
    4.   
  12.   
  13. 虽然 Small 不为空:这只能由于数值不稳定而成为可能。      
        
    1. Small 中删除第一个元素;称之为 l
    2.   
    3. 设置 Prob [l] = 1
    4.   
  14.         

    代:

         
        
    1. n 侧面模具生成合理的模具辊;打电话给
    2.   
    3. 以概率 Prob [i] 翻转出现在头上的有偏见的硬币。
    4.   
    5. 如果硬币出现&#34;头部,&#34;返回
    6.   
    7. 否则,请返回 Alias [i]
    8.   

答案 3 :(得分:6)

ruby​​中的一个例子

#each element is associated with its probability
a = {1 => 0.25 ,2 => 0.5 ,3 => 0.2, 4 => 0.05}

#at some point, convert to ccumulative probability
acc = 0
a.each { |e,w| a[e] = acc+=w }

#to select an element, pick a random between 0 and 1 and find the first   
#cummulative probability that's greater than the random number
r = rand
selected = a.find{ |e,w| w>r }

p selected[0]

答案 4 :(得分:6)

这可以在每个样本的O(1)预期时间内完成,如下所示。

计算每个元素i的CDF F(i)是小于或等于i的概率之和。

将元素i的范围r(i)定义为区间[F(i - 1),F(i)]。

对于每个区间[(i-1)/ n,i / n],创建一个桶,其中包含范围与区间重叠的元素列表。只要您非常小心,整个阵列总共花费O(n)时间。

当您随机抽样数组时,您只需计算随机数所在的存储桶,并与列表中的每个元素进行比较,直到找到包含它的时间间隔。

样本的成本是O(随机选择列表的预期长度)&lt; = 2。

答案 5 :(得分:5)

另一个Ruby示例:

def weighted_rand(weights = {})
  raise 'Probabilities must sum up to 1' unless weights.values.inject(&:+) == 1.0
  raise 'Probabilities must not be negative' unless weights.values.all? { |p| p >= 0 }
  # Do more sanity checks depending on the amount of trust in the software component using this method
  # E.g. don't allow duplicates, don't allow non-numeric values, etc.

  # Ignore elements with probability 0
  weights = weights.reject { |k, v| v == 0.0 }   # e.g. => {"a"=>0.4, "b"=>0.4, "c"=>0.2}

  # Accumulate probabilities and map them to a value
  u = 0.0
  ranges = weights.map { |v, p| [u += p, v] }   # e.g. => [[0.4, "a"], [0.8, "b"], [1.0, "c"]]

  # Generate a (pseudo-)random floating point number between 0.0(included) and 1.0(excluded)
  u = rand   # e.g. => 0.4651073966724186

  # Find the first value that has an accumulated probability greater than the random number u
  ranges.find { |p, v| p > u }.last   # e.g. => "b"
end

使用方法:

weights = {'a' => 0.4, 'b' => 0.4, 'c' => 0.2, 'd' => 0.0}

weighted_rand weights

期待什么:

d = 1000.times.map{ weighted_rand weights }
d.count('a') # 396
d.count('b') # 406
d.count('c') # 198

答案 6 :(得分:3)

使用pickup gem的Ruby解决方案:

require 'pickup'

chances = {0=>80, 1=>20}
picker = Pickup.new(chances)

示例:

5.times.collect {
  picker.pick(5)
}

给出了输出:

[[0, 0, 0, 0, 0], 
 [0, 0, 0, 0, 0], 
 [0, 0, 0, 1, 1], 
 [0, 0, 0, 0, 0], 
 [0, 0, 0, 0, 1]]

答案 7 :(得分:2)

如果数组很小,我会给数组一个长度,在本例中为5,并根据需要赋值:

array[
    0 => 0
    1 => 0
    2 => 0
    3 => 0
    4 => 1
]

答案 8 :(得分:2)

这是我在制作中使用的PHP代码:

/**
 * @return \App\Models\CdnServer
*/
protected function selectWeightedServer(Collection $servers)
{
    if ($servers->count() == 1) {
        return $servers->first();
    }

    $totalWeight = 0;

    foreach ($servers as $server) {
        $totalWeight += $server->getWeight();
    }

    // Select a random server using weighted choice
    $randWeight = mt_rand(1, $totalWeight);
    $accWeight = 0;

    foreach ($servers as $server) {
        $accWeight += $server->getWeight();

        if ($accWeight >= $randWeight) {
            return $server;
        }
    }
}

答案 9 :(得分:1)

诀窍可能是对具有反映概率的元素重复的辅助数组进行采样

给出与其概率相关的元素,如百分比:

h = {1 => 0.5, 2 => 0.3, 3 => 0.05, 4 => 0.05 }

auxiliary_array = h.inject([]){|memo,(k,v)| memo += Array.new((100*v).to_i,k) }   

ruby-1.9.3-p194 > auxiliary_array 
 => [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,                                 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4] 

auxiliary_array.sample

如果你想尽可能通用,你需要根据最大小数位数来计算乘数,并在100的位置使用它:

m = 10**h.values.collect{|e| e.to_s.split(".").last.size }.max

答案 10 :(得分:1)

“命运之轮” O(n),仅用于小型阵列:

function pickRandomWeighted(array, weights) {
    var sum = 0;
    for (var i=0; i<weights.length; i++) sum += weights[i];
    for (var i=0, pick=Math.random()*sum; i<weights.length; i++, pick-=weights[i])
        if (pick-weights[i]<0) return array[i];
}

答案 11 :(得分:0)

我认为大于或等于0.8但小于1.0的数字会选择第三个元素。

换句话说:

x是0到1之间的随机数

如果0.0> = x&lt; 0.2:第1项

如果0.2> = x < 0.8:第2项

如果0.8> = x < 1.0:第3项

答案 12 :(得分:0)

我将在https://stackoverflow.com/users/626341/masciugo回答时改进。

基本上你制作一个大数组,其中元素显示的次数与权重成正比。

它有一些缺点。

  1. 重量可能不是整数。想象一下,元素1具有pi的概率,而元素2具有1-pi的概率。你怎么分开呢?或者想象一下,如果有数百个这样的元素。
  2. 创建的数组可能非常大。想象一下,如果最小公倍乘数是100万,那么我们需要在我们想要选择的数组中使用一个100万个元素的数组。
  3. 为了解决这个问题,这就是你所做的。

    创建此类数组,但只是随机插入一个元素。插入元素的概率与权重成正比。

    然后从平时选择随机元素。

    因此,如果有3个具有不同权重的元素,您只需从1-3个元素的数组中选择一个元素。

    如果构造的元素为空,则可能会出现问题。也就是说,恰好没有元素出现在数组中,因为它们的骰子滚动不同。

    在这种情况下,我建议插入元素的概率是p(插入)= wi / wmax。

    这样,将插入一个元素,即具有最高概率的元素。其他元素将按相对概率插入。

    假设我们有2个对象。

    元素1显示.20%的时间。 元素2显示了0.40%的时间并具有最高的概率。

    在该阵列中,元素2将一直显示出来。元素1将显示一半的时间。

    因此,元素2将被称为元素1的2倍。为了通用性,所有其他元素将被称为与其权重成比例。所有概率的总和也是1,因为数组总是至少有1个元素。

答案 13 :(得分:0)

另一种可能性是,将exponential distribution中提取的随机数与该数组的每个元素的权重相关联。然后选择“订货号”最低的元素。在这种情况下,特定元素具有最低数组编号的概率与数组元素的权重成正比。

这是O(n),不涉及任何重新排序或额外的存储,并且选择可以在单次通过数组的过程中完成。权重必须为正且大于零,但不必求和为任何特定值。

这还有一个优势,如果您将顺序号与每个数组元素一起存储,则可以选择通过增加顺序号来对数组进行排序,以获得对数组的随机排序,其中权重较高的元素具有一个提早出现的可能性更高(我在决定选择哪个DNS SRV记录,决定要查询的机器时发现这很有用。

重复进行随机抽样替换需要每次重新通过阵列。对于无需替换的随机选择,可以按增加的排序顺序对数组进行排序,并可以按此顺序读取 k 个元素。

请参见wikipedia page about the exponential distribution(尤其是有关此类变量集合的最小值的分布的说明)以证明上述内容是正确的,并且还涉及生成此类变量的技术的指针:如果 T 在[0,1)中具有均匀的随机分布,则 Z = -log(1-T)/ w (其中 w 是分布的参数;此处关联元素的权重)具有指数分布。

也就是说:

  1. 对于数组中的每个元素 i ,计算 zi = -log(T)/ wi (或 zi = -log(1-T)/ wi ),其中T是从[0,1)中的均匀分布得出的,而 wi 是第i个元素的权重。
  2. 选择具有最低 zi 的元素。

元素 i 的选择概率为 wi /(w1 + w2 + ... + wn)

请参见下文,以Python进行的说明,它需要对10000个试验中的每一个进行一次权重传递。

import math, random

random.seed()

weights = [10, 20, 50, 20]
nw = len(weights)
results = [0 for i in range(nw)]

n = 10000
while n > 0: # do n trials
    smallest_i = 0
    smallest_z = -math.log(1-random.random())/weights[0]
    for i in range(1, nw):
        z = -math.log(1-random.random())/weights[i]
        if z < smallest_z:
            smallest_i = i
            smallest_z = z

    results[smallest_i] += 1 # accumulate our choices

    n -= 1

for i in range(nw):
    print("{} -> {}".format(weights[i], results[i]))

编辑(用于历史记录):发布此内容后,我确定无法成为第一个想到这一点的人,考虑到此解决方案的另一次搜索显示这确实是情况。