我想从一个数组中随机选择一个元素,但每个元素都有已知的选择概率。
所有概率(在数组内)之和为1。
您会建议哪个算法最快、最适合进行大量计算?
例如:
id => chance
array[
0 => 0.8
1 => 0.2
]
对于这个伪代码,所讨论的算法在多次调用时应该会以 0
id 上的四个元素对应一个 1
id 的元素统计返回。
计算离散累积概率密度函数(CDF)的列表 - 或者简单地说是权重的累积和数组。然后在0到所有权重之和(如果你的情况下可能为1)的范围内生成一个随机数,使用二分搜索在你的离散CDF数组中找到这个随机数并获取对应于该条目的值 - 这就是您的加权随机数。
这个算法很直接
rand_no = rand(0,1)
for each element in array
if(rand_num < element.probablity)
select and break
rand_num = rand_num - element.probability
floor(random() * n)
)和翻转偏倚硬币的函数(random() < p)。
算法:Vose's Alias Method
初始化:
- 创建大小为n的数组Alias和Prob。
- 创建两个工作列表Small和Large。
- 将每个概率乘以n。
- 对于每个缩放后的概率pi:
- 如果pi<1,则将i添加到Small。
- 否则(pi ≥1 ),将i添加到Large。
- 当Small和Large都不为空时(可能先清空Large):
- 从Small中移除第一个元素,并将其称为l。
- 从Large中移除第一个元素,并将其称为g。
- 设置Prob[l] = pl。
- 设置Alias[l] = g。
- 设置pg := (pg+pl)−1。(这是一种更稳定的数值计算方式)
- 如果pg<1,则将g添加到Small中。
- 否则(pg ≥ 1),将g添加到Large中。
- 当Large不为空时:
- 从Large中移除第一个元素,并将其称为g。
- 设置Prob[g] = 1。
- 当Small不为空时:这只可能是由于数值不稳定引起的。
- 从Small中移除第一个元素,并将其称为l。
- 设置Prob[l] = 1。
生成:
- 从一个有n个面的公正骰子上生成一个随机数,称其为第i面。
- 投掷一个有偏的硬币,其正面朝上的概率为 Prob[i]。
- 如果硬币正面朝上,则返回 i。
- 否则,返回 Alias[i]。
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
方法以解决你所描述的问题。 - knugie一个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]
按如下方式,每个样本的期望时间为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;
}
}
}
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]]
在我看来,最有效的算法是为数组的每个元素生成一个随机数,该随机数从具有由该元素权重给定的参数的指数分布中绘制。当你遍历数组时,保留具有最低“排序数字”的元素。在这种情况下,特定元素具有数组最低排序号的概率与数组元素的权重成比例。
详细信息和代码如下。
该算法是O(n),不涉及排序或额外存储,并且选择可以在一次遍历数组中完成。权重必须大于零,但不必总和为任何特定值。
附加功能:如果您将每个数组元素的排序号码存储在其中,则可以选择按增加的排序号对数组进行排序,以获得数组的随机排序,在其中具有更高权重的元素很有可能较早出现(我发现这在决定要选择哪个DNS SRV记录以决定查询哪台计算机时非常有用)。
其他算法:重复随机抽样需要每次通过数组进行新的遍历;对于无重复随机选择,可以按照递增排序号的顺序对数组进行排序,并以此顺序读取k个元素。
请参阅维基百科关于指数分布的页面(特别是有关这种变量集合最小值分布的注释),证明上述结论的正确性,同时也为生成这种变量的技术提供了指针:如果T在[0,1)内具有均匀随机分布,则Z=-log(1-T)/w(其中w是分布的参数;这里是相关元素的权重)具有指数分布。
也就是说:
对于数组中的每个元素i,计算zi = -log(1-T)/wi,其中T是从[0,1)均匀分布中抽取的值,wi是第i个元素的权重。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
# we have selected element 'smallest_i'
results[smallest_i] += 1 # accumulate our choices
n -= 1
for i in range(nw):
print("{} -> {}".format(weights[i], results[i]))
编辑(历史记录):发布后,我确信自己不可能是第一个想到这个解决方案的人,再次搜索后发现确实如此。
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];
}
log2(500) = 9
。 - thejhlower_bound()
或Python中的bisect_left()
。 - Sven Marnach