我想从数组中随机选择一个元素,但是每个元素都有一个已知的选择概率。
(在数组中)所有机会的总和为1。
您会建议哪种算法最快,最适合进行大量计算?
例:
id => chance
array[
0 => 0.8
1 => 0.2
]
对于此伪代码,所讨论的算法应在多个调用上统计地返回id上的四个元素,以id上0
的一个元素1
。
我想从数组中随机选择一个元素,但是每个元素都有一个已知的选择概率。
(在数组中)所有机会的总和为1。
您会建议哪种算法最快,最适合进行大量计算?
例:
id => chance
array[
0 => 0.8
1 => 0.2
]
对于此伪代码,所讨论的算法应在多个调用上统计地返回id上的四个元素,以id上0
的一个元素1
。
Answers:
计算列表的离散累积密度函数(CDF)-或简单地说就是权重的累积和数组。然后生成一个介于0和所有权重之和之间的随机数(在您的情况下可能为1),进行二进制搜索以在离散的CDF数组中找到该随机数,并获取与该条目对应的值-是您的加权随机数。
lower_bound()
在C ++或bisect_left()
Python中。
该算法很简单
rand_no = rand(0,1)
for each element in array
if(rand_num < element.probablity)
select and break
rand_num = rand_num - element.probability
我发现本文对于全面了解此问题最有用。 这个stackoverflow问题也可能是您要寻找的。
我相信最佳解决方案是使用Alias方法(维基百科)。它需要O(n)的时间进行初始化,需要O(1)的时间进行选择,还需要O(n)的内存。
这是生成滚动加权n边模具的结果的算法(从这里开始,很难从长度为n的数组中选择一个元素),如本文所述。作者假设您具有滚动公平的骰子(floor(random() * n)
)和翻转有偏见的硬币(random() < p
)的功能。
算法:Vose的别名方法
初始化:
- 创建数组Alias和Prob,每个数组的大小为n。
- 创建两个工作清单Small和Large。
- 将每个概率乘以n。
- 对于每个缩放的概率p i:
- 如果p i <1,则将i加到Small。
- 否则(p我≥1 ),添加我到大。
- 当大号和小号不为空时:(大号可能会先清空)
- 从Small中删除第一个元素;称它为l。
- 从Large删除第一个元素; 称它为g。
- 设置Prob [l] = p l。
- 设置Alias [l] = g。
- 设置p g:=(p g + p l)-1。(这是一个在数值上更稳定的选项。)
- 如果p g <1,则将g加到Small。
- 否则(p克≥1 ),添加克到大。
- 虽然大不为空:
- 从Large删除第一个元素; 称它为g。
- 设置Prob [g] = 1。
- 当Small不为空时:仅由于数字不稳定,才有可能。
- 从Small中删除第一个元素;称它为l。
- 设置Prob [l] = 1。
代:
- 从n面模具产生公平的模具辊;叫我一边。
- 翻转以概率Prob [i]出现的偏向硬币。
- 如果硬币“冒出”,则返回i。
- 否则,返回Alias [i]。
另一个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
大致期望:
sample = 1000.times.map{ weighted_rand weights }
sample.count('a') # 396
sample.count('b') # 406
sample.count('c') # 198
sample.count('d') # 0
weighted_rand
方法来解决您描述的问题。
红宝石的一个例子
#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]
可以按每个样本O(1)的预期时间进行操作,如下所示。
将每个元素i的CDF F(i)计算为小于或等于i的概率之和。
将元素i的范围r(i)定义为区间[F(i-1),F(i)]。
对于每个间隔[(i-1)/ n,i / n],创建一个存储桶,该存储桶由范围与该间隔重叠的元素列表组成。只要您相当谨慎,整个阵列总共要花费O(n)时间。
当您对数组进行随机采样时,您只需计算随机数位于哪个存储桶中,然后与列表中的每个元素进行比较,直到找到包含它的间隔。
样本成本为O(随机选择的列表的预期长度)<= 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;
}
}
}
使用拾音器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]]
如果数组很小,在这种情况下,我将给数组一个长度为5的值,并根据需要分配值:
array[
0 => 0
1 => 0
2 => 0
3 => 0
4 => 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
我可以想象,大于或等于0.8但小于1.0的数字将选择第三个元素。
换句话说:
x是0到1之间的随机数
如果0.0> = x <0.2:项目1
如果0.2> = x <0.8:项目2
如果0.8> = x <1.0:项目3
我将在https://stackoverflow.com/users/626341/masciugo答案上进行改进。
基本上,您可以制作一个大数组,其中元素显示的次数与权重成正比。
它有一些缺点。
为了解决这个问题,这就是您要做的。
创建这样的数组,但只能随机插入一个元素。元素插入的概率与权重成正比。
然后从通常选择随机元素。
因此,如果有3个具有不同权重的元素,则只需从1-3个元素的数组中选择一个元素。
如果构造的元素为空,则可能会出现问题。碰巧的是,没有元素出现在数组中,因为它们的骰子滚动不同。
在这种情况下,我建议插入一个元素的概率为p(inserted)= wi / wmax。
这样,将插入一个元素,即概率最高的元素。其他元素将通过相对概率插入。
说我们有2个对象。
元素1出现的时间为.20%。元素2出现的时间为.40%,并且概率最高。
在数组中,元素2将一直显示。元素1将显示一半的时间。
因此,元素2的调用次数将是元素1的2倍。一般而言,所有其他元素的调用将与其权重成比例。它们的所有概率之和也为1,因为该数组将始终至少包含1个元素。
另一种可能性是将与指数分布相关的随机数与该数组的每个元素相关联,该随机数与该元素的权重给出的参数有关。然后选择具有最低“订购号”的元素。在这种情况下,特定元素具有数组最低排序数的概率与数组元素的权重成正比。
这是O(n),不涉及任何重新排序或额外的存储,并且选择可以在单次通过数组的过程中完成。权重必须为正且大于零,但不必求和为任何特定值。
这样做还有一个好处,如果您将顺序号与每个数组元素一起存储,则可以选择通过增加顺序号来对数组进行排序,以获得对数组的随机排序,其中权重较高的元素具有较高的概率早点到来(在确定选择哪个DNS SRV记录,确定要查询的计算机时,我发现这很有用)。
重复进行随机抽样替换需要每次重新通过阵列。对于无选择的随机选择,可以按增加的订购数对数组进行排序,并且k读取元素。
看到关于指数分布维基页(特别是关于这样个变量的集合的最小值的分布的话),用于证明的是,上述是真实的,并且还用于对生成这样的变元的技术的指针:如果Ť在[0,1)中具有均匀的随机分布,则Z = -log(1-T)/ w(其中w是分布的参数;此处是关联元素的权重)具有指数分布。
那是:
将以概率wi /(w1 + w2 + ... + wn)选择元素i。
请参阅下面的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]))
编辑(用于历史记录):发布此内容后,我确定无法成为第一个想到这一点的人,考虑到此解决方案的另一次搜索表明确实如此。
log2(500) = 9
每次查找中采取步骤。