#!/usr/bin/env python
##
## Name: perm.py
## Purpose: Various computations related to permutation.
## Author: M. J. Fromberger
## Info: $Id: Permutation.py 441 2007-10-19 14:59:52Z sting $
##
from operator import add, itemgetter
import random
class Permutation (object):
"""This class allows the manipulation of permutations as
computational objects. The basic operation on a permutation
is multiplication, which applies the permutation on the RHS
to the permutation or sequence on the LHS and returns the
result.
"""
def __init__(self, seq):
"""Construct a new permutation from a list of indices. The
indices must cover the range (0,..len(seq)).
"""
self._perm = tuple(seq)
def __len__(self):
"""Return the number of elements of the permutation."""
return len(self._perm)
def __getitem__(self, itm):
"""Read elements or slices of the permutation."""
return self._perm[itm]
def __iter__(self):
"""Iterate over the indices of the permutation."""
return iter(self._perm)
def __repr__(self):
return 'Permutation(%s)' % (self._perm,)
def __mul__(self, other):
"""Multiply (compose) this permutation with another permutation.
The dimensions of the two arguments must match.
"""
if len(other) <> len(self):
raise ValueError("Arguments have incompatible dimensions")
try:
return Permutation(self[i] for i in other)
except TypeError:
raise ValueError("Incorrect type for permutation index")
def __rmul__(self, other):
"""Right-multiply another sequence type by this permutation,
assuming compatible dimensions.
"""
if len(other) <> len(self):
raise ValueError("Arguments have incompatible dimensions")
if isinstance(other, str):
return ''.join(other[i] for i in self)
else:
return type(other)(other[i] for i in self)
def __add__(self, other):
"""Add this permutation to either another permutation or to an
index. The effect is to add the indices of the permutation,
modulo the number of total permutations. If the right-hand
argument is a permutation, it must have compatible dimensions.
"""
my_idx = self.index()
try:
other_idx = other.index()
if len(self) <> len(other):
raise ValueError("Arguments have incompatible dimensions")
except AttributeError:
try:
other_idx = int(other)
except ValueError:
raise TypeError("Right-hand argument must be permutation"
" or integer")
new_idx = my_idx + other_idx
try:
return Permutation.from_index(len(self), new_idx)
except IndexError:
return Permutation.from_index(len(self),
new_idx % factorial(len(self)))
def __pow__(self, z):
"""Raise a permutation to a power by repeated composition."""
if not isinstance(z, (int, long)) or z < 0:
raise TypeError("power must be a non-negative integer")
# This is basically the square-and-multiply algorithm that is
# used for ordinary exponentiation, only using composition of
# permutations instead
out = Permutation.identity(len(self))
cur = self
while z <> 0:
if z % 2 == 1:
out *= cur
cur *= cur
z /= 2
return out
def inverse(self):
"""Return the inverse of the permutation."""
inv = [0] * len(self)
for i in xrange(len(self)):
inv[self[i]] = i
return Permutation(inv)
def reversed(self):
"""Return the reverse of the permutation."""
return Permutation(reversed(self))
def invert(self):
"""Invert the permutation in place."""
inv = [0] * len(self)
for i in xrange(len(self)):
inv[self[i]] = i
self._perm = tuple(inv)
def reverse(self):
"""Reverse the permutation in place."""
self._perm = tuple(reversed(self._perm))
return self
def cycles(self):
"""Return a list of permutation indices in cycle normal form:
Each cycle begins with its least index, and all cycles are
ordered decreasing by their primary indices.
"""
def tindex(tup, elt): # Since tuples have no .index().
for pos in xrange(len(tup)):
if tup[pos] == elt:
return pos
else:
return -1
P = self._perm
not_done = list(P)
cycles = []
while not_done:
pos = tindex(P, not_done[0])
cycle = [ pos ]
not_done.remove(pos)
cursor = P[pos]
while cursor <> pos:
cycle.append(cursor)
not_done.remove(cursor)
cursor = P[cursor]
cycles.append(cycle)
return reduce(add, sorted(cycles, key = itemgetter(0),
reverse = True))
def index(self):
"""Return the index of this permutation in the canonical
factoradic mapping of permutations to indices in (0,..n!).
"""
tmp = range(len(self))
idx = [None] * len(self)
for pos, elt in enumerate(self):
idx[pos] = tmp.index(elt)
tmp.pop(idx[pos])
return fact2int(idx)
@staticmethod
def identity(n):
"""Generate an identity permutation of (0,..n)."""
return Permutation(xrange(n))
@staticmethod
def random(n):
"""Generate a random permutation of (0,..n)."""
tmp = range(n)
random.shuffle(tmp)
return Permutation(tmp)
@staticmethod
def from_index(n, z):
"""Construct a permutation of (0,..n) from an integer index in
the range (0,..n!). Raises IndexError if z >= n!
"""
tmp = range(n)
out = [None] * n
idx = int2fact(z)
idx = [0] * (n - len(idx)) + idx
for pos, elt in enumerate(idx):
out[pos] = tmp[elt]
tmp.pop(elt)
return Permutation(out)
@staticmethod
def from_cycles(cyc):
"""Construct a permutation from a list in cycle normal form."""
breaks = [ p for p in xrange(len(cyc))
if p > 0 and not [ x for x in cyc[:p]
if x < cyc[p] ] ]
res = [None] * len(cyc)
last = 0
for b in breaks + [len(cyc)]:
cur = cyc[last:b]
last = b
save = prev = cur[0]
for i in cur[1:]:
res[prev] = i
prev = i
res[prev] = save
return Permutation(res)
def invert_perm_inplace(P):
"""Given a permutation P of (0,..n), specified as a list, compute
the inverse permutation P' in place, replacing P. The result P is
also returned.
"""
mark = [False] * len(P)
for i in xrange(len(P)):
if not mark[i]:
j = P[i]
save = i
while not mark[j]:
mark[j] = True
next = P[j]
P[j] = save
save = j
j = next
return P
def generate_moves(cycles):
"""Given a permutation of (0,..n) as a list of disjoint cycles,
return a minimal list of the "moves" necessary to apply that
permutation to a sequence. The moves are human-readable strings
of the form
move ,
... where and are either locations in the sequence
being permuted, or the word "spare" to indicate a location that is
separate."""
moves = []
for cycle in cycles:
if len(cycle) > 1:
moves.append('move %d, spare' % cycle[0])
prev = cycle[0]
for elt in reversed(cycle[1:]):
moves.append('move %d, %d' % (elt, prev))
prev = elt
moves.append('move spare, %d' % cycle[1])
return moves
def permutations(seq):
"""Returns a generator that yields all the permutations of seq in
lexicographic order. The order of elements in seq determines the
basic ordering of permutations.
The generator does not copy its input, so modifying the source
while the generator is idle will change the output.
"""
# In-place permutation algorithm from E. W. Dikjstra, "A
# Discipline of Programming" (1976).
px = range(len(seq) + 1) # permutation indices
px[-1] = -1 # sentinel
while True:
yield list(seq[p] for p in px[:-1])
for i in reversed(xrange(len(seq))):
if px[i] < px[i + 1]:
break
else:
return # no more permutations
for j in reversed(xrange(len(seq))):
if px[j] > px[i]:
break
px[i], px[j] = px[j], px[i]
for j in reversed(xrange(len(seq))):
i += 1
if j <= i:
break
px[i], px[j] = px[j], px[i]
def count_breaks(n, k):
"""Returns the number of distinct ways to partition a sequence of
length n into k + 1 groups of contiguous elements. For instance,
if n = 4 and k = 2, there are three ways: a:b:cd, a:bc:d, and
ab:c:d."""
def cb(n, k, c):
assert n > 0
if (n, k) in c:
return c[n, k]
if k == 0 or k == n - 1:
return 1
elif k == 1:
return n - 1
else:
c[n, k] = sum([ sum([ cb(p, i, c) *
cb(n - p, k - i, c)
for i in xrange(min(k, p)) ])
for p in xrange(1, n) ])
return c[n, k]
return cb(n, k, {})
def int2fact(z):
"""Compute the factoradic representation of abs(z) for an integer
z, returning the digits in an array of decreasing order of
significance.
"""
if z == 0:
return [0]
elif z < 0:
z = -z
out = []
rdx = 1
while z <> 0:
out.append(z % rdx)
z /= rdx
rdx += 1
out.reverse()
return out
def fact2int(fr):
"""Convert a factoradic representation of an integer back into the
integer it represents. The input should be a list of digit values
in decreasing order of significance.
"""
out = 0
pval = 1
for pos, digit in enumerate(reversed(fr)):
out += pval * digit
pval *= pos + 1
return out
def factorial(n):
"""Compute the integer factorial."""
s = 1
while n > 0:
s *= n
n -= 1
return s
__all__ = ("Permutation", "int2fact", "fact2int", "permutations")
# Here there be dragons