我想从数组中随机选择一个元素,但每个元素都有一个已知的选择概率。
所有机会(在阵列中)总和为1。
您认为哪种算法最快,最适合大型计算?
示例:
id => chance
array[
0 => 0.8
1 => 0.2
]
对于此伪代码,有问题的算法应该在多个调用上统计返回id 0
上的四个元素,用于id 1
上的一个元素。
答案 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的别名方法
初始化:
- 创建数组 Alias 和 Prob ,每个 n 。
- 创建两个工作清单,小和大。
- 将每个概率乘以 n 。
- 对于每个缩放概率 p i :
- 如果 p i &lt; 1 ,将 i 添加到 Small 。
- 否则( p i ≥1),将 i 添加到 Large 。
- 小且大不为空:(大可能先清空)
- 从 Small 中删除第一个元素;称之为 l 。
- 从 Large 中删除第一个元素;称之为 g 。
- 设置 Prob [l] = p l 。
- 设置别名[l] = g 。
- 设置 p g :=(p g + p l ) - 1 。 (这是一个更加数字稳定的选择。)
- 如果 p g &lt; 1 ,请将 g 添加到 Small 。
- 否则( p g ≥1),将 g 添加到 Large 。
- Large 不为空时:
- 从 Large 中删除第一个元素;称之为 g 。
- 设置 Prob [g] = 1 。
- 虽然 Small 不为空:这只能由于数值不稳定而成为可能。
醇>
- 从 Small 中删除第一个元素;称之为 l 。
- 设置 Prob [l] = 1 。
代:
- 从 n 侧面模具生成合理的模具辊;打电话给我。
- 以概率 Prob [i] 翻转出现在头上的有偏见的硬币。
- 如果硬币出现&#34;头部,&#34;返回我。
- 否则,请返回 Alias [i] 。
醇>
答案 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回答时改进。
基本上你制作一个大数组,其中元素显示的次数与权重成正比。
它有一些缺点。
为了解决这个问题,这就是你所做的。
创建此类数组,但只是随机插入一个元素。插入元素的概率与权重成正比。
然后从平时选择随机元素。
因此,如果有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 是分布的参数;此处关联元素的权重)具有指数分布。
也就是说:
元素 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]))
编辑(用于历史记录):发布此内容后,我确定无法成为第一个想到这一点的人,考虑到此解决方案的另一次搜索显示这确实是情况。