如何实施加权洗牌


22

我最近写了一些我认为效率很低的代码,但是由于它只包含几个值,所以我接受了它。但是,我仍然对以下方面的更好算法感兴趣:

  1. X个对象的列表,每个对象都分配有一个“权重”
  2. 权重总和
  3. 生成一个从0到总和的随机数
  4. 遍历对象,从总和中减去它们的权重,直到总和为非正数
  5. 从列表中删除该对象,然后将其添加到新列表的末尾

项目2,4和5都需要n时间,因此这是一种O(n^2)算法。

这可以改善吗?

作为加权随机播放的示例,元素具有更大的重量,位于前部的机会更大。

示例(我将生成随机数以使其真实):

6个权重为6,5,4,3,2,1的对象; 总和是21

我选择了19 19-6-5-4-3-2 = -1:,因此2进入了第一位置,权重现在为6,5,4,3,1; 总和是19

我选择了16 16-6-5-4-3 = -2:,因此3进入第二位置,权重现在为6,5,4,1; 总和是16

我选择了3 3-6 = -3,因此6排在第三位,权重现在为5,4,1;总和是10

我选择8:,8-5-4 = -1因此4排在第四位,权重现在为5,1;总和是6

我选择5:,5-5=0因此5排在第五位,权重现在为1;总和是1

我选择了1 :1-1=0,因此1移到最后一个位置,我没有重量了,我完成了


6
加权洗牌到底是什么?这是否意味着重量越高,物体越可能位于甲板顶部?
2014年

出于好奇,步骤(5)的目的是什么。如果列表是静态的,则有多种方法可以改善此问题。
2014年

是的多瓦尔 我从列表中删除了该项目,因此它不会多次出现在混洗过的列表中。
弥敦道·美林

列表中项目的重量是否恒定?

一个项目的权重将比另一个项目大,但项目X的权重将始终相同。(显然,如果您删除物品,则较大的重量将按比例变大)
Nathan Merrill 2014年

Answers:


14

这可以通过O(n log(n))使用树来实现。

首先,创建树,在每个节点的右侧和左侧保留所有后代节点的累积总和。

要对项目进行采样,请从根节点递归采样,使用累积和来确定是返回当前节点,左侧的节点还是右侧的节点。每次对节点采样时,将其权重设置为零,并更新父节点。

这是我在Python中的实现:

import random

def weigthed_shuffle(items, weights):
    if len(items) != len(weights):
        raise ValueError("Unequal lengths")

    n = len(items)
    nodes = [None for _ in range(n)]

    def left_index(i):
        return 2 * i + 1

    def right_index(i):
        return 2 * i + 2

    def total_weight(i=0):
        if i >= n:
            return 0
        this_weigth = weights[i]
        if this_weigth <= 0:
            raise ValueError("Weigth can't be zero or negative")
        left_weigth = total_weight(left_index(i))
        right_weigth = total_weight(right_index(i))
        nodes[i] = [this_weigth, left_weigth, right_weigth]
        return this_weigth + left_weigth + right_weigth

    def sample(i=0):
        this_w, left_w, right_w = nodes[i]
        total = this_w + left_w + right_w
        r = total * random.random()
        if r < this_w:
            nodes[i][0] = 0
            return i
        elif r < this_w + left_w:
            chosen = sample(left_index(i))
            nodes[i][1] -= weights[chosen]
            return chosen
        else:
            chosen = sample(right_index(i))
            nodes[i][2] -= weights[chosen]
            return chosen

    total_weight() # build nodes tree

    return (items[sample()] for _ in range(n - 1))

用法:

In [2]: items = list(range(10))
   ...: weights = list(range(10, 0, -1))
   ...:

In [3]: for _ in range(10):
   ...:     print(list(weigthed_shuffle(items, weights)))
   ...:
[5, 0, 8, 6, 7, 2, 3, 1, 4]
[1, 2, 5, 7, 3, 6, 9, 0, 4]
[1, 0, 2, 6, 8, 3, 7, 5, 4]
[4, 6, 8, 1, 2, 0, 3, 9, 7]
[3, 5, 1, 0, 4, 7, 2, 6, 8]
[3, 7, 1, 2, 0, 5, 6, 4, 8]
[1, 4, 8, 2, 6, 3, 0, 9, 5]
[3, 5, 0, 4, 2, 6, 1, 8, 9]
[6, 3, 5, 0, 1, 2, 4, 8, 7]
[4, 1, 2, 0, 3, 8, 6, 5, 7]

weigthed_shuffle是一个生成器,因此您可以k有效地对热门商品进行抽样。如果要改组整个数组,只需遍历生成器,直到耗尽为止(使用list函数)。

更新:

加权随机采样(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]

最近的更新看起来与错误的单线解决方案极为相似。您确定正确吗?
Giacomo Alzetta

19

编辑:此答案不会以预期的方式解释权重。即重量为2的商品比重量为1的商品获得第一的可能性低两倍。

随机播放列表的一种方法是为列表中的每个元素分配随机数,然后按这些数字进行排序。我们可以扩展这个想法,我们只需要选择加权随机数即可。例如,您可以使用random() * weight。不同的选择将产生不同的分布。

在像Python这样的东西中,这应该很简单:

items.sort(key = lambda item: random.random() * item.weight)

注意不要对键进行多次评估,因为它们最终会得到不同的值。


2
老实说,这是天才,因为它很简单。假设您使用的是nlogn排序算法,则此方法应能很好地工作。
弥敦道·美林

砝码的重量是多少?如果它们很高,则仅按重量对对象进行分类。如果它们很低,则物体几乎是随机的,并且根据重量只有很小的扰动。无论哪种方式,我都一直使用这种方法,但是排序位置的计算可能需要进行一些调整。
david.pfx

@ david.pfx权重的范围应为随机数的范围。这样max*min = min*max,任何排列都是可能的,但是有些排列的可能性更大(尤其是如果权重分布不均匀)
Nathan Merrill 2014年

2
实际上,这种方法是错误的!想象一下权重75和25。对于75情况,在2/3的情况下它将选择一个大于25的数字。对于其余1/3的时间,它将在25%的时间“击败” 25。75将是时间的前2/3 +(1/3 * 1/2):83%。尚未解决此问题。
亚当·拉邦

1
该解决方案应通过用指数分布代替随机采样的均匀分布来工作。
P-Gn 2014年

5

首先,让我们从待排序列表中给定元素的权重为常数开始进行工作。在迭代之间它不会改变。如果可以,那么……那是一个更大的问题。

为了进行说明,让我们使用一副纸牌,在其中要将正面卡加权到前面。 weight(card) = card.rank。总结一下,如果我们不知道权重的分布确实为O(n)一次。

这些元素存储在排序结构中,例如在可索引跳转列表上进行的修改,以便可以从给定节点访问级别的所有索引:

   1 10
 o ---> o -------------------------------------------- -------------> o顶层
   1 3 2 5
 o ---> o -----------------> o ---------> o ---------------- -----------> o 3级
   1 2 1 2 5
 o ---> o ---------> o ---> o ---------> o ----------------- ----------> o 2级
   1 1 1 1 1 1 1 1 1 1 1 
 o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o底层

头1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th NIL
      节点节点节点节点节点节点节点节点节点节点节点

但是,在这种情况下,每个节点还“占用”了其重量一样大的空间。

现在,在此列表中查找卡时,可以在O(log n)时间访问其位置,并在O(1)时间从关联的列表中将其删除。好的,可能不是O(1),可能是O(log log n)时间(我不得不考虑更多)。在上面的示例中,删除第6个节点将涉及更新所有四个级别-这四个级别与列表中有多少个元素无关(取决于实现这些级别的方式)。

由于元素的重量是恒定的,因此可以简单地执行操作sum -= weight(removed)而不必再次遍历该结构。

因此,您获得了O(n)的一次性成本和O(log n)的查找值以及从O(1)的列表成本中删除的成本。这就是O(n)+ n * O(log n)+ n * O(1),这使您的整体性能为O(n log n)。


让我们用卡片看看,因为那是我上面使用的。

      10
前3名-----------------------> 4d
                                。
       3 7。
    2 ---------> 2d ---------> 4d
                  。。
       1 2。3 4。
机器人1->广告-> 2d-> 3d-> 4d

这是一个真正的小甲板只有4中它卡。应该很容易看到如何扩展它。如果使用52张卡,理想的结构将具有6个级别(log 2(52)〜= 6),尽管如果您跳入跳过列表,甚至可以减少到较小的数量。

所有权重的总和为10。因此,您可以从[1 .. 10)及其4中获得一个随机数。 您可以通过跳过列表查找位于ceiling(4)处的项目。由于4小于10,因此您从顶层移至第二层。四个大于3,所以现在我们在菱形的2处。4小于3 + 7,所以我们向下移动到底部,而4小于3 + 3,所以我们有3个菱形。

从结构中删除3个菱形后,该结构现在如下所示:

       7
顶部3 ----------------> 4d
                         。
       3 4。
    2 ---------> 2d-> 4d
                  。。
       1 2。4。
机器人1->广告-> 2d-> 4d

您会注意到,节点在结构中占据与其权重成比例的“空间”量。这允许进行加权选择。

由于这近似于平衡的二叉树,因此无需在底层遍历(即为O(n)),而是从顶层开始即可快速跳过结构以查找所需内容对于。

相反,其中大部分可以通过某种平衡树来完成。问题是,当删除节点时,结构的重新平衡变得令人困惑,因为这不是经典的树状结构,并且要记住,钻石4现在已从[6 7 8 9]位置移至[3 4] [5 6]可能比树形结构的收益要高。

但是,尽管跳过列表在O(log n)时间内可以向下跳过列表的能力近似于二叉树,但它具有处理链接列表的简单性。

这并不是说所有操作都很容易(删除元素时,您仍然需要在所有需要修改的链接上保留制表符),但这意味着仅更新无论您拥有多少关卡及其链接而不是正确的树结构上的所有内容。


我不确定您所描述的内容如何与“跳过列表”相匹配(但是,我确实只是在查找跳过列表)。根据我在Wikipedia上的了解,权重越高,权重越低。但是,您描述的是跳绳的宽度应为重量。另一个问题...使用这种结构,您如何选择随机元素?
弥敦道·美林

1
@MrTi因此对可索引跳过列表的想法进行了修改。关键是要能够以O(log n)时间而不是O(n)时间将先前元素的权重总和<23才能访问该元素。您仍然可以按照描述的方式选择随机元素,从[0,sum(weights)]中选择一个随机数,然后从列表中获取相应的元素。节点/卡在跳过列表中的顺序无关紧要-因为较重的加权项占用的较大“空间”是关键。

啊我懂了。我喜欢。
弥敦道·美林
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.