【发布时间】:2011-05-26 16:49:29
【问题描述】:
我想从一个数组中随机选择一个元素,但每个元素都有一个已知的选择概率。
所有机会加在一起(在数组内)总和为 1。
您认为哪种算法最快且最适合大型计算?
例子:
id => chance
array[
0 => 0.8
1 => 0.2
]
对于这个伪代码,有问题的算法应该在多次调用中统计返回 id 0 上的四个元素,id 1 上的一个元素。
【问题讨论】:
我想从一个数组中随机选择一个元素,但每个元素都有一个已知的选择概率。
所有机会加在一起(在数组内)总和为 1。
您认为哪种算法最快且最适合大型计算?
例子:
id => chance
array[
0 => 0.8
1 => 0.2
]
对于这个伪代码,有问题的算法应该在多次调用中统计返回 id 0 上的四个元素,id 1 上的一个元素。
【问题讨论】:
另一种可能性是将数组的每个元素与从exponential distribution 抽取的随机数关联起来,参数由该元素的权重给出。然后选择具有最低此类“订购号”的元素。在这种情况下,特定元素具有数组最低排序数的概率与数组元素的权重成正比。
这是 O(n),不涉及任何重新排序或额外存储,并且可以在一次遍历数组的过程中完成选择。权重必须大于零,但不必总和为任何特定值。
这还有一个好处,如果您将排序号与每个数组元素一起存储,您可以选择通过增加排序号来对数组进行排序,以获得数组的随机排序,其中权重较高的元素具有提早出现的可能性更高(我发现这在决定选择哪个 DNS SRV 记录、决定查询哪台机器时很有用)。
带放回的重复随机抽样每次都需要一个新的遍历数组;对于无放回的随机选择,数组可以按照序号递增的顺序进行排序,k个元素可以按照这个顺序读出。
请参阅Wikipedia page about the exponential distribution(特别是关于此类变量集合的最小值分布的注释)以证明上述是正确的,以及指向生成此类变量的技术的指针:如果 T 在 [0,1) 中具有均匀随机分布,则 Z=-log(1-T)/w(其中 w 是分布参数;这里是相关元素的权重)具有指数分布。
即:
元素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]))
编辑(历史记录): 发布此内容后,我确信我不是第一个想到它的人,并且考虑到此解决方案的另一个搜索表明这确实是案例。
【讨论】:
这是 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 方法来解决您描述的问题。
"Wheel of Fortune" 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];
}
【讨论】:
这是我在生产中使用的 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;
}
}
}
【讨论】:
我发现this article 对完全理解这个问题最有用。 This stackoverflow question 也可能是你要找的。p>
我相信最佳解决方案是使用Alias Method (wikipedia)。 初始化需要 O(n) 时间,进行选择需要 O(1) 时间,以及 O(n) 内存。 p>
这里是生成滚动加权 n 面骰子结果的算法(从这里从长度-n 数组中选择一个元素是微不足道的)取自this article。
作者假设您具有掷公平骰子 (floor(random() * n)) 和掷偏硬币 (random() < p) 的功能。
算法:Vose 的别名方法
初始化:
- 创建数组Alias和Prob,每个数组的大小为n。
- 创建两个工作清单,Small 和 Large。
- 将每个概率乘以 n。
- 对于每个缩放概率 pi:
- 如果 pi,将 i 添加到 Small。
- 否则 (pi ≥ 1),将 i 添加到 Large。
- 虽然 Small 和 Large 不为空:(Large 可能会先被清空)
- 从Small中移除第一个元素;叫它l。
- 从Large中移除第一个元素;称之为g。
- 设置Prob[l]=pl。
- 设置别名[l]=g。
- 设置 pg := (pg+pl)−1。 (这是一个数值更稳定的选项。)
- 如果 pg,则将 g 添加到 Small。
- 否则 (pg ≥ 1),将 g 添加到 Large。
- 虽然 Large 不为空:
- 从Large中移除第一个元素;称之为g。
- 设置概率[g] = 1。
- 虽然 Small 不为空:这仅可能是由于数值不稳定。
- 从Small中移除第一个元素;叫它l。
- 设置概率[l] = 1。
世代:
- 从 n 面的骰子中生成公平的骰子;叫方i。
- 投掷一枚有偏向的硬币,该硬币以概率Prob[i]出现正面。
- 如果硬币出现“正面”,则返回 i。
- 否则,返回别名[i]。
【讨论】:
我将改进https://stackoverflow.com/users/626341/masciugo 的答案。
基本上你制作一个大数组,其中元素出现的次数与权重成正比。
它有一些缺点。
为了解决这个问题,这就是你要做的。
创建这样的数组,但只随机插入一个元素。插入元素的概率与权重成正比。
然后从常规中选择随机元素。
因此,如果有 3 个不同权重的元素,您只需从 1-3 个元素的数组中选择一个元素。
如果构造的元素为空,则可能会出现问题。只是碰巧没有元素出现在数组中,因为它们的骰子滚动不同。
在这种情况下,我建议插入元素的概率是 p(inserted)=wi/wmax。
这样,将插入一个元素,即概率最高的元素。其他元素按相对概率插入。
假设我们有 2 个对象。
元素 1 出现 0.20% 的时间。 元素 2 出现的概率为 0.40%,概率最高。
在数组中,元素 2 会一直出现。元素 1 将出现一半时间。
所以元素 2 将被称为元素 1 的 2 倍。为了一般性,所有其他元素将被称为与其权重成正比。此外,它们所有概率的总和为 1,因为数组总是至少有 1 个元素。
【讨论】:
使用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]]
【讨论】:
我想大于或等于 0.8 但小于 1.0 的数字会选择第三个元素。
换句话说:
x 是 0 到 1 之间的随机数
如果 0.0 >= x
如果 0.2 >= x
如果 0.8 >= x
【讨论】:
诀窍可能是使用反映概率的元素重复对辅助数组进行采样
给定与其概率相关的元素,以百分比表示:
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
【讨论】:
这可以在每个样本的 O(1) 预期时间内完成,如下所示。
计算每个元素 i 的 CDF F(i) 为小于或等于 i 的概率之和。
定义元素 i 的范围 r(i) 为区间 [F(i - 1), F(i)]。
对于每个区间 [(i - 1)/n, i/n],创建一个包含范围与区间重叠的元素列表的存储桶。只要您相当小心,整个数组总共需要 O(n) 时间。
当您对数组进行随机采样时,您只需计算随机数在哪个桶中,然后与列表中的每个元素进行比较,直到找到包含它的区间。
样本的成本是 O(随机选择的列表的预期长度)
【讨论】:
计算列表的离散累积密度函数 (CDF) - 或者简单地说,就是权重的累积和数组。然后生成一个介于 0 和所有权重之和之间的随机数(在您的情况下可能为 1),进行二进制搜索以在离散 CDF 数组中找到此随机数并获取与此条目对应的值 - 这个是你的加权随机数。
【讨论】:
log2(500) = 9 步。
lower_bound() in C++ 或 bisect_left() in Python.
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]
【讨论】:
算法很简单
rand_no = rand(0,1)
for each element in array
if(rand_num < element.probablity)
select and break
rand_num = rand_num - element.probability
【讨论】:
如果数组很小,我会给数组一个长度,在这种情况下,为 5,并根据需要分配值:
array[
0 => 0
1 => 0
2 => 0
3 => 0
4 => 1
]
【讨论】: