pythonrandompermutation

Generate a random derangement of a list


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

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".


Solution

  • 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: