16

How can I randomly shuffle a list so that none of the elements remains in its original position?

In other words, given a list A with distinct elements, I'd like to generate a permutation B of it so that

  • this permutation is random
  • and for each n, a[n] != b[n]

e.g.

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

I don't know the proper term for such a permutation (is it "total"?) thus having a hard time googling. The correct term appears to be "derangement".

georg
  • 211,518
  • 52
  • 313
  • 390
  • 11
    I would note then that this is not a 'totally' random shuffle. – Paddy Aug 08 '14 at 09:34
  • 2
    There is a similar question on stackoverflow -> http://stackoverflow.com/questions/7279895/shuffle-list-ensuring-that-no-item-remains-in-same-position But the answerer says: `My algorithm is actually bad: you still have a chance of ending with the last point unshuffled.` Hope this helps you pointing in the right direction. – Mathias Aug 08 '14 at 09:41
  • @J0HN: I've edited the example to make the point clear. – georg Aug 08 '14 at 09:46
  • 1
    What about `[1,1,2,3]`? Do only the indexes need to be "totally" shuffled, or should the algorithm look at the values, too? – Kijewski Aug 08 '14 at 09:48
  • Your second condition can't be fulfilled for a constant list like [1, 1, 1]. So I think, what you want is "and for each n: B(n) != n", which means a permutation without fix points. – Thomas B. Aug 08 '14 at 09:55
  • Your two criteria are contradictory. If it's a random permutation, not all `b[n]` will be distinct from `a[n]`. – OJFord Aug 08 '14 at 09:58
  • @Kay: doesn't matter as all elements are distinct – georg Aug 08 '14 at 10:02
  • 1
    @OllieFord: what I mean, the algorithm should generate one of the "good" permutations randomly. – georg Aug 08 '14 at 10:05
  • Possible duplicate: http://stackoverflow.com/questions/8369941/how-to-generate-permutations-where-ai-i – Rafał Dowgird Aug 08 '14 at 11:02
  • @RafałDowgird: you're missing the "random" point. – georg Aug 08 '14 at 11:20

7 Answers7

10

After some research I was able to implement the "early refusal" algorithm as described e.g. in this paper [1]. It goes like this:

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)

The idea is: we keep shuffling the array, once we find that the permutation we're working on is not valid (v[i]==i), we break and start from scratch.

A quick test shows that this algorithm generates all derangements uniformly:

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

Results:

(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

I choose this algorithm for simplicity, this presentation [2] briefly outlines other ideas.

References:

  • [1] An analysis of a simple algorithm for random derangements. Merlini, Sprugnoli, Verri. WSPC Proceedings, 2007.
  • [2] Generating random derangements. Martínez, Panholzer, Prodinger.
Stef
  • 13,242
  • 2
  • 17
  • 28
georg
  • 211,518
  • 52
  • 313
  • 390
  • I took the liberty of adding explicit references to the paper and presentation you link, so that they can still be found if the links break. – Stef Dec 05 '22 at 08:50
6

Such permutations are called derangements. In practice you can just try random permutations until hitting a derangement, their ratio approaches the inverse of 'e' as 'n' grows.

Rafał Dowgird
  • 43,216
  • 11
  • 77
  • 90
  • Thanks! "derangement" is the word I was looking for. – georg Aug 08 '14 at 12:05
  • I don't think any of the other answers here (including mine) solve the problem - This should be the accepted answer. – Chris Martin Aug 09 '14 at 05:55
  • I tried this (like `while 1: shuffle(a); if is_derangement(a) return a`), unfortunately the resulting distribution is not uniform. – georg Aug 11 '14 at 08:02
  • @georg Could you expand? If `shuffle()` provides uniform distribution over permutations, then selecting derangements (or any other set) from the output of `shuffle()` must provide uniform distribution over derangements (or any other set). – Rafał Dowgird Aug 11 '14 at 10:44
  • @RafałDowgird: could be that my tests were a bit off. In any case, the "early fault" algorithm is a reasonable optimization of this approach and works quite well for me. – georg Aug 11 '14 at 20:43
4

As a possible starting point, the Fisher-Yates shuffle goes like this.

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)

Perhaps this will do the trick?

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))

Here's the version described by Vatine:

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

A quick statistical test:

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

This does appear uniform over its range, and it has the nice property that each element has an equal chance to appear in each allowed slot.

But unfortunately it doesn't include all of the derangements. There are 9 derangements of size 4. (The formula and an example for n=4 are given on the Wikipedia article).

Chris Martin
  • 30,334
  • 10
  • 78
  • 137
  • I think this risks one of "index overflow" or "leave last element untouched". – Vatine Aug 08 '14 at 09:43
  • Hmm, you're right. Fixed the index problem, but it is possible for the last element to remain unmoved. – Chris Martin Aug 08 '14 at 09:51
  • Edited again - This one has a chance? Though I'm too tired to do the math at the moment. – Chris Martin Aug 08 '14 at 10:20
  • Quick says "probably". Apparently, a classic Fisher-Yates (what you implemented as the "uniform shuffle algorithm", although swapping in the "passed" section of the list), starting from index 1, choosing "less than current index" as the position to swap with guarantees that all elements end up in a different position and your second algorithm seems to do that, but with the "tail" end of the array. – Vatine Aug 08 '14 at 13:57
  • Updated again. My version doesn't work (suffers from the same index overflow as before, only with the second-to-last element instead of the last). And yours (assuming I implemented correctly) doesn't generate all possible derangements. – Chris Martin Aug 09 '14 at 05:51
1

This should work

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)

Note the variable it which exits the code if too many iterations are called. This accounts for arrays such as [1, 1, 1] or [3]

EDIT

Turns out that if you're using this with large arrays (bigger than 15 or so), it will be CPU intensive. Using a randomly generated 100 element array and upping it to len(array)**3, it takes my Samsung Galaxy S4 a long time to solve.

EDIT 2

After about 1200 seconds (20 minutes), the program ended saying 'Total Random shuffle impossible'. For large arrays, you need a very large number of permutations... Say len(array)**10 or something.

Code:

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)
Beta Decay
  • 805
  • 1
  • 8
  • 20
  • In other words, try generating permutations until the correct one is found. This might work, but I'm a bit concerned about performance (my lists have 100+ elements). – georg Aug 08 '14 at 12:06
  • @georg Yeah, I just tested that myself... I think Chris Martin's answer might be the best then – Beta Decay Aug 08 '14 at 12:45
1

Here is a smaller one, with pythonic syntax -

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

All it does is shuffles the list until there is no element-wise match. Also, be careful that it'll run forever if a list that cannot be deranged is passed.It happens when there are duplicates. To remove duplicates simply call the function like this derange(list(set(my_list_to_be_deranged))).

Gurupad Mamadapur
  • 989
  • 1
  • 13
  • 24
0
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
vks
  • 67,027
  • 10
  • 91
  • 124
  • 1
    I don't understand what your code does. Care to comment? – georg Aug 08 '14 at 12:07
  • Here i chose a random element from the input list.then i check if its already in new list.if not then i check if its equal to d element at the index.if not den i have put one condition to check if its a condition like 312 and last has to be 4 but that shoudnt be in d ans.so i exclude that case. – vks Aug 08 '14 at 12:18
  • I just tested this (see the test function in my answer). The distribution isn't uniform. – Chris Martin Aug 10 '14 at 19:12
  • it does contain all the arrangements right?can't think of a reason why it is not uniform :( – vks Aug 11 '14 at 04:39
0

A quick way is to try to shuffle your list until you reach that state. You simply try to shuffle your list until you are left with a list that satisfies your condition.

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)
Rafael
  • 7,002
  • 5
  • 43
  • 52