【问题标题】:Picking a random option, where each option has a different probability of being picked选择一个随机选项,其中每个选项都有不同的被选中概率
【发布时间】:2013-10-16 03:56:59
【问题描述】:

假设给你三个“选项”,ABC

您的算法必须选择并返回一个随机的。为此,只需将它们放入数组{A,B,C} 并生成一个随机数(0、1 或 2),这将是要返回的数组中元素的索引。

现在,这个算法有一个变种:假设A 有 40% 的机会被选中,B 有 20%,C 有 40%。如果是这种情况,您可以采用类似的方法:生成一个数组 {A,A,B,C,C} 并使用一个随机数 (0, 1, 2, 3, 4) 来选择要返回的元素。

这行得通。但是,我觉得它非常低效。想象一下将这个算法用于大量数量的选项。您将创建一个有点大的数组,可能有 100 个元素,每个元素代表 1%。现在,这仍然不是很大,但是假设您的算法每秒使用多次,这可能会很麻烦。


我考虑过创建一个名为Slot 的类,它有两个属性:.value.size。为每个选项创建一个槽,其中.value 属性是选项的值,.size 等于数组中此类选项的出现次数。然后生成一个从 0 到总出现次数的随机数,并检查该数字落在哪个槽上。

我更关心算法,但这是我的 Ruby 尝试:

class Slot
  attr_accessor :value
  attr_accessor :size
  def initialize(value,size)
    @value = value
    @size  = size
  end
end

def picker(options)
  slots = []
  totalSize = 0
  options.each do |value,size|
    slots << Slot.new(value,size)
    totalSize += size
  end
  pick = rand(totalSize)
  currentStack = 0
  slots.each do |slot|
    if (pick <= currentStack + slot.size)
      return slot.value
    else
      currentStack += slot.size
    end
  end
  return nil
end

50.times do
  print picker({"A" => 40, "B" => 20, "C" => 40})
end

哪些输出:

CCCCACCCCAAACABAAACACACCCAABACABABACBAAACACCBACAAB


是否有更有效的方法来实现一种选择随机选项的算法,其中每个选项被选择的概率不同?

【问题讨论】:

  • 我最近讨论了如何从离散分布here 中生成随机变量。这很容易,我认为你在正确的轨道上。你说你主要关心的是效率。如果你的随机数是整数,我建议你构造一个哈希,比如h[rand(..)] =&gt; 'A', 'B' or 'C',这取决于随机数的值。
  • Weighted random numbers 的可能副本。这本质上是“加权随机数,但在 Ruby 中”。不确定这是否算作不重复..

标签: ruby algorithm random


【解决方案1】:

最简单的方法大概就是写一个case语句:

def get_random()
  case rand(100) + 1
    when  1..50   then 'A'
    when 50..75   then 'B'
    when 75..100  then 'C'
  end
end

这样做的问题是您不能传递任何选项,因此如果您希望它能够接受选项,您可以编写这样的函数。下面这个和你写的很像,只是短了一点:

def picker(options)
  current, max = 0, options.values.inject(:+)
  random_value = rand(max) + 1
  options.each do |key,val|
     current += val
     return key if random_value <= current
  end
end

# A with 25% prob, B with 75%.
50.times do
  print picker({"A" => 1, "B" => 3})
end
# => BBBBBBBBBABBABABBBBBBBBABBBBABBBBBABBBBBBABBBBBBBA

# If you add upp to 100, the number represent percentage.
50.times do
  print picker({"A" => 40, "T" => 30, "C" => 20, "G" => 10})
end
# => GAAAATATTGTACCTCAATCCAGATACCTTAAGACCATTAAATCTTTACT 

【讨论】:

    【解决方案2】:

    作为更高效算法的第一个近似值,如果您计算累积分布函数(这只是对分布函数的一次遍历,计算运行总和),那么您可以使用 a 找到随机选择的整数的位置二进制搜索而不是线性搜索。如果您有很多选项,这将有所帮助,因为它将搜索时间从 O(#options) 减少到 O(log #options)。

    不过,有一个 O(1) 的解决方案。这是基本大纲。

    假设我们有 N 个选项 1...N,权重为 ω<sub>1</sub>...ω<sub>N</sub>,其中所有 ω 值至少为 0。为简单起见,我们缩放权重,使其平均值为 1,或者换句话说,他们的总和是N。 (我们只需将它们乘以 N/Σω。我们实际上不必这样做,但它使接下来的几段更容易在没有 MathJax 的情况下键入。)

    现在,创建一个包含N 元素的向量,其中每个元素都有两个选项标识符(lohi)和一个截止点p。选项标识符只是整数1...N,而p 将被计算为(0, 1.0) 范围内的实数。

    我们继续填写向量如下。依次对每个元素i

    • 如果某些ω<sub>j</sub> 正好是1.0,那么我们设置:
         lo<sub>i</sub> = j
         hi<sub>i</sub> = j
         p<sub>i</sub> = 1.0
      我们从权重列表中删除ω<sub>j</sub>

    • 否则,一定有ω<sub>j</sub> &lt; 1.0ω<sub>k</sub> &gt; 1.0。 (那是因为平均权重是1.0,没有一个有平均值。有些肯定少有些多,因为不可能所有元素都大于平均值或所有元素都小于比平均值。)现在,我们设置:
         lo<sub>i</sub> = j
         hi<sub>i</sub> = k
         p<sub>i</sub> = ω<sub>j</sub>
         ω<sub>k</sub> = ω<sub>k</sub> - (1 - ω<sub>j</sub>)
      我们再一次从权重中删除ω<sub>j</sub>

    请注意,在这两种情况下,我们都移除了一个权重,并将权重总和减少了 1.0。所以平均权重还是1.0。

    我们以这种方式继续,直到整个向量被填满。 (最后一个元素将有p = 1.0)。

    给定这个向量,我们可以选择一个加权随机选项,如下所示:

    • 1...N 范围内生成一个随机整数i,在(0, 1.0] 范围内生成一个随机浮点值r。如果r &lt; p<sub>i</sub>,那么我们选择选项lo<sub>i</sub>;否则,我们选择选项hi<sub>i</sub>

    应该很清楚为什么这可以从向量的构造中进行。每个高于平均权重的选项的权重分布在各个向量元素中,而每个低于平均权重的选项被分配给具有相应选择概率的某个向量元素的一部分。

    在实际实现中,我们会将权重范围映射到整数值,并使总权重接近最大整数(它必须是N 的倍数,所以会有一些晃动。)我们然后可以选择一个槽并从一个随机整数中选择槽内的权重。事实上,我们可以通过添加一些 0 加权选项来强制槽数为 2 的幂来修改算法以避免除法。因为整数运算不会完美地运行,所以需要一些摆弄,但最终结果可以在统计上正确,以所使用的 PRNG 的特征为模,它会执行几乎 em> 与N 选项的简单未加权选择一样快(一次移位和几次比较额外),代价是向量占用少于6N 存储元素(计算必须几乎翻倍数量的可能性插槽)。

    【讨论】:

    • 我花了一段时间才理解它,我想你可以澄清jk 的选择是通过搜索剩余的权重来做出的。不过确实有用的答案,以前我对此使用了二进制搜索,虽然我没有当前用途,但我几乎还是想去实现它:-)
    • 因为你没有提到,所以你描述的方法被称为Walker's alias method
    • @NeilSlater 它已经实现为 Ruby gem - 请参阅 aliastable
    • 如前所述,这可能会崩溃,除非您总是从盈余列中“窃取”足够的概率以完全填补当前列中的不足,即使这样做会导致过去的盈余进入赤字模式。否则,您可能会进入只有一个 ωj 1 的结束阶段,因为总赤字必须等于总盈余,但它们不必具有 1-1 对应关系。
    • @pjs:在你使用 ωj
    【解决方案3】:

    虽然这不是一个直接的答案,但我会向您展示帮助您概述此问题的来源:http://www.av8n.com/physics/arbitrary-probability.htm

    编辑:

    刚刚在 ruby​​ 中找到了一个不错的来源,pickup gem

    require 'pickup'
    headings = {
      A: 40,
      B: 20,
      C: 40,
    }
    pickup = Pickup.new(headings)
    pickup.pick
    #=> A
    pickup.pick
    #=> B
    pickup.pick
    #=> A
    pickup.pick
    #=> C
    pickup.pick
    #=> C
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-10-31
      • 2017-11-10
      • 2011-02-15
      • 2011-10-20
      • 1970-01-01
      • 1970-01-01
      • 2016-06-17
      • 2013-03-11
      相关资源
      最近更新 更多