Hi everyone!
I have this blog drafter for quite a while, but the $300 contest motivated me to actually finish and publish it. I don't care about the prize that much as I think many other blogs published recently are talking about something even cooler. But I like to make people suffer, so I will apply anyway to make peltorator read even more stuff >:)
Besides, the contest makes it harder to stay on contribution top, so I got to do something to stay afloat. That being said...
Today I'd like to write about an algorithm that solves the following problem:
You're given a set of permutations Σ={σ1,…,σm}. Find the size |⟨σ1,…,σm⟩| of a group G generated by Σ.
At first, I wanted to get into a lot of group-theoretic details with proofs and all, but then I felt like it would make the article harder to comprehend, so I will try to keep group-theoretic stuff to minimum, while telling more about the high-level idea.
Prerequisites
It is strongly recommended to be familiar with some simpler basis constructing algorithms, e.g. Gauss elimination or XOR basis.
Familiarity with basic group theory (e.g. definitions) will also be beneficial.
What does the statement mean?
In this article, we will treat permutations as a bijective functions from {1,…,n} to itself.
For example, the permutation σ=(123231) is perceived as a function σ, such that σ(1)=2, σ(2)=3 and σ(3)=1.
Then, we may as well define the composition σ1σ2 of permutations σ1 and σ2 as (σ1σ2)(k)=σ1(σ2(k)).
By the group ⟨G⟩ generated by Σ we mean the set of permutation that we will get by repeatedly composing permutations with each other in all possible ways, until no new permutations may be produced. Further we will rely on the following properties of permutations:
- There is an identity permutation e(k)=k, such that eσ=σe=σ for any σ;
- For every permutation σ, there is unique σ−1 such that σ(σ−1(k))=σ−1(σ(k))=k, that is σσ−1=σ−1σ=e.
Together, the properties above just say that permutations form a group.
High-level idea
General idea of the algorithm is to find k sets G1,…,Gk⊂G such that every element g∈G may be uniquely represented as g=g1g2…gk, where g1∈G1,…,gk∈Gk. Then it will also hold that |G|=|G1||G2|…|Gk|.
It would be even nicer to make G1,…,Gk such that finding g1g2…gk for any given g is a reasonably simple task.
Which G1,…,Gk to use?
This idea above is quite similar to representing any vector r of a linear space as a linear combination r=a1r1+⋯+akrk of basis vectors. The basis r1,…,rk is usually constructed in a row-echelon form, meaning that each vector has a specific coordinate associated with it, so that all basis vectors with higher indices have zero coefficient in this coordinate. In this way, the coefficient ai may be found just by looking on the corresponding coordinate of r−a1r1−⋯−ai−1ri−1.
Generally, in linear spaces we may allow zero vectors in the basis and choose r1,…,rn in such way that ri has zero value in all coordinates j<i. Then, the vector airi is uniquely determined by the i-th coefficient of r−a1r1−⋯−ai−1ri−1.
Getting back to permutations, Gi may be interpreted as a set of all possible values of airi. So instead of representing the vector r as a linear combination of airi, we represent the permutation g as a composition of gi. In linear spaces, we wanted airi to have the value 0 in coordinates j<i. And also for airi to be uniquely determined by the i-th coordinate of r−a1r1−⋯−ai−1ri−1.
What would it mean for us in permutation context?
What is the equivalent of r−a1r1−⋯−ai−1ri−1 in permutations?
We ultimately want g=g1…gn. Cancelling first i−1 permutations on both sides, we get to g′=g−1i−1…g−11g.
What is the equivalent of coordinates in permutations?
It's the values σ(1),σ(2),…,σ(n).
What is the equivalent of j-th coordinate being 0 for j<i?
We needed it, so that the composition of g−1i and g′ always preserved g′(j)=j for j<i.
By definition it means that g−1i(g′(j))=g′(j)=j, which only happens when g−1i(j)=gi(j)=j.
What is the equivalent of "airi is uniquely determined by the i-th coordinate of r−a1r1−⋯−ai−1ri−1"?
In the very end we ultimately want g−nn…g−11g=e, where e is the identity permutation e(k)=k.
To achieve this, we must pick gi in such way that g−1i will turn g′(i) into i.
By definition, it means that g−1i(g′(i))=i, or equivalently gi(i)=g′(i).
In other words, we must have a(i)≠b(i) for any a,b∈Gi.
That being said, Schreier-Sims algorithm constructs n sets G1,…,Gn such that
- every element gi∈Gi has gi(j)=j for j<i;
- all elements gi∈Gi have distinct gi(i);
- The sets G and G1∪G2∪⋯∪Gn generate the same permutation group.
I will use Python in my code examples below. Let schreier_sims(S)
be the function that takes a list of permutations Σ and returns G1,…,Gn. Each Gi is also represented as a list of permutations. Then checking that an element belongs to G may be done as follows:
# compute the composition p[i] = p1[p2[i]]
def apply(p1, p2):
return tuple(p1[p2[i]] for i in range(len(p2)))
# compute the inverse permutation ans[p[i]] = i
def inverse(p):
ans = [0] * len(p)
for i in range(len(p)):
ans[p[i]] = i
return tuple(ans)
# compute the sets G1, ..., Gn
def schreier_sims(S):
...
# G is the list of Schreier-Sims basis sets,
# p is the permutation that we need to check
def belongs(G, p):
for i in range(len(G)):
for g in G[i]:
if p[i] == g[i]:
p = apply(inverse(g), p)
return all(p[j] == j for j in range(len(p)))
belongs(schreier_sims([[1, 2, 0]]), [2, 0, 1]) # True
belongs(schreier_sims([[1, 2, 0]]), [2, 1, 0]) # False
Is this enough?
No! There is an additional implicit assumption that we have made here. Specifically, that every permutation g∈G such that g(j)=j for j<i can be generated just by permutations from the sets Gi,Gi+1,…,Gn. While this is a somewhat natural assumption if you're familiar with Gauss algorithm, it isn't necessarily true.
It could be that combining permutations from Gi, you will get a permutation gi such that gi(i)=i and then you will have to apply it to make g an identity permutation in the end. Therefore, we need a stronger set of constraints:
- every element gi∈Gi has gi(j)=j for j<i;
- all elements gi∈Gi have distinct gi(i);
- For any permutation g∈⟨G⟩ such that g(j)=j for j<i, it is also true that g∈⟨Gi,Gi+1,…,Gn⟩.
In group theory terms, the last condition means that the set of permutations G1∪⋯∪Gn is a strong generating set of ⟨G⟩.
Note that this is very similar to how we construct the basis for vectors over remainders modulo possibly composite number m (see e.g. the article by errorgorn). Key difference with vectors over fields here is the possible existence of torsions, when it is possible to annihilate the i-th coordinate of a vector by multiplying the vector with a non-zero constant (thus, possibly not annihilating the remaining coordinates).
Similarly with permutations, it may be possible to annihilate the i-th term of a permutation from Gi by applying other permutations from Gi to it, without making the permutation as a whole the identity permutation, hence it might be still needed in the decomposition unless G1,…,Gn constitutes a strong generating set.
How to construct G1,…,Gn?
Schreier-Sims algorithms construct a strong generating set iteratively. Let G(i) be the set of permutation g∈⟨G⟩, such that g(j)=j for j<i. In particular, G(1)=⟨G⟩. Note that G(i) is closed under composition, that is g1g2∈G(i) for any g1,g2∈G(i), because
for any j<k. In group-theoretic terms, G(i) is called a stabilizer subgroup of 1,2,…,i−1.
From the definition of a strong generating set above it follows that
The iteration of the Schreier-Sims algorithm assumes that we have a generating set of permutation S such that G(i)=⟨S⟩, and from that it finds a set Gi and a new generating set S′ such that G(i)=⟨Gi∪S′⟩, and G(i+1)=⟨S′⟩. In this way, it is possible to apply similar steps iteratively to construct Gi+1,Gi+2,…,Gn.
Finding Gi
To find Gi, we should find a representative for every possible value of g(i) for g∈G(i). In other words, for every obtainable j≠i, we should find a permutation gj∈G(i) such that gj(i)=j. Once we apply g−1j, to the permutation g having g(i)=j, we would make its i-th term being equal to i and therefore it will move to G(i+1), which is dealt with later on.
That being said, Gi will consist exactly of gj for all obtainable j. The set of all possible g(i) is called the orbit of the number i. We can find and organize the orbit by building a so-called Schreier tree. The tree consists of all possible values of g(i), and edges of the tree correspond to permutations that should be applied to change one element of the orbit to another.
Now, let gj∈G(i) be such that gj(i)=j. Then for any g∈S it holds that (ggj)(i)=g(j). This fact allows to construct the Schreier tree with a simple depth-first search. We will store actual permutations gj in an array orbit
:
# orbit[j] = permutation g in <S> s.t. g[i] = j
def build_schreier_tree(i, S, orbit):
for g in S:
if g[i] not in orbit:
orbit[g[i]] = apply(g, orbit[i])
# g[i] = j
build_schreier_tree(g[i], S, orbit)
The running time of the construction above is O(n|S|+n2).
Finding S′
Assume that we have S s.t. ⟨S⟩=G(i) and Gi, represented by the elements of the orbit of i. Then the generating set S′ such that ⟨S′⟩=G(i+1) can be found from what is called the Schreier's lemma:
Here, orbiti are the possible values of g(i) for g∈G(i) and gj∈Gi is a representative having gj(i)=j.
Indeed, consider an element s′=s1s2…sk∈G(i+1). Let ri=sisi+1…sk, then we may rewrite it as
which, using that s′(i)=i and rk(i)=sk(rk+1(i))=sk(u) for rk+1(i)=u, in turn further rewrites as
The process of generating such set is straight-forward:
# Given <S> = G^(i) and
# the orbit representatives of i,
# find S' s.t. <S'> = G^(i+1)
def make_gen(S, orbit):
n = len(next(iter(S)))
newS = set()
for s in S:
for u in orbit:
newS.add(reduce(apply, [inverse(orbit[s[u]]), s, orbit[u]]))
return newS
Note that the size of S′ generated in this way may be as high as n|S|, which would allow an exponential growth if not halted.
Making sure S′ has a reasonable size
For any set S′, it is possible to construct another set S″ such that \langle S' \rangle = \langle S' ' \rangle, but |S' '| \leq n^2. There are several ways to construct such sets, for example Jerrum's filter even guarantees that |S' '| \leq n, but at the cost of higher construction complexity. In Schreier-Sims algorithm, however, since the construction and sifting is repeated several times iteratively, the preferred way is Sims filter. It guarantees that for any pair (i, j) there is at most one permutation g \in S' ' such that g(k)=k for k < i and g(i)=j.
The construction process for it is fairly straightforward and essentially repeats the Gauss method:
# Sifts S and returns S'
# such that <S> = <S'> and |S'| <= n^2
def normalize(S):
n = len(next(iter(S)))
newS = set()
base = [{} for i in range(n)]
for s in S:
for x in range(0, n):
if s[x] != x:
if s[x] in base[x]:
s = apply(inverse(s), base[x][s[x]])
else:
base[x][s[x]] = s
newS.add(s)
break
return newS
Getting it all together
Gluing all the pieces mentioned above together, we get the following algorithm:
def schreier_sims(S):
n = len(next(iter(S)))
ans = []
for i in range(n):
orbit = {}
# initiate with identity permutation
orbit[i] = tuple(range(n))
# find G_i = orbit
build_schreier_tree(i, S, orbit)
# add G_i to the answer
ans += [[orbit[j] for j in orbit]]
# generate S' and make sure it is at most n^2 in size
S = normalize(make_gen(S, orbit))
return ans
Complexity analysis here is not very pleasant and yields something around O(n^6). It is possible to further reduce it by roughly a factor of n by terminating the sifting process earlier, if sufficient amount of uniformly generated elements of G are sifted through.
Example problem
There was a problem in Petrozavodsk Summer-2011. Moscow SU Unpredictable Contest that asked specifically to find the size of a permutation group for n \leq 50. If you don't have an access to opentrains, I have uploaded this specific problem to the mashup which you can access by the invitation link, and here is my submission to it using the Schreier-Sims algorithm: [submission:188935679].