Permutation Generation Algorithms
This article introduces common algorithms for permuation generation.
Algorithm 1: Backtracking Algorithm
def perm_backtrack(l):
'''
Switch-to-end backtracking.
'''
def dfs(l, n):
if n == 0:
print(l)
return
for j in range(n):
l[n - 1], l[j] = l[j], l[n - 1]
dfs(l, n - 1)
l[n - 1], l[j] = l[j], l[n - 1]
dfs(l, len(l))
The idea is, when generating permutation of length n
, first switch each
element to the last index and then call recursion on length n - 1
.
To work correctly, the content of the list needs to be kept the same as original when the call returns. That’s why we use backtracking, as shown by the second swap.
Algorithm 2: Nested Rotation Algorithm
def perm_rotation(l):
'''
Nested rotation.
'''
def dfs(l, i):
if i == len(l):
print(l)
return
for _ in range(len(l) - i):
dfs(l, i + 1)
l.append(l.pop(i))
dfs(l, 0)
I came up with this function after inspired by a combinatorial problem. I like its succinctness so much. This is probably the shortest permutation generator one can ever write.
The idea is to rotate sublists at different levels. This not only fills the last position with different values, but also restores the sublists to their original state when the recursion exits. Thanks to the rounding nature of rotation, there is not any backtracking artifact (such as the second swap in Algorithm 1).
The problem with this algorithm is that it doesn’t work well on arrays, because arrays don’t support rotation very well (\(O(n)\) instead of \(O(1)\) time). However, it’d be OK with a linked list. Also, if efficiency is not an issue, this function is probably the easiest to write due to its beautiful nature.
Algorithm 3: Steinhaus–Johnson–Trotter Algorithm
def perm_sjt(l):
'''
Steinhaus–Johnson–Trotter algorithm (plain changes).
'''
n = len(l)
idx = [i for i in range(n)]
pos = [i for i in range(n)]
count = [n - 1 - i for i in range(n)]
step = [1 for i in range(n)]
cur = 0
while True:
print(l)
while count[cur] == 0:
step[cur] = -step[cur]
count[cur] = n - 1 - cur
cur = cur + 1
if cur == n:
return
cpos, npos = pos[cur], pos[cur] + step[cur]
idx[cpos], idx[npos] = idx[npos], idx[cpos]
l[cpos], l[npos] = l[npos], l[cpos]
pos[idx[cpos]], pos[idx[npos]] = cpos, npos
count[cur] -= 1
cur = 0
The code I came up with here isn’t the original SJT algorithm, but it shares the same idea of exchanging adjacent elements. It looks similar to Even’s speedup, but has a different implementation.
The good point is that only one exchange needs to be done for each permutation
(the idx
list is not needed if data type is numeric). The while loop executes
only when Element 0 “hits the end”, which happens \(1/n\) of the time. Thus
the while loop overhead is \(O(1 / n * n) = O(1)\). This means the time needed
for each permutation is constant (amortized).
Algorithm 4: Heap’s Algorithm
def perm_heap(l):
'''
Heap's algorithm.
'''
def dfs(l, n):
if n == 0:
print(l)
return
for i in range(n):
dfs(l, n - 1)
pos = 0 if n % 2 else i
l[pos], l[n - 1] = l[n - 1], l[pos]
dfs(l, len(l))
This is an amazing algorithm which reduces the number of element exchanges down to 1. This means there is no backtracking (as you can compare it with Algorithm 1). It exchanges the last element with:
-
The first element, if array length is odd.
-
The i-th element, if array length is even.
It’s not obvious why this works correctly, so let’s demystify it.
-
When
n == 1
, the algorithm starts with[0]
and ends with[0]
. -
When
n == 2
, the algorithm starts with[0, 1]
and ends with[1, 0]
:0, 1 # i == 0, swap l[0] with l[1] 1, 0 # i == 1, swap l[1] with l[1] 1, 0
-
When
n == 3
, the algorithm starts with[0, 1, 2]
and ends with[0, 1, 2]
:0, 1, 2 # n == 2 recursion 1, 0, 2 # i == 0, swap l[0] with l[2] 2, 0, 1 # n == 2 recursion 0, 2, 1 # i == 1, swap l[0] with l[2] 1, 2, 0 # n == 2 recursion 2, 1, 0 # i == 2, swap l[0] with l[2] 0, 1, 2
-
When
n == 4
, the algorithm starts with[0, 1, 2, 3]
and ends with[3, 0, 1, 2]
:0, 1, 2, 3 # n == 3 recursion 0, 1, 2, 3 # i == 0, swap l[0] with l[3] 3, 1, 2, 0 # n == 3 recursion 3, 1, 2, 0 # i == 1, swap l[1] with l[3] 3, 0, 2, 1 # n == 3 recursion 3, 0, 2, 1 # i == 2, swap l[2] with l[3] 3, 0, 1, 2 # n == 3 recursion 3, 0, 1, 2 # i == 3, swap l[3] with l[3] 3, 0, 1, 2
We may guess that:
-
The algorithm keeps the subarray intact when its length
n
is odd, and it correctly computes permutation with lengthn
. -
The algorithm rotates the subarray one place to the right when its length
n
is even, and it correctly computes permutation with lengthn
.
This is true and can be proved using induction:
-
The algorithm keeps the subarray intact when
n
is 1. And the algorithm correctly generates permutation with length 1. -
Suppose the algorithm keeps the subarray intact when
n == 2m - 1
. We prove the algorithm rotates the subarray one place to the right whenn == 2m
.We prove by drawing a table for when
n == 2m
:0, 1, 2, ..., 2m - 3, 2m - 2, 2m - 1 # n == 2m - 1 recursion 0, 1, 2, ..., 2m - 3, 2m - 2, 2m - 1 # i == 0, swap l[0] with l[2m - 1] 2m - 1, 1, 2, ..., 2m - 3, 2m - 2, 0 # n == 2m - 1 recursion 2m - 1, 1, 2, ..., 2m - 3, 2m - 2, 0 # i == 1, swap l[1] with l[2m - 1] 2m - 1, 0, 2, ..., 2m - 3, 2m - 2, 1 # n == 2m - 1 recursion 2m - 1, 0, 2, ..., 2m - 3, 2m - 2, 1 # i == 2, swap l[2] with l[2m - 1] 2m - 1, 0, 1, ..., 2m - 3, 2m - 2, 2 # n == 2m - 1 recursion 2m - 1, 0, 1, ..., 2m - 3, 2m - 2, 2 # ... ... 2m - 1, 0, 1, ..., 2m - 4, 2m - 2, 2m - 3 # n == 2m - 1 recursion 2m - 1, 0, 1, ..., 2m - 4, 2m - 2, 2m - 3 # i == 2m - 2, swap l[2m - 2] with l[2m - 1] 2m - 1, 0, 1, ..., 2m - 4, 2m - 3, 2m - 2 # n == 2m - 1 recursion 2m - 1, 0, 1, ..., 2m - 4, 2m - 3, 2m - 2 # i == 2m - 1, swap l[2m - 1] with l[2m - 1] 2m - 1, 0, 1, ..., 2m - 4, 2m - 3, 2m - 2
The result proves the claim.
Please also note that the algorithm enumerates all possible values in the last element and correctly generates
n
-permulation from(n-1)
-permutation. -
Suppose the algorithm rotates the subarray one place to the right when
n == 2m
. We prove the algorithm keeps the subarray intact whenn == 2m + 1
.We prove by drawing a table for when
n == 2m + 1
:0 , 1, 2, ..., 2m - 2, 2m - 1, 2m # n == 2m recursion 2m - 1, 0, 1, ..., 2m - 3, 2m - 2, 2m # i == 0, swap l[0] with l[2m] 2m , 0, 1, ..., 2m - 3, 2m - 2, 2m - 1 # n == 2m recursion 2m - 2, 2m, 0, ..., 2m - 4, 2m - 3, 2m - 1 # i == 1, swap l[0] with l[2m] 2m - 1, 2m, 0, ..., 2m - 4, 2m - 3, 2m - 2 # n == 2m recursion 2m - 3, 2m - 1, 2m, ..., 2m - 5, 2m - 4, 2m - 2 # i == 2, swap l[0] with l[2m] 2m - 2, 2m - 1, 2m, ..., 2m - 5, 2m - 4, 2m - 3 # ... ... 2 , 3, 4, ..., 2m , 0 , 1 # n == 2m recursion 0 , 2, 3, ..., 2m - 1, 2m , 1 # i == 2m - 1, swap l[0] with l[2m] 1 , 2, 3, ..., 2m - 1, 2m , 0 # n == 2m recursion 2m , 1, 2, ..., 2m - 2, 2m - 1, 0 # i == 2m, swap l[0] with l[2m] 0 , 1, 2, ..., 2m - 2, 2m - 1, 2m
The result proves the claim.
Please also note that the algorithm enumerates all possible values in the last element and correctly generates
n
-permulation from(n-1)
-permutation.
This concludes the proof.