我有一组我想随机随机播放的元素,但每个元素都有不同的优先级或权重。因此,具有较大权重的元素必须具有更多的概率才能成为结果的顶部。
我有这个数组:
elements = [
{ :id => "ID_1", :weight => 1 },
{ :id => "ID_2", :weight => 2 },
{ :id => "ID_3", :weight => 6 }
]
我想要将其洗牌,以便身份"ID_3"
的元素 ~6次比元素"ID_1"
和 ~3次更多的概率比元素"ID_2"
更多的概率。
澄清:一旦你选择了第一个位置,其他元素将使用相同的逻辑战斗其余位置。
答案 0 :(得分:6)
我可以想到两种方法来解决它,虽然我的直觉告诉我应该修改Fisher-Yates以更好地实现它:
O(n * W)解决方案:(编程简单)
首先,根据权重创建重复项(与您的方法相同),并填充新列表。现在在这个列表上运行一个标准的shuffle(fisher-yates)。迭代列表并丢弃所有重复项,并仅保留每个元素的第一次出现。这在O(n*W)
中运行,其中n
是列表中元素的数量,W
是平均权重(伪多项式解决方案)。
O(nlogn)解决方案:(非常难以编程)
第二种方法是创建元素权重总和列表:
sum[i] = weight[0] + ... + weight[i]
现在,在0
到sum[n]
之间绘制一个数字,然后选择sum
大于/等于此随机数的第一个元素。
这将是下一个元素,丢弃元素,重新创建列表,然后重复。
这在O(n^2*logn)
可以通过创建二叉树而不是列表来进一步增强,其中每个节点还存储整个子树的权重值。
现在,在选择一个元素后,找到匹配元素(其总和是高于随机选择数的第一个),删除节点,并重新计算路径路径上的权重。
这将创建树O(n)
,O(logn)
在每一步找到节点,O(logn)
重新计算总和。重复它直到树耗尽,然后你得到O(nlogn)
解决方案。
这种方法的概念与Order Statistics Trees非常相似,但使用权重之和而不是后代数。删除后的搜索和平衡将与订单统计树类似地进行。
构造和使用二叉树的说明。
假设您elements=[a,b,c,d,e,f,g,h,i,j,k,l,m]
与weights=[1,2,3,1,2,3,1,2,3,1,2,3,1]
首先构造一个几乎完整的二叉树,并填充其中的元素。请注意,树不是二进制搜索树,只是一个常规树,因此元素的顺序无关紧要 - 我们以后不需要对它进行维护。
您将获得类似以下树的内容:
图例: w - 该节点的权重,整个子树的权重的sw和。
接下来,计算每个子树的权重总和。从叶子开始,计算s.w = w
。对于每个其他节点计算s.w = left->s.w + right->s.w
,从下往上填充树(post order traversal)。
在s.w.
中完成构建树,填充树并为每个节点计算O(n)
。
现在,你需要迭代地选择0到权重之和的随机数(根的s.w.值,在我们的例子中为25)。假设该数字为r
,并为每个这样的数字找到匹配节点。
找到匹配节点是递归完成的
if `r< root.left.sw`:
go to left son, and repeat.
else if `r<root.left.sw + root.w`:
the node you are seeking is the root, choose it.
else:
go to `root.right` with `r= r-root.left.sw - root.w`
示例,选择r=10
:
Is r<root.left.sw? Yes. Recursively invoke with r=10,root=B (left child)
Is r<root.left.sw No. Is r < root.left.sw + root.w? No. Recursively invoke with r=10-6-2=2, and root=E (right chile)
Is r<root.left.sw? No. Is r < root.left.sw + root.w? Yes. Choose E as next node.
这是在每次迭代O(h) = O(logn)
中完成的。
现在,您需要删除该节点,并重置树的权重。
一种删除确保树处于对数权重的方法对于二进制堆是微笑的:用最右下方的节点替换所选择的节点,删除新的最右下方节点,并重新平衡从两个相关节点开始的两个分支到树上。
首先切换:
然后重新计算:
请注意,只需要重新计算两条路径,每条路径的深度最多为O(logn)
(图片中的橙色节点),因此删除和重新计算也是O(logn)
。
现在,你得到了一个新的二叉树,修改了权重,你就可以选择下一个候选者,直到树用完为止。
答案 1 :(得分:2)
我会按如下方式对数组进行洗牌:
<强>代码强>
def weighted_shuffle(array)
arr = array.sort_by { |h| -h[:weight] }
tot_wt = arr.reduce(0) { |t,h| t += h[:weight] }
ndx_left = arr.each_index.to_a
arr.size.times.with_object([]) do |_,a|
cum = 0
rn = (tot_wt>0) ? rand(tot_wt) : 0
ndx = ndx_left.find { |i| rn <= (cum += arr[i][:weight]) }
a << arr[ndx]
tot_wt -= arr[ndx_left.delete(ndx)][:weight]
end
end
<强>实施例强>
elements = [
{ :id => "ID_1", :weight => 100 },
{ :id => "ID_2", :weight => 200 },
{ :id => "ID_3", :weight => 600 }
]
def display(arr,n)
n.times.with_object([]) { |_,a|
p weighted_shuffle(arr).map { |h| h[:id] } }
end
display(elements,10)
["ID_3", "ID_2", "ID_1"]
["ID_1", "ID_3", "ID_2"]
["ID_1", "ID_3", "ID_2"]
["ID_3", "ID_2", "ID_1"]
["ID_3", "ID_2", "ID_1"]
["ID_2", "ID_3", "ID_1"]
["ID_2", "ID_3", "ID_1"]
["ID_3", "ID_1", "ID_2"]
["ID_3", "ID_1", "ID_2"]
["ID_3", "ID_2", "ID_1"]
n = 10_000
pos = elements.each_index.with_object({}) { |i,pos| pos[i] = Hash.new(0) }
n.times { weighted_shuffle(elements).each_with_index { |h,i|
pos[i][h[:id]] += 1 } }
pos.each { |_,h| h.each_key { |k| h[k] = (h[k]/n.to_f).round(3) } }
#=> {0=>{"ID_3"=>0.661, "ID_2"=>0.224, "ID_1"=>0.115},
# 1=>{"ID_2"=>0.472, "ID_3"=>0.278, "ID_1"=>0.251},
# 2=>{"ID_1"=>0.635, "ID_2"=>0.304, "ID_3"=>0.061}}
这表示,在调用10,000次weighted_shuffle
时,所选择的第一个元素是“ID_3”66.1%的时间,“ID_2”占22.4%的时间,“ID_1”是剩余的11.5% % 的时间。 “ID_2”在47.2%的次数中被选中,依此类推。
<强>解释强>
arr
是要洗牌的哈希数组。随机播放以arr.size
步进行。在每个步骤中,我使用提供的权重随机绘制arr
的元素,而无需替换。如果h[:weight]
对tot
之前未选择的h
的所有元素arr
求和,则选择任何一个哈希h
的概率为h[:weight]/tot
。通过查找p
的第一个累积概率rand(tot) <= p
来完成每个步骤的选择。通过降低权重来预先排序element
的元素,使得最后一步更有效率,这是在方法的第一步中完成的:
elements.sort_by { |h| -h[:weight] }
#=> [{ :id => "ID_3", :weight => 600 },
# { :id => "ID_2", :weight => 200 },
# { :id => "ID_1", :weight => 100 }]
这是使用arr
的索引数组实现的,称为ndx_left
,在其上执行迭代。在选择索引为h
的哈希i
后,tot
会通过减去h[:weight]
进行更新,i
会从ndx_left
中删除。
<强>变体强>
以下是上述方法的变体:
def weighted_shuffle_variant(array)
arr = array.sort_by { |h| -h[:weight] }
tot_wt = arr.reduce(0) { |t,h| t += h[:weight] }
n = arr.size
n.times.with_object([]) do |_,a|
cum = 0
rn = (tot_wt>0) ? rand(tot_wt) : 0
h, ndx = arr.each_with_index.find { |h,_| rn <= (cum += h[:weight]) }
a << h
tot_wt -= h[:weight]
arr[ndx] = arr.pop
end
end
不是在arr
中维护尚未被选中的元素索引数组,而是在适当的位置修改arr
,并在选择每个元素时将大小减小一。如果选择了元素arr[i]
,则会将最后一个元素复制到偏移i
,并删除arr
的最后一个元素:
arr[i] = arr.pop
<强>基准强>
复制h
elements
次h[:weight]
的每个元素的方法,然后随后移动uniq
,如果结果非常低效。如果这不明显,这是一个基准。我将weighted_shuffle
与@ Mori的解决方案进行了比较,该解决方案代表了“复制,随机播放,删除”方法:
def mori_shuffle(array)
array.flat_map { |h| [h[:id]] * h[:weight] }.shuffle.uniq
end
require 'benchmark'
def test_em(nelements, ndigits)
puts "\nelements.size=>#{nelements}, weights have #{ndigits} digits\n\n"
mx = 10**ndigits
elements = nelements.times.map { |i| { id: i, weight: rand(mx) } }
Benchmark.bm(15 "mori_shuffle", "weighted_shuffle") do |x|
x.report { mori_shuffle(elements) }
x.report { weighted_shuffle(elements) }
end
end
elements.size=>3, weights have 1 digits
user system total real
mori_shuffle 0.000000 0.000000 0.000000 ( 0.000068)
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000051)
elements.size=>3, weights have 2 digits
user system total real
mori_shuffle 0.000000 0.000000 0.000000 ( 0.000035)
weighted_shuffle 0.010000 0.000000 0.010000 ( 0.000026)
elements.size=>3, weights have 3 digits
user system total real
mori_shuffle 0.000000 0.000000 0.000000 ( 0.000161)
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000027)
elements.size=>3, weights have 4 digits
user system total real
mori_shuffle 0.000000 0.000000 0.000000 ( 0.000854)
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000026)
elements.size=>20, weights have 2 digits
user system total real
mori_shuffle 0.000000 0.000000 0.000000 ( 0.000089)
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000090)
elements.size=>20, weights have 3 digits
user system total real
mori_shuffle 0.000000 0.000000 0.000000 ( 0.000771)
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000071)
elements.size=>20, weights have 4 digits
user system total real
mori_shuffle 0.000000 0.000000 0.000000 ( 0.005895)
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000073)
elements.size=>100, weights have 2 digits
user system total real
mori_shuffle 0.000000 0.000000 0.000000 ( 0.000446)
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000683)
elements.size=>100, weights have 3 digits
user system total real
mori_shuffle 0.010000 0.000000 0.010000 ( 0.003765)
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000659)
elements.size=>100, weights have 4 digits
user system total real
mori_shuffle 0.030000 0.010000 0.040000 ( 0.034982)
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000638)
elements.size=>100, weights have 5 digits
user system total real
mori_shuffle 0.550000 0.040000 0.590000 ( 0.593190)
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000623)
elements.size=>100, weights have 6 digits
user system total real
mori_shuffle 5.560000 0.380000 5.940000 ( 5.944749)
weighted_shuffle 0.010000 0.000000 0.010000 ( 0.000636)
weighted_shuffle
和weighted_shuffle_variant
考虑到基准引擎都已经预热,我不妨比较我建议的两种方法。结果类似,weighted_shuffle
具有一致的边缘。以下是一些典型结果:
elements.size=>20, weights have 3 digits
user system total real
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000062)
weighted_shuffle_variant 0.000000 0.000000 0.000000 ( 0.000108)
elements.size=>20, weights have 4 digits
user system total real
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000060)
weighted_shuffle_variant 0.000000 0.000000 0.000000 ( 0.000089)
elements.size=>100, weights have 2 digits
user system total real
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000666)
weighted_shuffle_variant 0.000000 0.000000 0.000000 ( 0.000871)
elements.size=>100, weights have 4 digits
user system total real
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000625)
weighted_shuffle_variant 0.000000 0.000000 0.000000 ( 0.000803)
elements.size=>100, weights have 6 digits
user system total real
weighted_shuffle 0.000000 0.000000 0.000000 ( 0.000664)
weighted_shuffle_variant 0.000000 0.000000 0.000000 ( 0.000773)
与weighted_shuffle
相比,weighted_shuffle_variant
不维护尚未被选中的({1}}副本的元素索引数组(节省时间)。相反,它用数组的最后一个元素替换数组中的选定元素,然后elements
是最后一个元素,导致数组的大小在每一步减少一个。不幸的是,这会通过减轻重量来破坏元素的排序。相比之下,pop
通过降低权重的顺序来维持考虑元素的优化。总的来说,后者的权衡似乎比前者更重要。
答案 2 :(得分:1)
def self.random_suffle_with_weight(elements, &proc)
consecutive_chain = []
elements.each do |element|
proc.call(element).times { consecutive_chain << element }
end
consecutive_chain.shuffle.uniq
end
答案 3 :(得分:0)
我有我的解决方案,但我认为可以改进:
module Utils
def self.random_suffle_with_weight(elements, &proc)
# Create a consecutive chain of element
# on which every element is represented
# as many times as its weight.
consecutive_chain = []
elements.each do |element|
proc.call(element).times { consecutive_chain << element }
end
# Choosine one element randomly from
# the consecutive_chain and remove it for the next round
# until all elements has been chosen.
shorted_elements = []
while(shorted_elements.length < elements.length)
random_index = Kernel.rand(consecutive_chain.length)
selected_element = consecutive_chain[random_index]
shorted_elements << selected_element
consecutive_chain.delete(selected_element)
end
shorted_elements
end
end
测试:
def test_random_suffle_with_weight
element_1 = { :id => "ID_1", :weight => 10 }
element_2 = { :id => "ID_2", :weight => 20 }
element_3 = { :id => "ID_3", :weight => 60 }
elements = [element_1, element_2, element_3]
Kernel.expects(:rand).with(90).returns(11)
Kernel.expects(:rand).with(70).returns(1)
Kernel.expects(:rand).with(60).returns(50)
assert_equal([element_2, element_1, element_3], Utils.random_suffle_with_weight(elements) { |e| e[:weight] })
end
答案 4 :(得分:0)
elements.flat_map { |h| [h[:id]] * h[:weight] }.shuffle.uniq
答案 5 :(得分:0)
Weighted Random Sampling (2005; Efraimidis, Spirakis)为此提供了非常优雅的算法。实现非常简单,可以在O(n log(n))
:
def weigthed_shuffle(items, weights):
order = sorted(range(len(items)), key=lambda i: -random.random() ** (1.0 / weights[i]))
return [items[i] for i in order]