【问题标题】:Generate a random derangement of a list生成列表的随机混乱
【发布时间】:2014-10-01 17:45:30
【问题描述】:

如何随机打乱一个列表,以使所有元素都不会保留在其原始位置?

换句话说,给定一个具有不同元素的列表A,我想生成一个排列B,以便

  • 这个排列是随机的
  • 对于每个na[n] != b[n]

例如

a = [1,2,3,4]
b = [4,1,2,3] # good
b = [4,2,1,3] # good

a = [1,2,3,4]
x = [2,4,3,1] # bad

我不知道这种排列的正确术语(它是“全部”吗?)因此很难在谷歌上搜索。正确的术语似乎是“混乱”。

【问题讨论】:

  • 我会注意到这不是一个“完全”随机的随机播放。
  • 在 stackoverflow 上有一个类似的问题 -> stackoverflow.com/questions/7279895/… 但回答者说:My algorithm is actually bad: you still have a chance of ending with the last point unshuffled. 希望这可以帮助您指出正确的方向。
  • @J0HN:我已经编辑了示例以明确这一点。
  • [1,1,2,3] 怎么样?只有索引需要“完全”打乱,还是算法也应该查看值?
  • @OllieFord:我的意思是,算法应该随机生成“好的”排列之一。

标签: python random permutation


【解决方案1】:

经过一些研究,我能够实现“提前拒绝”算法,如描述的,例如在this paper。它是这样的:

import random

def random_derangement(n):
    while True:
        v = [i for i in range(n)]
        for j in range(n - 1, -1, -1):
            p = random.randint(0, j)
            if v[p] == j:
                break
            else:
                v[j], v[p] = v[p], v[j]
        else:
            if v[0] != 0:
                return tuple(v)

我们的想法是:我们不断改组数组,一旦我们发现我们正在处理的排列无效 (v[i]==i),我们就会中断并从头开始。

快速测试表明该算法均匀地生成所有紊乱:

N = 4

# enumerate all derangements for testing
import itertools
counter = {}
for p in itertools.permutations(range(N)):
    if all(p[i] != i for i in p):
        counter[p] = 0

# make M probes for each derangement
M = 5000
for _ in range(M*len(counter)):
    # generate a random derangement
    p = random_derangement(N)
    # is it really?
    assert p in counter
    # ok, record it
    counter[p] += 1

# the distribution looks uniform
for p, c in sorted(counter.items()):
    print p, c

结果:

(1, 0, 3, 2) 4934
(1, 2, 3, 0) 4952
(1, 3, 0, 2) 4980
(2, 0, 3, 1) 5054
(2, 3, 0, 1) 5032
(2, 3, 1, 0) 5053
(3, 0, 1, 2) 4951
(3, 2, 0, 1) 5048
(3, 2, 1, 0) 4996

为了简单起见,我选择了这个算法,this presentation 简要概述了其他想法。

【讨论】:

    【解决方案2】:

    这样的排列被称为错位。在实践中,您可以尝试随机排列,直到出现紊乱,随着 'n' 的增长,它们的比率接近 'e' 的倒数。

    【讨论】:

    • 谢谢! “精神错乱”是我一直在寻找的词。
    • 我认为这里的任何其他答案(包括我的)都不能解决问题 - 这应该是公认的答案。
    • 我试过这个(比如while 1: shuffle(a); if is_derangement(a) return a),不幸的是结果分布不均匀。
    • @georg 你能扩展一下吗?如果shuffle() 在排列上提供均匀分布,那么从shuffle() 的输出中选择混乱(或任何其他集合)必须在混乱(或任何其他集合)上提供均匀分布。
    • @RafałDowgird:可能是我的测试有点偏离。无论如何,“早期故障”算法是对这种方法的合理优化,对我来说效果很好。
    【解决方案3】:

    作为一个可能的起点,Fisher-Yates shuffle 是这样的。

    def swap(xs, a, b):
        xs[a], xs[b] = xs[b], xs[a]
    
    def permute(xs):
        for a in xrange(len(xs)):
            b = random.choice(xrange(a, len(xs)))
            swap(xs, a, b)
    

    也许这样可以解决问题?

    def derange(xs):
        for a in xrange(len(xs) - 1):
            b = random.choice(xrange(a + 1, len(xs) - 1))
            swap(xs, a, b)
        swap(len(xs) - 1, random.choice(xrange(n - 1))
    

    这是 Vatine 描述的版本:

    def derange(xs):
        for a in xrange(1, len(xs)):
            b = random.choice(xrange(0, a))
            swap(xs, a, b)
        return xs
    

    快速统计测试:

    from collections import Counter
    
    def test(n):
        derangements = (tuple(derange(range(n))) for _ in xrange(10000))
        for k,v in Counter(derangements).iteritems():
            print('{}   {}').format(k, v)
    

    test(4):

    (1, 3, 0, 2)   1665
    (2, 0, 3, 1)   1702
    (3, 2, 0, 1)   1636
    (1, 2, 3, 0)   1632
    (3, 0, 1, 2)   1694
    (2, 3, 1, 0)   1671
    

    这确实在其范围内看起来是一致的,并且它具有每个元素在每个允许的插槽中出现的机会均等的好属性。

    但不幸的是,它并不包括所有的紊乱。有 9 个大小为 4 的错位。(n=4 的公式和示例在 the Wikipedia article 上给出)。

    【讨论】:

    • 我认为这有“索引溢出”或“保留最后一个元素不变”的风险。
    • 嗯,你是对的。修复了索引问题,但最后一个元素可能保持不动。
    • 再次编辑 - 这个有机会吗?虽然我现在太累了,无法计算。
    • Quick 说“可能”。显然,从索引 1 开始,选择“小于当前索引”作为交换位置,一个经典的 Fisher-Yates(您实现为“统一洗牌算法”,尽管在列表的“通过”部分进行交换)保证所有元素最终都位于不同的位置,并且您的第二个算法似乎可以做到这一点,但使用数组的“尾”端。
    • 再次更新。我的版本不起作用(遭受与以前相同的索引溢出,仅使用倒数第二个元素而不是最后一个元素)。而你的(假设我正确实施)不会产生所有可能的混乱。
    【解决方案4】:

    这应该可以工作

    import random
    
    totalrandom = False
    array = [1, 2, 3, 4]
    it = 0
    while totalrandom == False:
        it += 1
        shuffledArray = sorted(array, key=lambda k: random.random())
        total = 0
        for i in array:
            if array[i-1] != shuffledArray[i-1]: total += 1
        if total == 4:
            totalrandom = True
    
        if it > 10*len(array):
            print("'Total random' shuffle impossible")
            exit()
    print(shuffledArray)
    

    注意变量it,如果调用了太多迭代,它会退出代码。这考虑了诸如 [1, 1, 1] 或 [3] 之类的数组

    编辑

    事实证明,如果您将它用于大型阵列(大于 15 个左右),它将占用大量 CPU。使用随机生成的 100 元素数组并将其提升到 len(array)**3,我的三星 Galaxy S4 需要很长时间才能解决。

    编辑 2

    大约 1200 秒(20 分钟)后,程序以“不可能完全随机洗牌”结束。对于大型数组,您需要大量的排列...比如说 len(array)**10 或其他东西。

    代码:

    import random, time
    
    totalrandom = False
    array = []
    it = 0
    
    for i in range(1, 100):
        array.append(random.randint(1, 6))
    
    start = time.time()
    
    while totalrandom == False:
        it += 1
        shuffledArray = sorted(array, key=lambda k: random.random())
        total = 0
        for i in array:
            if array[i-1] != shuffledArray[i-1]: total += 1
        if total == 4:
            totalrandom = True
    
        if it > len(array)**3:
            end = time.time()
            print(end-start)
            print("'Total random' shuffle impossible")
            exit()
    
    end = time.time()
    print(end-start)
    print(shuffledArray)
    

    【讨论】:

    • 换句话说,尝试生成排列,直到找到正确的排列。这可能有效,但我有点担心性能(我的列表有 100 多个元素)。
    • @georg 是的,我刚刚自己测试过......我认为克里斯马丁的答案可能是最好的
    【解决方案5】:

    这是一个较小的,具有pythonic语法 -

    import random
    def derange(s):
     d=s[:]
     while any([a==b for a,b in zip(d,s)]):random.shuffle(d)
     return d
    

    它所做的只是打乱列表直到没有元素匹配。另外,请注意,如果传递了一个不能被破坏的列表,它将永远运行。当有重复时会发生这种情况。要删除重复项,只需调用 derange(list(set(my_list_to_be_deranged))) 这样的函数。

    【讨论】:

    • 但这可能会持续很长时间!
    【解决方案6】:
    import random
    a=[1,2,3,4]
    c=[]
    i=0
    while i < len(a):
        while 1:
         k=random.choice(a)
         #print k,a[i]
         if k==a[i]:
             pass
         else:
             if k not in c:
                 if i==len(a)-2:
                     if a[len(a)-1] not in c:
                         if k==a[len(a)-1]:
                             c.append(k)
                             break
                     else:
                         c.append(k)
                         break
                 else:
                     c.append(k)
                     break
        i=i+1
    print c
    

    【讨论】:

    • 我不明白你的代码是做什么的。想发表评论吗?
    • 这里我从输入列表中选择了一个随机元素。然后我检查它是否已经在新列表中。如果没有,那么我检查它是否等于索引处的 d 元素。如果不是我放的 den一个条件来检查它是否像 312 和最后一个条件必须是 4 但这不应该在 d ans 中。所以我排除了这种情况。
    • 我刚刚对此进行了测试(请参阅我的答案中的测试功能)。分布不均匀。
    • 它确实包含所有的安排,对吧?想不出它不统一的原因:(
    【解决方案7】:

    一种快速的方法是尝试打乱您的列表,直到您达到该状态。您只需尝试打乱您的列表,直到您得到满足您条件的列表。

    import random
    import copy
    
    
    def is_derangement(l_original, l_proposal):
        return all([l_original[i] != item for i, item in enumerate(l_proposal)])
    
    
    l_original = [1, 2, 3, 4, 5]
    l_proposal = copy.copy(l_original)
    
    while not is_derangement(l_original, l_proposal):
        random.shuffle(l_proposal)
    
    print(l_proposal)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-10-05
      • 2020-05-23
      • 1970-01-01
      • 2016-07-15
      • 1970-01-01
      相关资源
      最近更新 更多