from __init__ import install_dependencies
await install_dependencies()
%reload_ext divewidgets
Permutation using recursion¶
For examples:
- The list of (3-)permutations of
1,2,3
is:
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]
- The list of 2-permutations of
1,2,3
is:
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
- The list of 4-permutation of 1,2,3 is:
[]
In the above, we used
- a
tuple
delimited by()
such as(1,2,3)
to store items of a permutation, and - a
list
delimited by[]
such as[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]
to store all the permutations.
Generating permutations is a fundamental problem in computing and combinatorics. A simple way to generate permutations is by the following recurrence relation. (There are also other algorithms.)
The following implements the recurrence equation as a recursion:
def permutation(*a, k=None):
"""Construct all (k-)permutations of the position arguments.
Parameters
----------
*a: object, object, ...
n items specified as positional arguments
k: int
Optional argument indicating the size of each permutation.
Default: number n of items specified in *a.
Returns
-------
list:
List of all k-permutations represented as ordered tuples of the n objects.
"""
n = len(a)
output = []
if k is None:
k = n
if 0 < k <= n:
for i in range(n):
output.extend(
[(a[i],) + p for p in permutation(*a[:i], *a[i + 1 :], k=k - 1)]
)
elif k == 0:
return [()]
return output
print(permutation(1, 2, 3))
print(permutation(1, 2, 3, k=2))
In particular, the recurrence is implemented by the for loop:
...
for i in range(n):
output.extend([(a[i], ) + p
for p in permutation(*a[:i], *a[i + 1:], k=k - 1)])
...
In the above code, (a[i],) + p
creates a -permutation of the items in a
by concatenating for each index i
- a singleton tuple
(a[i],)
and - a -permutation
p
of all elements buta[i]
.
(See the example in the recurrence relation described earlier.)
YOUR ANSWER HERE
Number of permutations¶
Computing permutations using recursion is slow. Why?
There are too many permutations, even for a reasonably small . Try running the following code in a new console:
n = 10
output = permutation(*range(1, n + 1))
print(f"# {n}-permutations:", len(output))
Quite surprisingly, the number of permutations can be calculated significantly faster without enumerating all the permutations.
The quantity is fundamental in the field of combinatorics with enormous applications.
def num_permutation(n):
"""Compute the number of permutations.
Parameters
----------
n: int
Number of items.
Return
------
int:
Number of permutations of n items.
"""
# YOUR CODE HERE
raise NotImplementedError
Source
# tests
assert num_permutation(10) == 3628800
assert num_permutation(0) == 1
assert num_permutation(-1) == 0
def num_permutation(n, k=None):
"""Compute the number of k-permutations of n items.
Parameters
----------
n: int
Number of items to permute.
k: int
Optional argument indicating the size of each permutation.
Default: n
Returns
-------
int:
Number of k-permutations of n items.
"""
# YOUR CODE HERE
raise NotImplementedError
Source
# tests
assert isinstance(num_permutation(0), int)
assert num_permutation(3) == 6
assert num_permutation(3, 0) == 1
assert num_permutation(3, 2) == 6
assert num_permutation(10, 5) == 30240
Permutation using iteration¶
The following function permutation_sequence(*a)
returns a generator that generates the list of -permutations one-by-one for from 0 to len(a)
.
def permutation_sequence(*a):
"""Returns a generator for the k-permutations of the positional arguments
for k from 0 to len(a)."""
n = len(a)
output, idx_left = [()], [tuple(range(n))]
for k in range(n + 1):
yield output
next_output, next_idx_left = [], []
for m in range(len(idx_left)):
for j in range(len(idx_left[m])):
i = idx_left[m][j]
next_output.append(output[m] + (a[i],))
next_idx_left.append(idx_left[m][:j] + idx_left[m][j + 1 :])
output, idx_left = next_output, next_idx_left
for permutation_list in permutation_sequence(1, 2, 3):
print(permutation_list)
Unlike the recursive function permutation
, the above generates a -permutation of items iteratively by
- choosing for from 0 to such that
- is not already chosen, i.e., .
E.g., the permutations of is generated iteratively as follows:
- 1
- 1,2
- (1,2,3)
- 1,3
- (1,3,2)
- 2
- 2,1
- (2,1,3)
- 2,3
- (2,3,1)
- 3
- 3,1
- (3,1,2)
- 3,2
- (3,2,1)
Is iteration significantly faster?
Try running the following code in a new console:
n = 10
for k, permutation_list in enumerate(permutation_sequence(*range(1, n + 1))):
print("# {}-permutations of {} items: {}".format(k, n, len(permutation_list)))
Unfortunately, there is not much improvement. Nevertheless, we can efficiently compute the number of -permutations based on the previously computed number of -permutations:
For from 0 to ,
def num_permutation_sequence(n):
output = 1
for k in range(0, n + 1):
# YOUR CODE HERE
raise NotImplementedError
Source
# tests
assert [m for m in num_permutation_sequence(3)] == [1, 3, 6, 6]
assert [m for m in num_permutation_sequence(10)] == [
1,
10,
90,
720,
5040,
30240,
151200,
604800,
1814400,
3628800,
3628800]
def num_permutation_sequence(n):
# YOUR CODE HERE
raise NotImplementedError
Source
# tests
g = num_permutation_sequence(3)
assert (next(g), next(g), g.send(0), next(g), next(g), next(g), g.send(0)) == (
1,
3,
4,
12,
24,
24,
120)
Deduplication using Decorator¶
An issue with the function permutation
is that it regards arguments at different positions as distinct, even if they may have the same value. E.g.,permutation(1,1,2)
returns [(1, 1, 2), (1, 2, 1), (1, 1, 2), (1, 2, 1), (2, 1, 1), (2, 1, 1)]
where each distinct permutation appears twice.
To remove duplicates from a list, we can
- convert the list to a
set
(which automatically removes duplicates), - and then convert the set back to a list.
print("Deduplicated:", list(set(permutation(1, 1, 2))))
Using a decorator, we can fix permutation
without rewriting the function.
import functools
def deduplicate_output(f):
"""Takes in a function f that returns a list possibly with duplicates,
returns a decorator that removes duplicates from the output list."""
@functools.wraps(f)
def wrapper(*args, **kwargs):
return list(set(f(*args, **kwargs)))
return wrapper
permutation = deduplicate_output(permutation)
print("Deduplicated: ", permutation(1, 1, 2))
permutation = permutation.__wrapped__
print("Original: ", permutation(1, 1, 2))
def deduplicate_input(f):
"""Takes in a function f that takes a variable number of arguments
possibly with duplicates, returns a decorator that removes duplicates
in the positional argument."""
@functools.wraps(f)
def wrapper(*args, **kwargs):
# YOUR CODE HERE
raise NotImplementedError
return wrapper
Source
# tests
permutation = deduplicate_input(permutation)
try:
assert set(permutation(1, 1, 2)) == set([(1, 2), (2, 1)])
finally:
permutation = permutation.__wrapped__