Algorithm
There is an idea based on subfactorial formula !n = (n-1)(!(n-1) + !(n-2))
.
There are two ways to generate derangement of size n
:
Generate derangement of size n-1
, append n
-th item, swap n
-th and random item in range [1, n-1]
.
Generate derangement of size n-2
, get i = random[1, n-1]
, increment all values >= i
in the derangement, insert value i
to i
-th place. The modified permutation is not derangement any more, but the only item (i
) is on his place. Append n
-th item, swap n
-th and i
-th items. The result is derangement.
To generate derangement we select first of second way with probabilities
!(n-1) / (!(n-1) + !(n-2))
and !(n-2) / (!(n-1) + !(n-2))
.
The only issue is that this algorithm is not linear of n
in time. I think it is O(n^2)
due to item insertion and increment in the second way.
Evaluation
Below is test in Python. It runs permutation generator 1000000 times and shows number of calls to random generator, expected number of permutations of particular kind, distribution of permutations.
MotiNK
has been added in the last moment to check it. Unfortunately it does not generate all permutations.
Derangement
implements the idea described above. It works OK but it is quadratic. :(
FisherYatesDerangement1
is Fisher-Yates "try and reject".
FisherYatesDerangement2
is optimized slightly. It is possible to reject permutation early while Fisher-Yates is generating permutation.
FisherYates
is added as a baseline. It does not generate derangements but all permutations.
import collections
import random
def is_derangement(a):
for i, v in enumerate(a):
if i == v:
return False
return True
# suggested by [MotiNK](https://cs.stackexchange.com/users/37884/motink)
#
# for i=0..n-2
# swapIdx = random(i+1,n) // random(start,end) --> start <= i < end
# swapElement(array, i, swapIdx)
class MotiNK:
def __init__(self, n):
self._n = n
self._c = 0
def next(self):
a = list(range(self._n))
for i in range(self._n - 1):
j = random.randrange(i + 1, self._n)
self._c += 1
a[i], a[j] = a[j], a[i]
assert is_derangement(a)
return tuple(a)
def count(self):
return self._c
class FisherYates:
def __init__(self, n):
self._n = n
self._c = 0
def next(self):
a = list(range(self._n))
for i in range(self._n):
j = random.randrange(i, self._n)
self._c += 1
a[i], a[j] = a[j], a[i]
return tuple(a)
def count(self):
return self._c
class FisherYatesDerangement1:
def __init__(self, n):
self._fy = FisherYates(n)
def next(self):
while True:
a = self._fy.next()
if is_derangement(a):
return a
def count(self):
return self._fy.count()
class FisherYatesDerangement2:
def __init__(self, n):
self._n = n
self._c = 0
def next(self):
while True:
a = list(range(self._n))
if self._shuffle(a):
assert is_derangement(a)
return tuple(a)
def count(self):
return self._c
def _shuffle(self, a):
for i in range(self._n):
j = random.randrange(i, self._n)
self._c += 1
if i == a[j]:
return False
a[i], a[j] = a[j], a[i]
return True
def subfactorial(n):
f1 = 0
f2 = 1
for i in range(n):
f1, f2 = f2, i * (f1 + f2)
return f2
class Derangement:
def __init__(self, n):
self._n = n
self._c = 0
def next(self):
a = self._derangement(self._n)
assert(is_derangement(a))
return tuple(a)
def count(self):
return self._c
def _derangement(self, n):
assert n != 1
if n == 0:
return []
n1 = subfactorial(n - 1)
n2 = subfactorial(n - 2)
k = random.randrange(n1 + n2)
self._c += 1
i = random.randrange(n - 1)
self._c += 1
if k < n1:
a = self._derangement(n - 1)
else:
a = self._derangement(n - 2)
for j in range(n - 2):
if a[j] >= i:
a[j] += 1
a.insert(i, i)
a.append(n - 1)
a[i], a[n - 1] = a[n - 1], a[i]
return a
def test_generator(k, label, gen):
c = collections.Counter(gen.next() for _ in range(k))
m = len(c)
print(
label,
f'{gen.count():10}',
f'{k / m:10.2f}',
sorted(c.values())
)
for n in range(3, 6):
print('n =', n)
m = 1000000
test_generator(m, 'MotiNK ', MotiNK(n))
test_generator(m, 'Derangement ', Derangement(n))
test_generator(m, 'FisherYatesDerangement1', FisherYatesDerangement1(n))
test_generator(m, 'FisherYatesDerangement2', FisherYatesDerangement2(n))
test_generator(m, 'FisherYates ', FisherYates(n))
Results:
n = 3
MotiNK 2000000 500000.00 [499557, 500443]
Derangement 4000000 500000.00 [499959, 500041]
FisherYatesDerangement1 8986053 500000.00 [499855, 500145]
FisherYatesDerangement2 6493519 500000.00 [498982, 501018]
FisherYates 3000000 166666.67 [166124, 166291, 166399, 166842, 166971, 167373]
n = 4
MotiNK 3000000 166666.67 [165922, 166499, 166550, 166680, 166937, 167412]
Derangement 5332446 111111.11 [110578, 110775, 111087, 111098, 111105, 111165, 111184, 111501, 111507]
FisherYatesDerangement1 10663028 111111.11 [110525, 110740, 110906, 111113, 111193, 111203, 111421, 111436, 111463]
FisherYatesDerangement2 7434896 111111.11 [110378, 110400, 111099, 111141, 111186, 111194, 111300, 111543, 111759]
FisherYates 4000000 41666.67 [41325, ..., 41911]
n = 5
MotiNK 4000000 41666.67 [41338, ..., 42069]
Derangement 7091636 22727.27 [22449, ..., 23012]
FisherYatesDerangement1 13626165 22727.27 [22403, ..., 23004]
FisherYatesDerangement2 9344157 22727.27 [22392, ..., 23014]
FisherYates 5000000 8333.33 [8118, ..., 8632]