Representing permutations
Here is a way to represent a permutation with a fairly economical number of bits.
The number of permutations on objects is which is Therefore, to specify one of these permutations requires at least bits. I found a way to represent such a permutation with bits. This has probably been discovered before (even several times). But I still enjoyed coming up with it.
Sufficiency
Let It is easiest to represent a permutation on no elements, of course. This requires no bits of data—the permutation must send the element to itself. All the information is contained, so to speak, in the type of the permutation, and there is only one such permutation. And in fact,
So suppose instead that is a permutation on elements. To fix ideas, let us assume the elements are the numbers Then the permutation fixes We may therefore regard as a permutation on elements. By induction, we may represent with bits. Since there are possibilities for (for itself is a possibility), we may represent with bits. Now, so and together determine Therefore we may represent with
bits, completing the induction.
How to act
By the above argument, we may write
Hence the initial cycle is the one on the “inside.” It is easier to shift bits right than to shift them left. So we will put the initial cycle’s bits in the least significant position possible. Thus in general, cycles’ bits will be recorded in the least significant position possible, after prior cycles’ bits have been so recorded.
To give an example before formalizing this in code, consider the
number 100
given in binary. Now, we want this to represent
a permutation
on four elements. So we’ll write it as 00100
instead. The
least significant bit is 0
, indicating the cycle
instead of the identity cycle
The next bits need to represent one of the three possible elements
in the next cycle
This requires two bits, which here are 10
, which is 2
itself. Thus this represents the cycle
Finally we require two bits still to represent the four possible
elements
in the final cycle
These bits are 00
, so this cycle is
All told then we have
With that example given, let’s now derive more formally how to act on an element via a permutation given via this representation. The overall structure should be something like
initialize variables
while (there is another transposition)
apply it to x
update variables
return x
How do we tell if there is another tranposition, and if so, what it is? Considering the previous example, we see that it might be useful to have a bitmask variable to grab a collection of bits; and a variable indicating what element the current transposition considers its initial element in the cycle. Now, the question is whether all that is enough information to tell when to exit the loop. If we allow ourselves to refer to the number say as a constant, then yes we can. If we can stop, since only acts on So now we know the algorithm looks like
initialize variables
while (f < N)
get next tranposition
apply it to x
update variables
return x
Clearly the next transposition is
where
is the number indicated by the next collection of bits. One convenient
way to write this would be y ← mask & bits
, where
mask
is a mask with the appropriate number of bits;
bits
are the remaining bits that we haven’t dealt with from
the representation of
and &
is bitwise-and, not
boolean-and. So we can rewrite the loop body as follows:
initialize variables
while (f < N)
y ← mask & bits
if x = f
x ← y
else if x = y
x ← f
update variables
return x
This has a branch inside a loop, which is not great. Identifying
booleans with natural numbers in the usual way,
viz. false = 0
and true = 1
, we can rewrite
the branch as follows.
initialize variables
while (f < N)
y ← mask & bits
x ← x + (x = f)⋅(y-x) + (x = y)⋅(f-x)
update variables
return x
It remains to determine how to update the variables f
and mask
. Of course f
is easy; just increment
it. Now, mask
is supposed to be
least-significant 1 bits, and 0 bits everywhere else. That is, it is
supposed to be the number
But putting that in the update would evaluate a discrete logarithm. We
can do better than that. First, we know f
is representable
in
bits. The maximum number so representable happens to be
mask
itself. If f+1 > mask
then the next
f
will require an additional bit. So instead of evaluating
a discrete logarithm, to update the variables we can just write
f ← f+1
if f > mask
mask ← 2⋅mask + 1
Again, this conditional assignment occurs in a loop. We may rewrite it as follows.
b ← f > mask
mask ← mask + b⋅(mask + 1)
This assumes that the next interesting bits are already least
significant. So we must also update the bits
variable by
shifting it right the appropriate number of bits. We could divide
bits
by mask+1
, but division is best avoided.
Instead we keep track of the number of bits in the mask,
nbits
. We increment it along with mask
when
needed. Thus updating the variables looks like this:
bits ← bits >> nbits
f ← f+1
b ← f > mask
mask ← mask + b⋅(mask + 1)
nbits ← nbits + b
It remains to initialize the variables. Clearly we begin with
f = 0
.1 There are no other elements yet that
we need to distinguish from f
, so nbits = 0
and mask = 0
likewise.
Therefore, in conclusion, the following Hoare triple should hold. (Here we use Dijkstra’s guarded command language.)
f,nbits,mask,x ← 0,0,0,X;
do f < N →
y ← mask & bits
; x ← x + (x=f)⋅(y-x) + (x=y)⋅(f-x)
; bits ← bits >> nbits
; f ← f+1
; b ← f > mask
; mask, nbits ← mask + b⋅(mask+1), nbits + b
od;
In Python we could write this as follows.
def act(N,sigma,x):
= 0,0,0
f,nbits,mask while f < N:
= mask & sigma
y += (x==f)*(y-x) + (x==y)*(f-x)
x >>= nbits
sigma += 1
f = f > mask
b += b*(mask+1)
mask += b
nbits return x
Some comments
The above method is redundant, and doesn’t check that all the tranpositions have the form with One could write a method to verify the form of the tranposition before using it. The above code is faster for skipping this verification.
Also, this method is as data-space-efficient as possible for Since I like three-dimensional computational topology, this is all I need. However, for the number of permutations is at most 24. So a table lookup in this case might be more appropriate than the above function!
The biggest permutations you can fit in one 64-bit word are those on nineteen elements. Remarkably, these fit exactly in 64 bits with the above representation. Likewise, one can fit permutations on five elements in a single byte.
Permutations on 32 elements need 124 bits with this representation, and no more elements are possible with 128 bits allowed. This is a (space) improvement over the “sheep-and-goats” representation in Warren’s excellent Hacker’s Delight, which uses 160 bits to represent permutations on 32 elements.
Footnote
The reader may be concerned about the base case It is true that the code above will necessarily fail in this case. But it also succeeds in this case, since to run the code, you first need an element to run it on. Assuming we have an element from the empty set lets us conclude the code terminates correctly, even though it also fails.↩︎