Skip to content

Commit 92a13a0

Browse files
[3.14] Add derangements() recipe (gh-143671) (gh-143677)
1 parent 7ed4134 commit 92a13a0

File tree

1 file changed

+41
-1
lines changed

1 file changed

+41
-1
lines changed

Doc/library/itertools.rst

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -842,7 +842,7 @@ and :term:`generators <generator>` which incur interpreter overhead.
842842
from contextlib import suppress
843843
from functools import reduce
844844
from math import comb, prod, sumprod, isqrt
845-
from operator import itemgetter, getitem, mul, neg
845+
from operator import is_not, itemgetter, getitem, mul, neg
846846

847847
def take(n, iterable):
848848
"Return first n items of the iterable as a list."
@@ -978,6 +978,16 @@ and :term:`generators <generator>` which incur interpreter overhead.
978978
slices = starmap(slice, combinations(range(len(seq) + 1), 2))
979979
return map(getitem, repeat(seq), slices)
980980

981+
def derangements(iterable, r=None):
982+
"Produce r length permutations without fixed points."
983+
# derangements('ABCD') → BADC BCDA BDAC CADB CDAB CDBA DABC DCAB DCBA
984+
# Algorithm credited to Stefan Pochmann
985+
seq = tuple(iterable)
986+
pos = tuple(range(len(seq)))
987+
have_moved = map(map, repeat(is_not), repeat(pos), permutations(pos, r=r))
988+
valid_derangements = map(all, have_moved)
989+
return compress(permutations(seq, r=r), valid_derangements)
990+
981991
def iter_index(iterable, value, start=0, stop=None):
982992
"Return indices where a value occurs in a sequence or iterable."
983993
# iter_index('AABCADEAF', 'A') → 0 1 4 7
@@ -1663,6 +1673,36 @@ The following recipes have a more mathematical flavor:
16631673
['A', 'AB', 'ABC', 'ABCD', 'B', 'BC', 'BCD', 'C', 'CD', 'D']
16641674

16651675

1676+
>>> ' '.join(map(''.join, derangements('ABCD')))
1677+
'BADC BCDA BDAC CADB CDAB CDBA DABC DCAB DCBA'
1678+
>>> ' '.join(map(''.join, derangements('ABCD', 3)))
1679+
'BAD BCA BCD BDA CAB CAD CDA CDB DAB DCA DCB'
1680+
>>> ' '.join(map(''.join, derangements('ABCD', 2)))
1681+
'BA BC BD CA CD DA DC'
1682+
>>> ' '.join(map(''.join, derangements('ABCD', 1)))
1683+
'B C D'
1684+
>>> ' '.join(map(''.join, derangements('ABCD', 0)))
1685+
''
1686+
>>> # Compare number of derangements to https://oeis.org/A000166
1687+
>>> [len(list(derangements(range(n)))) for n in range(10)]
1688+
[1, 0, 1, 2, 9, 44, 265, 1854, 14833, 133496]
1689+
>>> # Verify that identical objects are treated as unique by position
1690+
>>> identical = 'X'
1691+
>>> distinct = 'x'
1692+
>>> seq1 = ('A', identical, 'B', identical)
1693+
>>> result1 = ' '.join(map(''.join, derangements(seq1)))
1694+
>>> result1
1695+
'XAXB XBXA XXAB BAXX BXAX BXXA XAXB XBAX XBXA'
1696+
>>> seq2 = ('A', identical, 'B', distinct)
1697+
>>> result2 = ' '.join(map(''.join, derangements(seq2)))
1698+
>>> result2
1699+
'XAxB XBxA XxAB BAxX BxAX BxXA xAXB xBAX xBXA'
1700+
>>> result1 == result2
1701+
False
1702+
>>> result1.casefold() == result2.casefold()
1703+
True
1704+
1705+
16661706
>>> list(powerset([1,2,3]))
16671707
[(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]
16681708
>>> all(len(list(powerset(range(n)))) == 2**n for n in range(18))

0 commit comments

Comments
 (0)