根据不同权重随机混洗数组的算法

时间:2015-04-30 16:10:06

标签: ruby algorithm sorting

我有一组我想随机随机播放的元素,但每个元素都有不同的优先级或权重。因此,具有较大权重的元素必须具有更多的概率才能成为结果的顶部。

我有这个数组:

elements = [
  { :id => "ID_1", :weight => 1 },
  { :id => "ID_2", :weight => 2 },
  { :id => "ID_3", :weight => 6 }
]

我想要将其洗牌,以便身份"ID_3"的元素 ~6次比元素"ID_1" ~3次更多的概率比元素"ID_2"更多的概率。

更新

澄清:一旦你选择了第一个位置,其他元素将使用相同的逻辑战斗其余位置。

6 个答案:

答案 0 :(得分:6)

我可以想到两种方法来解决它,虽然我的直觉告诉我应该修改Fisher-Yates以更好地实现它:

O(n * W)解决方案:(编程简单)

首先,根据权重创建重复项(与您的方法相同),并填充新列表。现在在这个列表上运行一个标准的shuffle(fisher-yates)。迭代列表并丢弃所有重复项,并仅保留每个元素的第一次出现。这在O(n*W)中运行,其中n是列表中元素的数量,W是平均权重(伪多项式解决方案)。

O(nlogn)解决方案:(非常难以编程)

第二种方法是创建元素权重总和列表:

sum[i] = weight[0] + ... + weight[i]

现在,在0sum[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]

首先构造一个几乎完整的二叉树,并填充其中的元素。请注意,树不是二进制搜索树,只是一个常规树,因此元素的顺序无关紧要 - 我们以后不需要对它进行维护。

您将获得类似以下树的内容:

enter image description here

图例: w - 该节点的权重,整个子树的权重的sw和。

接下来,计算每个子树的权重总和。从叶子开始,计算s.w = w。对于每个其他节点计算s.w = left->s.w + right->s.w,从下往上填充树(post order traversal)。

enter image description here

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)中完成的。

现在,您需要删除该节点,并重置树的权重。
一种删除确保树处于对数权重的方法对于二进制堆是微笑的:用最右下方的节点替换所选择的节点,删除新的最右下方节点,并重新平衡从两个相关节点开始的两个分支到树上。

首先切换:

enter image description here

然后重新计算:

enter image description here

请注意,只需要重新计算两条路径,每条路径的深度最多为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 elementsh[: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_shuffleweighted_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)

基于@amit suggestion

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]