Prerequisites: Basic concepts of abstract algebra, Group theory, and Permutations
Introduction
The Schreier–Sims algorithm is an algorithm in computational group theory, named after the mathematicians Otto Schreier and Charles Sims. It can solve many problems in polynomial time, such as finding the order of a finite permutation group and checking whether a given permutation belongs to a given group. The Schreier–Sims algorithm was first introduced by Sims in 1970 based on Schreier's lemma. In 19811, Donald Knuth further improved its running time. A faster randomized version was later developed. Computer algebra systems such as GAP and Magma usually use highly optimized Monte Carlo versions2 of this algorithm.
Notation
Following the convention in computational group theory literature, this article uses a right action for group actions, meaning that permutations are composed from left to right. All group actions considered here can be viewed as permutation actions, although some algorithms also work for more general group actions. Accordingly, the set on which the group acts is by default , whose elements are called points. The result of permutation acting on point is denoted by ; we also say that permutation moves point to . Under the action of permutation group , the orbit of point is denoted by , and its stabilizer is denoted by . The notion of a stabilizer also extends to a set : its stabilizer is defined as .
Overview
The Schreier–Sims algorithm mainly attempts to solve the following problem:
- Given a set of size , a set , and a set of permutations, how can we efficiently store the permutation group generated by , namely , in a computer and perform a series of queries on that group?
Clearly, such a group may be very large, far larger than the sets and . For example, the order of the -th symmetric group is , but it can be generated by only two permutations. Storing every element of the group is impractical.
Similar to constructing a linear basis of a vector space using Gaussian elimination, the Schreier–Sims algorithm seeks a “basis” for the finite permutation group :
- The input is a generating set for , namely , containing several permutations.
- If is nontrivial, the action of always moves some point , namely .
- Find the orbit of point , , and for every point , find in a permutation that moves to .
- Find a generating set for the stabilizer of point , , denoted .
- Recursively call the algorithm on until the trivial group is reached.
The reason this works is that the stabilizer of point , , is a subgroup of , and its right cosets partition . These cosets correspond one-to-one with the orbit of point , . The permutations found in step 3 are representatives of these cosets. The set of coset representatives is called a transversal of the stabilizer . In other words, every element of , denoted , corresponds to a unique pair , with . Therefore, storing the subgroup together with its transversal is enough to store all of . The problem of storing the subgroup has already been solved: simply make the recursive call.
The implementation still has many details to organize, which is the main subject of this article. Before that, we first examine the result after calling the algorithm and see what structure it uses to store the group and what queries it can answer. Some concepts are needed first.
Stabilizer chain
Suppose the algorithm is called on a total of times. At the -th call, the input is a set of permutations, the point found is , the transversal obtained is , and the generating set of the stabilizer is . Let . The algorithm obtains the subgroup chain
Moreover, every subgroup in the chain is a stabilizer:
Thus, the Schreier–Sims algorithm can be viewed as computing a stabilizer chain.
Base and strong generating set
If a subset of , denoted , satisfies , then is called a base of permutation group . Clearly, the algorithm obtains a base . This means that the results of the permutation acting on these points uniquely determine this permutation in . The base output by the algorithm also satisfies
That is, every point in the base contains useful information about the elements of the group. Such a base is called nonredundant. The algorithm always outputs a nonredundant base, whose corresponding stabilizer chain is strictly descending.
The union of the generating sets obtained by the algorithm,
is also a generating set for , and holds. A generating set satisfying this condition is called a strong generating set of relative to the base . Thus, the Schreier–Sims algorithm can also be viewed as computing a base and strong generating set (BSGS) for group . These two descriptions are equivalent, and this article does not distinguish between them.
The algorithm also obtains a sequence of orbits and their corresponding transversals . These orbits are called the fundamental orbits of . Whenever this article mentions the stabilizer chain of group or a base and strong generating set, the corresponding fundamental orbits and transversals are assumed to have been obtained as well.
Data structure
This article provides a series of pseudocode listings. In the pseudocode, the stabilizer chain (or BSGS) is stored in a data structure :
The members of this structure are the generating set of the current group, the fundamental orbit , the corresponding transversal , and the stabilizer stored as a nested substructure. The stabilizer is of course another structure of the same kind. The whole group is stored as a layered structure, with each layer describing a group in the stabilizer chain. The innermost layer is an empty structure representing .
In the pseudocode, the members can be accessed as , , , and , respectively. The first element of the orbit is by default the base point , and the corresponding coset representative is by default the identity transformation . Although array indices are used here to access elements of the orbit and transversal, they need not actually be stored as arrays; they should be understood as providing a way to access the first orbit element and query the corresponding coset representative by an orbit point. Specific implementation details are discussed below.
Applications
Once a base and strong generating set has been obtained, a series of queries about the group can be answered. The most basic ones, and the ones most often encountered in algorithm contests, are finding the order of the group and deciding whether a permutation belongs to a given group.
Order of a group
If a base and strong generating set for group is known, Lagrange's theorem and the orbit-stabilizer theorem show that the order of group can be computed as
Therefore, multiplying the sizes of all transversals (or equivalently, the lengths of all fundamental orbits ) gives the order of .
Membership testing
Given a base and strong generating set for , we can also decide whether a permutation belongs to . This is the membership testing problem.
The problem can be solved recursively. To decide whether , first find, in , the representative of the coset containing , namely . If it exists, set . Then , and is equivalent to ; the problem is reduced to testing . If no such exists, or if the recursion has reached with , then . In fact, this process not only decides whether , but when it also expresses as a product of transversal representatives, where . Such a representation exists and is unique for elements of . This again proves the formula for the order above.
The process can be written as the following pseudocode:
As we will see below, membership testing is also an important component of the implementation of the Schreier–Sims algorithm discussed in this article.
Computing orbits, transversals, and stabilizers
To implement Schreier–Sims, we first need to solve the following subproblem:3
- Given a group and its generating set , and a point , how do we find the orbit , the corresponding transversal , and a generating set for the stabilizer ?
This is the problem addressed in this section.
Storing orbits and transversals
The orbit and transversal can be obtained by direct search. The pseudocode is:
In an implementation, either breadth-first search or depth-first search works. The number of states visited is , so the time complexity is acceptable as long as the orbit and transversal are stored reasonably.
In the context of permutation groups, an orbit is simply a set of at most points. A Boolean array or an unordered set can be used for efficient lookup and insertion. Both operations take time and the total space is . Depending on the implementation of the transversal, an additional mark for the first element may be needed.
The question is what data structure should store the transversal .
Direct storage
The simplest method is to store every element in the transversal , denoted , directly. A single permutation is stored in one-line notation, requiring exactly space, so the space complexity of storing the transversal is . The advantage is access to an individual representative, at the cost of time to compute all representatives initially.
Schreier tree
Another common method is to use a tree structure to store the transversal. It is called a Schreier tree or Schreier vector4. The root is , and the vertices are the elements of the orbit . Whenever a new vertex reached from , namely , is found during the search, an edge is added from the old vertex to the new vertex , and the index (or pointer) of 's permutation is stored on the edge. Since the generating set is already stored, the additional space for the transversal is . For large , this can save considerable space and the initial computation takes time. The drawback is that obtaining a representative requires traversing the path from the vertex to the root and recomputing the representative, so the time complexity depends heavily on the depth of the Schreier tree. In general, the depth may reach .
In practice, the time-space tradeoff should be chosen according to the situation. In algorithm contests, is usually not large and space is sufficient, while time is often the bottleneck. As we will see, the most time-consuming step of Schreier–Sims needs to access coset representatives repeatedly, so direct storage is often better. In some applications, however, may be large and memory may be tight, making Schreier trees useful. To avoid excessive depth, researchers have proposed many methods for rebuilding a shallow Schreier tree when the depth becomes too large. Interested readers can consult the references at the end.
In the pseudocode, we do not distinguish between implementations of the transversal. We only assume that the data structure storing the transversal supports accessing and modifying, for each orbit element , the corresponding representative .
Schreier's lemma
Once the orbit and transversal have been obtained, Schreier's lemma provides a way to obtain a generating set for the stabilizer .
Schreier's lemma
Let have a subgroup . Let be a (right) transversal of subgroup , with 5, and, for , denote the representative of its coset by . Then
is a generating set for subgroup . Its elements are called Schreier generators of subgroup .
Proof
By the definition of a coset representative, for all , so .
Conversely, for any , since generates , there is a sequence such that
Let and recursively define . Then
For every , we have , so . But , so . Thus every is a product of elements , and generates .
Since the subgroup corresponding to is the stabilizer , after finding , combining the generating set of , namely , yields a generating set for .
Algorithm
A small modification of the pseudocode above computes the orbit, transversal, and generating set of the stabilizer at the same time:
For each pair in the pseudocode, is a new Schreier generator only when it does not produce a new orbit element; otherwise it is the identity transformation . Therefore, the algorithm generates at most
Schreier generators, including the initial identity transformation. For general cases this bound is tight.6 For actual finite groups, however, the bound is quite loose: most newly obtained Schreier generators are duplicates of earlier generators or can be composed from them.
Since the basic Schreier–Sims procedure can be implemented by recursively calling the orbit-and-stabilizer algorithm above, we already have a naive implementation. Without filtering, however, the number of Schreier generators grows exponentially: repeatedly applying shows that the innermost stabilizer generating set can reach . This is absurdly inefficient, since the innermost stabilizer is .
Sims provided a method to restrict the growth of Schreier generators, guaranteeing that the final strong generating set has size . This makes it possible to compute a base and strong generating set in polynomial time.
The Schreier–Sims algorithm
To solve the problem above, this section discusses an incremental implementation of Schreier–Sims, whose strong generating set has size .
Sifting
The core optimization over the naive algorithm is simple: before adding any Schreier generator to the stabilizer's generating set, the algorithm first performs sifting. Sifting means checking whether a new Schreier generator already belongs to the subgroup generated by the existing generators, and adding only generators that do not yet belong to it. The membership algorithm introduced above is enough.
However, this requires that a stabilizer chain (or BSGS) for the group generated by the Schreier generators obtained so far has already been built. Thus, whenever a new Schreier generator is added to a stabilizer's generating set, the inner stabilizer chain must be maintained dynamically for later sifting. Yet inserting one generator at the current level may produce several Schreier generators, causing several updates to the inner structure; each update of the inner structure may in turn trigger many updates deeper inside.
It may seem that the exponential explosion remains. It does not. Because sifting is performed first, a level is updated only when the generator being added truly enlarges the group stored at that level. Therefore, the number of updates at one level equals the number of strict growths of its stored group. A group of size has a subgroup chain of length at most , because Lagrange's theorem ensures that the group size at least doubles with every increase in chain length. Thus, each level is updated at most times, and the final strong generating set has size .
This estimate can be improved. The group here is known to be a subgroup of the -th symmetric group , so the subgroup chain length of is no greater than that of . It can be proved7 that the length of a strictly increasing subgroup chain in is at most . Hence, each level is actually updated times. The base size is also at most , so the final strong generating set has size .
The sifting method described here was proposed by Sims and is also called the Sims filter. A more complicated method proposed by Jerrum, called the Jerrum filter, guarantees a strong generating set of size . Interested readers can study it independently.
One small optimization is to have return the final “siftee”8 rather than a Boolean value (that is, use instead of lines and of the pseudocode). If the siftee , membership testing has failed, and we can add the siftee itself rather than the original to the current level. This siftee has already had several transversal factors removed, so fewer elements need to be moved and the local computation is reduced. This optimization has no effect on the overall complexity.
Procedure
We can now describe the Schreier–Sims procedure. First initialize an empty structure to store the stabilizer chain. Then add the generators to structure from one by one. The final structure is the stabilizer chain of . The pseudocode is:
The core is adding a new generator to the current structure , namely , through the subroutine . Before and after adding , must remain a stabilizer chain. Thus, before adding , we first sift it. If is not already in the group, compute the orbit, transversal, and Schreier generators incrementally. “Incrementally” means that computations already performed are not repeated, which is essential for the intended complexity.
Consider how to turn into an incremental version. The search state space is . After adding a new generator , the state space becomes . Their difference is
Therefore, when adding the new generator , first compute combinations of the new generator with the old orbit and corresponding transversal. If new orbit elements are found, then consider those elements with all generators, old and new. Repeat until the orbit no longer grows.
The pseudocode for adding a permutation to structure , namely , is:
This gives the complete Schreier–Sims algorithm.
Another implementation
The implementation above is correct, but lines and are somewhat repetitive. Knuth proposed a more concise recursive implementation in his paper. He treats the repeated part as updating the transversal (and orbit). Each update combines the transversal with all generators; depending on whether a new representative is produced, it recursively calls itself or the generator-adding procedure. The pseudocode is:
Comparing this pseudocode with the previous section shows that it is correct, and the two implementations have the same complexity.
Complexity
We need some notation to analyze the complexity. Let the length of a permutation be and the size of the generating set be . Let the length of the resulting nonredundant base be . In the -th stabilizer from the outside in, let the number of generators be , and let the size of the transversal (or the orbit length) be . We now analyze the time complexity of the incremental implementation above. The algorithm has two main parts: sifting, and computing orbits, transversals, and Schreier generators.
The input generators and the Schreier generators obtained during the algorithm must all be sifted, so the total number of sifting operations is . A single sifting operation computes products with transversal representatives. Let be the time to multiply by one representative. With direct storage, ; with a Schreier tree, . Thus, the total time for sifting is .
For orbit-related information, the incremental implementation computes each pair in the state space at most once. Depending on the storage method, computing a transversal representative takes either or time, but in either case this does not exceed the cost of computing a Schreier generator. With direct storage this is ; with a Schreier tree it is . Comparing this with the total complexity of sifting shows that computing this information does not exceed the time spent sifting. Therefore, the time complexity of Schreier–Sims is the obtained above.
For space complexity, the final data structure stores generators and transversal representatives. With direct storage, both generators and representatives require space; with a Schreier tree, generators require space and representatives require only space.
As stated earlier, the length of a strictly increasing subgroup chain in the symmetric group on points is , and this applies to both and . Therefore, an implementation using direct storage has time complexity and space complexity . When , a better estimate is time complexity9 and space complexity . For random generating sets, experiments show that the actual complexity is significantly below , approximately .
Although a Schreier tree introduces an additional factor of in the time complexity compared with direct storage, when is large and the group itself is much smaller than the symmetric group on points, its space complexity is , far smaller than the of direct storage. Such cases where Schreier-tree storage is better are rare in algorithm contests.
Reference implementation
Here is a reference implementation of Schreier–Sims. Since is small, the implementation directly fixes the base as instead of choosing it algorithmically. This makes the permutations in the -th layer from the inside out (excluding the empty structure) change only the first elements, simplifying later computation. Another optimization is that the stored transversal representatives are actually their inverse permutations, which simplifies permutation operations.
Reference implementation
#include <iostream>
#include <vector>
// A permutation.
class Permutation {
std::vector<int> perm;
void shrink() {
int m = perm.size();
for (; m && perm[m - 1] == m - 1; --m);
perm.resize(m);
}
public:
Permutation() {}
Permutation(const std::vector<int>& vec) : perm(vec) { shrink(); }
int operator[](int i) const { return i < perm.size() ? perm[i] : i; }
bool empty() const { return perm.empty(); }
// First LHS then RHS.
Permutation operator*(const Permutation& rhs) const {
Permutation res;
res.perm.resize(std::max(perm.size(), rhs.perm.size()));
for (int i = 0; i < res.perm.size(); ++i) {
res.perm[i] = rhs[(*this)[i]];
}
res.shrink();
return res;
}
// First LHS^{-1} then RHS.
Permutation operator/(const Permutation& rhs) const {
Permutation res;
res.perm.resize(std::max(perm.size(), rhs.perm.size()));
for (int i = 0; i < res.perm.size(); ++i) {
res.perm[(*this)[i]] = rhs[i];
}
res.shrink();
return res;
}
// Inverse.
Permutation inv() const {
Permutation res;
res.perm.resize(perm.size());
for (int i = 0; i < res.perm.size(); ++i) {
res.perm[(*this)[i]] = i;
}
return res;
}
};
// A stabilizer chain (a.k.a., BSGS) for a group.
class PermutationGroup {
size_t n, k;
std::vector<bool> orbit; // Orbit of the n-th point.
std::vector<Permutation> generators; // Generators.
std::vector<Permutation> transversal; // Inverse of coset representatives.
PermutationGroup* next; // Stabilizer.
// Sift a permutation.
void sift(Permutation& h) const {
if (!n) return;
int i = h[n - 1];
if (!orbit[i]) return;
h = h * transversal[i];
next->sift(h);
}
// Add one more element into the transversal.
void extend_transversal(Permutation t) {
int i = t[n - 1];
if (!orbit[i]) {
++k;
orbit[i] = true;
transversal[i] = t.inv();
for (const auto& s : generators) {
extend_transversal(t * s);
}
} else {
next->extend(t * transversal[i]);
}
}
public:
PermutationGroup(int n)
: n(n), k(1), orbit(n), transversal(n), next(nullptr) {
if (!n) return;
// Initialize the current layer.
orbit[n - 1] = true;
next = new PermutationGroup(n - 1);
}
// Destructor.
~PermutationGroup() {
if (next) delete next;
}
// Add one more permutation into the group.
void extend(Permutation g) {
sift(g);
if (g.empty()) return;
generators.emplace_back(g);
for (int i = 0; i < n; ++i) {
if (orbit[i]) {
extend_transversal(transversal[i] / g);
}
}
}
// Check whether a permutation belongs to the group.
bool membership_test(Permutation h) const {
sift(h);
return h.empty();
}
// Return the size of the group.
long long size() const { return n ? next->size() * k : 1LL; }
};
int main() {
int n, m;
std::cin >> n >> m;
PermutationGroup group(n);
// Read permutations and insert them to the group.
std::vector<int> vec(n);
for (; m; --m) {
for (int& x : vec) {
std::cin >> x;
--x; // Index starting at 0.
}
group.extend(Permutation(vec));
}
// Output the size of the group.
std::cout << group.size();
return 0;
}
Exercises
- LOJ 177. Order of a generated subgroup
- [WC2017] Board
- Permutations
- [Grand Prix of Yekaterinburg 2015] Problem H. Heimdall
References and notes
- Schreier–Sims algorithm - Wikipedia
- Sims, Charles C, Computational methods in the study of permutation groups, Computational Problems in Abstract Algebra, pp. 169–183, Pergamon, Oxford, 1970.
- Knuth, Donald E. Efficient representation of perm groups, Combinatorica 11 (1991), no. 1, 33–43.
- Ákos Seress, Permutation Group Algorithms, Cambridge University Press
- Alexander Hulpke's Notes on Computational Group Theory
- Derek Holt's Slides on The Schreier–Sims algorithm for finite permutation groups
- Martin Jaggi, Implementations of 3 Types of the Schreier–Sims Algorithm, MAS334 - Mathematics Computing Project, 2005
- Henrik Bäärnhielm. The Schreier–Sims algorithm for matrix groups
Footnotes
-
Knuth's paper was published in 1991, but his improvement had already been widely presented at conferences in 1981. The paper was written based on his conference lecture notes. ↩
-
Do not confuse this with the Monto Carlo method. Here, a Monte Carlo algorithm means a randomized algorithm whose error probability is constant and arbitrarily small. ↩
-
Neither this problem nor the algorithms in this section require the group action to be a permutation action, so they apply to more general settings. For example, applying these algorithms to a conjugation action likewise computes orbits (conjugacy classes), transversals, and stabilizers (centralizers). ↩
-
This tree structure can be implemented using a sequence of pointers to parent vertices, so it is also called a Schreier vector. ↩
-
The condition is not necessary for Schreier's lemma to hold. ↩
-
The Nielsen–Schreier theorem states that a free group generated by generators has a subgroup of index that is a free group generated by generators. ↩
-
See Cameron, P. J., Solomon, R., & Turull, A. (1989). Chains of subgroups in symmetric groups. Journal of algebra, 127(2), 340-352. ↩
-
This is not a strict term; different English sources may call it a siftee or a sifted element. ↩
-
The upper bound in Knuth's paper has one fewer logarithmic factor; obtaining it requires a more careful estimate of the lengths of the fundamental orbits in the stabilizer chain. ↩