Создать случайное нарушение списка

Как я могу случайным образом перетасовать список, чтобы ни один из элементов не оставался в исходном положении?

Другими словами, учитывая список A с разными элементами, я хотел бы сгенерировать его перестановку B, чтобы

  • эта перестановка случайна
  • и для каждого n, a[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

Я не знаю правильного термина для такой перестановки (это "total"?), тем самым имея трудное время для поиска. Правильный термин кажется "расстройством".

Ответ 1

После некоторых исследований мне удалось реализовать алгоритм "раннего отказа", как описано, например, в в этой статье. Это происходит следующим образом:

import random

def random_derangement(n):
    while True:
        v = 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

Я выбираю этот алгоритм для простоты, эта презентация кратко описывает другие идеи.

Спасибо всем))

Ответ 2

Такие перестановки называются нарушениями. На практике вы можете просто попробовать случайные перестановки до тех пор, пока не столкнетесь с расстройством, их отношение приближается к обратному "e" по мере роста "n".

Ответ 3

В качестве возможной отправной точки тасовка Фишера-Йейта выглядит следующим образом.

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 приведены на статье в Википедии).

Ответ 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, требуется долгое время для Samsung Galaxy S4.

РЕДАКТИРОВАТЬ 2

Примерно через 1200 секунд (20 минут) программа закончила говорить "Total Random shuffle possible". Для больших массивов вам нужно очень большое количество перестановок... Скажите len (массив) ** 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)

Ответ 5

Здесь ниже, с питоническим синтаксисом -

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

Ответ 7

const derange = (array) => {
  const suffledArray = [...array].sort(_ => Math.random() - 0.5)
  const hasElementOnSamePosition = suffledArray.some((element, i) => array[i] === element)
  return (hasElementOnSamePosition ? derange(array) : suffledArray)
}

Ответ 8

Быстрый способ - попытаться перетасовать свой список, пока не достигнете этого состояния. Вы просто пытаетесь перетасовать свой список, пока у вас не останется список, который удовлетворяет вашему состоянию.

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)