Schreier–Sims algorithm

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 X={1,2,⋯ ,n}X=\{1,2,\cdots,n\}, whose elements are called points. The result of permutation gg acting on point xx is denoted by xgx^g; we also say that permutation gg moves point xx to xgx^g. Under the action of permutation group GG, the orbit of point xx is denoted by xG={xg:g∈G}x^G=\{x^g:g\in G\}, and its stabilizer is denoted by Gx={g∈G:xg=x}G_x=\{g\in G:x^g=x\}. The notion of a stabilizer also extends to a set B⊆XB\subseteq X: its stabilizer is defined as GB=⋂x∈BGxG_B=\bigcap_{x\in B}G_x.

Overview

The Schreier–Sims algorithm mainly attempts to solve the following problem:

  • Given a set of size nn, a set XX, and a set SS of permutations, how can we efficiently store the permutation group generated by SS, namely G=⟨S⟩G=\langle S\rangle, in a computer and perform a series of queries on that group?

Clearly, such a group GG may be very large, far larger than the sets XX and SS. For example, the order of the nn-th symmetric group Sn=⟨(123⋯n),(12)⟩S_n=\langle(123\cdots n),(12)\rangle is n!n!, 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 GG:

  1. The input is a generating set for GG, namely SS, containing several permutations.
  2. If GG is nontrivial, the action of GG always moves some point β\beta, namely ∣βG∣>1|\beta^G|>1.
  3. Find the orbit of point β\beta, Δ=βG\Delta=\beta^{G}, and for every point δ∈Δ\delta\in\Delta, find in GG a permutation tδt_{\delta} that moves β\beta to δ\delta.
  4. Find a generating set for the stabilizer of point β\beta, GβG_{\beta}, denoted S′S'.
  5. Recursively call the algorithm on G′=⟨S′⟩G'=\langle S'\rangle until the trivial group {e}\{e\} is reached.

The reason this works is that the stabilizer of point β\beta, GβG_{\beta}, is a subgroup of GG, and its right cosets partition GG. These cosets correspond one-to-one with the orbit of point β\beta, βG\beta^G. The permutations tδt_{\delta} found in step 3 are representatives of these cosets. The set TT of coset representatives is called a transversal of the stabilizer GβG_{\beta}. In other words, every element of GG, denoted gg, corresponds to a unique pair (h,t)∈Gβ×T(h,t)\in G_\beta\times T, with g=htg=ht. Therefore, storing the subgroup GβG_{\beta} together with its transversal TT is enough to store all of GG. The problem of storing the subgroup GβG_{\beta} 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 G={S}G=\{S\} a total of kk times. At the ii-th call, the input is a set S(i−1)S^{(i-1)} of permutations, the point found is βi\beta_i, the transversal obtained is TiT_i, and the generating set of the stabilizer is S(i)S^{(i)}. Let G(i)=⟨S(i)⟩G^{(i)}=\langle S^{(i)}\rangle. The algorithm obtains the subgroup chain

G=G(0)>G(1)>⋯>G(k−1)>G(k)={e}.G=G^{(0)}> G^{(1)}>\cdots> G^{(k-1)}> G^{(k)}=\{e\}.

Moreover, every subgroup in the chain is a stabilizer:

G(i)=Gβ1,⋯ ,βi.G^{(i)}=G_{\beta_1,\cdots,\beta_i}.

Thus, the Schreier–Sims algorithm can be viewed as computing a stabilizer chain.

Base and strong generating set

If a subset of XX, denoted BB, satisfies GB={e}G_B=\{e\}, then BB is called a base of permutation group GG. Clearly, the algorithm obtains a base B={β1,⋯ ,βk}B=\{\beta_1,\cdots,\beta_k\}. This means that the results of the permutation g∈Gg\in G acting on these points uniquely determine this permutation in GG. The base output by the algorithm also satisfies

G(i−1)=Gβ1,⋯ ,βi−1>Gβ1,⋯ ,βi−1,βi=G(i).G^{(i-1)}=G_{\beta_1,\cdots,\beta_{i-1}}>G_{\beta_1,\cdots,\beta_{i-1},\beta_i}=G^{(i)}.

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,

Sˉ=⋃i=0kSi\bar S=\bigcup_{i=0}^kS_i

is also a generating set for GG, and ⟨Sˉ∩G(i)⟩=G(i)\langle\bar S\cap G^{(i)}\rangle = G^{(i)} holds. A generating set satisfying this condition is called a strong generating set of GG relative to the base BB. Thus, the Schreier–Sims algorithm can also be viewed as computing a base and strong generating set (BSGS) for group GG. These two descriptions are equivalent, and this article does not distinguish between them.

The algorithm also obtains a sequence of orbits Δi=βiG(i−1)\Delta_i=\beta_{i}^{G^{(i-1)}} and their corresponding transversals TiT_i. These orbits are called the fundamental orbits of GG. Whenever this article mentions the stabilizer chain of group GG 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 CC:

C=(S,Δ,T,C′).C=(S,\Delta,T,C').

The members of this structure are the generating set SS of the current group, the fundamental orbit Δ\Delta, the corresponding transversal TT, and the stabilizer C′C' stored as a nested substructure. The stabilizer C′C' 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 G(k)={e}G^{(k)}=\{e\}.

In the pseudocode, the members can be accessed as C.generatorsC.generators, C.orbitC.orbit, C.transversalC.transversal, and C.nextC.next, respectively. The first element C.orbit[0]C.orbit[0] of the orbit is by default the base point β\beta, and the corresponding coset representative C.transversal[β]C.transversal[\beta] is by default the identity transformation ee. 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 GG is known, Lagrange's theorem and the orbit-stabilizer theorem show that the order of group GG can be computed as

∣G∣=∏i=1k[G(i−1):G(i)]=∏i=1k[G(i−1):Gβi(i−1)]=∏i=1k∣Ti∣.|G|=\prod_{i=1}^k[G^{(i-1)}:G^{(i)}]=\prod_{i=1}^k[G^{(i-1)}:G^{(i-1)}_{\beta_i}]=\prod_{i=1}^k|T_i|.

Therefore, multiplying the sizes of all transversals TiT_i (or equivalently, the lengths of all fundamental orbits Δi\Delta_i) gives the order of GG.

Membership testing

Given a base and strong generating set for GG, we can also decide whether a permutation hh belongs to GG. This is the membership testing problem.

The problem can be solved recursively. To decide whether h∈G(i−1)h\in G^{(i-1)}, first find, in G(i)G^{(i)}, the representative of the coset containing hh, namely t∈Tit\in T_i. If it exists, set h′=ht−1h'=ht^{-1}. Then h=h′th=h't, and h∈G(i−1)h\in G^{(i-1)} is equivalent to h′∈G(i)h'\in G^{(i)}; the problem is reduced to testing h′∈G(i)h'\in G^{(i)}. If no such tt exists, or if the recursion has reached G(k)={e}G^{(k)}=\{e\} with h≠eh\neq e, then h∉Gh\notin G. In fact, this process not only decides whether h∈Gh\in G, but when h∈Gh\in G it also expresses hh as a product tk⋯t2t1t_k\cdots t_2t_1 of transversal representatives, where ti∈Tit_i\in T_i. Such a representation exists and is unique for elements of GG. This again proves the formula for the order above.

The process can be written as the following pseudocode:

Algorithm MembershipTest(C,h):Input. A stabilizer chain C for a group G and a permutation h.Output. Whether h∈G.Method.1while C is not empty 2β←C.orbit[0]3δ←βh4if δ∈C.orbit then5t←C.transversal[δ]6h←ht−17else 8return false9end if10C←C.next11end while12return h=e\begin{array}{l} \textbf{Algorithm }\textrm{MembershipTest}(C,h):\\ \textbf{Input. }\textrm{A stabilizer chain }C\textrm{ for a group }G\textrm{ and a permutation }h.\\ \textbf{Output. }\textrm{Whether }h\in G.\\ \textbf{Method.}\\ \begin{array}{ll} 1 & \textbf{while }C\textrm{ is not empty }\\ 2 & \qquad \beta \leftarrow C.orbit[0]\\ 3 & \qquad \delta \leftarrow \beta^h\\ 4 & \qquad \textbf{if }\delta\in C.orbit\textbf{ then}\\ 5 & \qquad \qquad t \leftarrow C.transversal[\delta]\\ 6 & \qquad \qquad h \leftarrow ht^{-1}\\ 7 & \qquad \textbf{else }\\ 8 & \qquad \qquad \textbf{return }\textrm{false}\\ 9 & \qquad \textbf{end if}\\ 10 & \qquad C \leftarrow C.next\\ 11 & \textbf{end while}\\ 12 & \textbf{return }h=e \end{array} \end{array}

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 GG and its generating set SS, and a point β\beta, how do we find the orbit βG\beta^G, the corresponding transversal TT, and a generating set for the stabilizer GβG_\beta?

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:

Algorithm OrbitTransversal(S,β):Input. A generating set S for a group G and a point β.Output. The orbit Δ=βG and the transversal T.Method.1Δ←[β]2T[β]←e3for δ∈Δ4for s∈S5γ←δs6if γ∉Δ then7append γ to Δ8T[γ]←T[δ]⋅s9end if10end for11end for12return Δ,T\begin{array}{l} \textbf{Algorithm }\textrm{OrbitTransversal}(S,\beta):\\ \textbf{Input. }\textrm{A generating set }S\textrm{ for a group }G\textrm{ and a point }\beta.\\ \textbf{Output. }\textrm{The orbit }\Delta=\beta^G\textrm{ and the transversal }T.\\ \textbf{Method.}\\ \begin{array}{ll} 1 & \Delta \leftarrow [\beta]\\ 2 & T[\beta] \leftarrow e\\ 3 & \textbf{for }\delta\in\Delta\\ 4 & \qquad \textbf{for }s\in S\\ 5 & \qquad \qquad \gamma \leftarrow \delta^s\\ 6 & \qquad \qquad \textbf{if }\gamma\notin\Delta\textbf{ then}\\ 7 & \qquad \qquad \qquad\textrm{append }\gamma\textrm{ to }\Delta\\ 8 & \qquad \qquad \qquad T[\gamma] \leftarrow T[\delta]\cdot s\\ 9 & \qquad \qquad \textbf{end if}\\ 10 & \qquad \textbf{end for}\\ 11 & \textbf{end for}\\ 12 & \textbf{return }\Delta, T \end{array} \end{array}

In an implementation, either breadth-first search or depth-first search works. The number of states visited is ∣S∣∣T∣|S||T|, 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 nn points. A Boolean array or an unordered set can be used for efficient lookup and insertion. Both operations take O(1)O(1) time and the total space is O(n)O(n). 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 TT.

Direct storage

The simplest method is to store every element in the transversal TT, denoted tt, directly. A single permutation is stored in one-line notation, requiring exactly nn space, so the space complexity of storing the transversal is O(∣T∣n)O(|T|n). The advantage is O(1)O(1) access to an individual representative, at the cost of O(∣T∣n)O(|T|n) 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 β\beta, and the vertices are the elements of the orbit Δ\Delta. Whenever a new vertex reached from δ\delta, namely γ=δs\gamma=\delta^s, is found during the search, an edge is added from the old vertex δ\delta to the new vertex γ\gamma, and the index (or pointer) of SS's permutation ss is stored on the edge. Since the generating set is already stored, the additional space for the transversal is O(∣T∣)O(|T|). For large nn, this can save considerable space and the initial computation takes O(∣T∣)O(|T|) 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 O(n)O(n).

In practice, the time-space tradeoff should be chosen according to the situation. In algorithm contests, nn 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, nn 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 TT supports accessing and modifying, for each orbit element δ∈Δ\delta\in\Delta, the corresponding representative T[δ]T[\delta].

Schreier's lemma

Once the orbit βG\beta^G and transversal TT have been obtained, Schreier's lemma provides a way to obtain a generating set for the stabilizer GβG_\beta.

Schreier's lemma

Let G=⟨S⟩G=\langle S\rangle have a subgroup H≤GH\le G. Let TT be a (right) transversal of subgroup HH, with e∈Te\in T5, and, for g∈Gg\in G, denote the representative t∈Tt\in T of its coset by g‾\overline g. Then

U={ts(ts‾)−1:t∈T,s∈S}U=\{ts(\overline{ts})^{-1}:t\in T,s\in S\}

is a generating set for subgroup HH. Its elements are called Schreier generators of subgroup HH.

Proof

By the definition of a coset representative, ts(ts‾)−1∈Hts(\overline{ts})^{-1}\in H for all t∈T,s∈St\in T,s\in S, so ⟨U⟩⊆H\langle U\rangle\subseteq H.

Conversely, for any h∈Hh\in H, since SS generates G≥HG\ge H, there is a sequence si∈S∪S−1s_i\in S\cup S^{-1} such that

h=s1s2⋯srh=s_1s_2\cdots s_r

Let t1=et_1=e and recursively define ti+1=siti‾∈Tt_{i+1}=\overline{s_it_i}\in T. Then

h=(t1s1t2−1)(t2s2t3−1)⋯(trsrtr+1)−1tr+1.\begin{aligned} h&=\left(t_1s_1t_2^{-1}\right)\left(t_2s_2t_3^{-1}\right)\cdots(t_rs_rt_{r+1})^{-1}t_{r+1}. \end{aligned}

For every i=1,2,⋯ ,ri=1,2,\cdots,r, we have tisiti+1−1=tisi(tisi‾)−1∈U∪U−1⊆Ht_is_it_{i+1}^{-1}=t_is_i(\overline{t_is_i})^{-1}\in U\cup U^{-1}\subseteq H, so tr+1∈Ht_{r+1}\in H. But H∩T={e}H\cap T=\{e\}, so tr+1=et_{r+1}=e. Thus every h∈Hh\in H is a product of elements ui=tisiti+1−1∈U∪U−1u_i=t_is_it_{i+1}^{-1}\in U\cup U^{-1}, and UU generates HH.

Since the subgroup corresponding to TT is the stabilizer GβG_\beta, after finding TT, combining the generating set of GG, namely SS, yields a generating set for GβG_\beta.

Algorithm

A small modification of the pseudocode above computes the orbit, transversal, and generating set of the stabilizer at the same time:

Algorithm OrbitTransversalStabilizer(S,β):Input. A generating set S for a group G and a point β.Output. The orbit Δ=βG, the transversal T, and a generating set S′ for the stabilizer Gβ.Method.1Δ←[β]2T[β]←e3S′←[e]4for δ∈Δ5for s∈S6γ←δs7if γ∉Δ then8append γ to Δ9T[γ]←T[δ]⋅s10else11append T[δ]⋅s⋅T[γ]−1 to S′12end if13end for14end for15return Δ,T,S′\begin{array}{l} \textbf{Algorithm }\textrm{OrbitTransversalStabilizer}(S,\beta):\\ \textbf{Input. }\textrm{A generating set }S\textrm{ for a group }G\textrm{ and a point }\beta.\\ \textbf{Output. }\textrm{The orbit }\Delta=\beta^G\textrm{, the transversal }T\textrm{, and a}\\ \qquad\textrm{ generating set }S'\textrm{ for the stabilizer }G_\beta.\\ \textbf{Method.}\\ \begin{array}{ll} 1 & \Delta \leftarrow [\beta]\\ 2 & T[\beta] \leftarrow e\\ 3 & S' \leftarrow [e]\\ 4 & \textbf{for }\delta\in\Delta\\ 5 & \qquad \textbf{for }s\in S\\ 6 & \qquad \qquad \gamma \leftarrow \delta^s\\ 7 & \qquad \qquad \textbf{if }\gamma\notin\Delta\textbf{ then}\\ 8 & \qquad \qquad \qquad\textrm{append }\gamma\textrm{ to }\Delta\\ 9 & \qquad \qquad \qquad T[\gamma] \leftarrow T[\delta]\cdot s\\ 10 & \qquad \qquad \textbf{else} \\ 11 & \qquad \qquad \qquad \textrm{append }T[\delta]\cdot s\cdot T[\gamma]^{-1}\textrm{ to }S'\\ 12 & \qquad \qquad \textbf{end if}\\ 13 & \qquad \textbf{end for}\\ 14 & \textbf{end for}\\ 15 & \textbf{return }\Delta, T, S' \end{array} \end{array}

For each pair (δ,s)(\delta,s) in the pseudocode, tδstδs−1t_\delta st_{\delta^s}^{-1} is a new Schreier generator only when it does not produce a new orbit element; otherwise it is the identity transformation ee. Therefore, the algorithm generates at most

∣S∣∣T∣−(∣T∣−1)=∣S∣(∣T∣−1)+1|S||T|-(|T|-1) = |S|(|T|-1)+1

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 O(∣Si∣)=O(∣Si−1∣∣Ti∣)O(|S_{i}|)=O(|S_{i-1}||T_i|) shows that the innermost stabilizer generating set can reach O(∣S∣∣G∣)O(|S||G|). This is absurdly inefficient, since the innermost stabilizer is {e}\{e\}.

Sims provided a method to restrict the growth of Schreier generators, guaranteeing that the final strong generating set Sˉ\bar S has size O(n2)O(n^2). 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 O(n2)O(n^2).

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 MembershipTest(C,h)\textrm{MembershipTest}(C,h) 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 ∣G∣|G| has a subgroup chain of length at most log⁡∣G∣\log|G|, 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 log⁡∣G∣\log|G| times, and the final strong generating set Sˉ\bar S has size ∣B∣log⁡∣G∣|B|\log|G|.

This estimate can be improved. The group GG here is known to be a subgroup of the nn-th symmetric group SnS_n, so the subgroup chain length of GG is no greater than that of SnS_n. It can be proved7 that the length of a strictly increasing subgroup chain in SnS_n is at most 3n/23n/2. Hence, each level is actually updated O(n)O(n) times. The base size is also at most nn, so the final strong generating set Sˉ\bar S has size O(n2)O(n^2).

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 O(n)O(n). Interested readers can study it independently.

One small optimization is to have MembershipTest(C,h)\textrm{MembershipTest}(C,h) return the final “siftee”8 hh rather than a Boolean value (that is, use return h\textbf{return }h instead of lines 1010 and 1414 of the pseudocode). If the siftee h≠eh\neq e, membership testing has failed, and we can add the siftee hh itself rather than the original hh to the current level. This siftee hh 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 CC to store the stabilizer chain. Then add the generators to structure CC from SS one by one. The final structure CC is the stabilizer chain of ⟨S⟩\langle S\rangle. The pseudocode is:

Algorithm SchreierSims(S):Input. A generating set S for a group G.Output. The stabilizer chain C for the group G.Method.1C←[]2for s∈S3C←Extend(C,s)4end for5return C\begin{array}{l} \textbf{Algorithm }\textrm{SchreierSims}(S):\\ \textbf{Input. }\textrm{A generating set }S\textrm{ for a group }G.\\ \textbf{Output. }\textrm{The stabilizer chain }C\textrm{ for the group }G.\\ \textbf{Method.}\\ \begin{array}{ll} 1 & C \leftarrow []\\ 2 & \textbf{for }s\in S\\ 3 & \qquad C \leftarrow \textrm{Extend}(C,s)\\ 4 & \textbf{end for}\\ 5 & \textbf{return }C \end{array} \end{array}

The core is adding a new generator to the current structure CC, namely ss, through the subroutine Extend(C,s)\textrm{Extend}(C,s). Before and after adding ss, CC must remain a stabilizer chain. Thus, before adding ss, we first sift it. If ss 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 OrbitTransversalStabilizer(S,β)\textrm{OrbitTransversalStabilizer}(S,\beta) into an incremental version. The search state space is Δ×S\Delta\times S. After adding a new generator ss, the state space becomes Δ′×(S∪{s})\Delta'\times\left(S\cup\{s\}\right). Their difference is

(Δ×{s})∪((Δ′∖Δ)×(S∪{s})).\left(\Delta\times\{s\}\right)\cup\left((\Delta'\setminus\Delta)\times \left(S\cup\{s\}\right)\right).

Therefore, when adding the new generator ss, first compute combinations of the new generator ss 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 CC, namely gg, is:

Algorithm Extend(C,g):Input. A stabilizer chain C for the group generated by S and apermutation g.Output. A stabilizer chain C for the group generated by S∪{g}.Method.1if MembershipTest(C,g) is passed then2return C3end if4if C is empty then5β←an element moved by g6C.orbit[0]←β7C.transversal[β]←e8end if9append g to C.generators10Δ←C.orbit11for δ∈Δ12γ←δg13if γ∉C.orbit then14append γ to C.orbit15C.transversal[γ]←C.transversal[δ]⋅g16else17s′←C.transversal[δ]⋅g⋅C.transversal[γ]−118C.next←Extend(C.next,s′)19end if20end for21for δ∈C.orbit∖Δ22for s∈C.generators23γ←δs24if γ∉C.orbit then25append γ to C.orbit26C.transversal[γ]←C.transversal[δ]⋅s27else28s′←C.transversal[δ]⋅s⋅C.transversal[γ]−129Extend(C.next,s′)30end if31end for32end for33return C\begin{array}{l} \textbf{Algorithm }\textrm{Extend}(C,g):\\ \textbf{Input. }\textrm{A stabilizer chain }C\textrm{ for the group generated by }S\textrm{ and a}\\ \qquad \textrm{permutation }g.\\ \textbf{Output. }\textrm{A stabilizer chain }C\textrm{ for the group generated by }S\cup\{g\}.\\ \textbf{Method.}\\ \begin{array}{ll} 1 & \textbf{if }\textrm{MembershipTest}(C,g)\textrm{ is passed}\textbf{ then}\\ 2 & \qquad \textbf{return }C\\ 3 & \textbf{end if}\\ 4 & \textbf{if }C\textrm{ is empty}\textbf{ then}\\ 5 & \qquad \beta \leftarrow \textrm{an element moved by }g\\ 6 & \qquad C.orbit[0] \leftarrow \beta \\ 7 & \qquad C.transversal[\beta] \leftarrow e\\ 8 & \textbf{end if}\\ 9 & \textrm{append }g\textrm{ to }C.generators\\ 10 & \Delta \leftarrow C.orbit \\ 11 & \textbf{for }\delta\in\Delta \\ 12 & \qquad \gamma \leftarrow \delta^g \\ 13 & \qquad \textbf{if }\gamma\notin C.orbit\textbf{ then}\\ 14 & \qquad \qquad \textrm{append }\gamma\textrm{ to }C.orbit\\ 15 & \qquad \qquad C.transversal[\gamma] \leftarrow C.transversal[\delta]\cdot g\\ 16 & \qquad \textbf{else}\\ 17 & \qquad \qquad s'\leftarrow C.transversal[\delta]\cdot g\cdot C.transversal[\gamma]^{-1}\\ 18 & \qquad \qquad C.next \leftarrow \textrm{Extend}(C.next, s')\\ 19 & \qquad \textbf{end if}\\ 20 & \textbf{end for} \\ 21 & \textbf{for }\delta\in C.orbit\setminus\Delta \\ 22 & \qquad \textbf{for }s\in C.generators \\ 23 & \qquad \qquad \gamma \leftarrow \delta^s \\ 24 & \qquad \qquad \textbf{if }\gamma\notin C.orbit\textbf{ then}\\ 25 & \qquad \qquad \qquad \textrm{append }\gamma\textrm{ to }C.orbit\\ 26 & \qquad \qquad \qquad C.transversal[\gamma] \leftarrow C.transversal[\delta]\cdot s\\ 27 & \qquad \qquad \textbf{else}\\ 28 & \qquad \qquad \qquad s'\leftarrow C.transversal[\delta]\cdot s\cdot C.transversal[\gamma]^{-1}\\ 29 & \qquad \qquad \qquad \textrm{Extend}(C.next, s')\\ 30 & \qquad \qquad \textbf{end if}\\ 31 & \qquad \textbf{end for}\\ 32 & \textbf{end for} \\ 33 & \textbf{return }C \end{array} \end{array}

This gives the complete Schreier–Sims algorithm.

Another implementation

The implementation above is correct, but lines 12∼1912\sim 19 and 23∼3023\sim 30 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:

Algorithm Extend(C,g):Input. A stabilizer chain C for the group generated by S and apermutation g.Output. A stabilizer chain C for the group generated by S∪{g}.Method.1if MembershipTest(C,g) is passed then2return C3end if4if C is empty then5β←an element moved by g6C.orbit[0]←β7C.transversal[β]←e8end if9append g to C.generators10for t∈C.transversal11ExtendTranserversal(C,t⋅g)12end for13return CSub-Algorithm ExtendTranserversal(C,t):Method.1β←C.orbit[0]2γ←βt3if γ∉C.orbit then4append γ to C.orbit5C.transversal[γ]←t6for s∈C.generators7ExtendTranserversal(C,t⋅s)8end for9else10s′←t⋅C.transversal[γ]−111Extend(C.next,s′)12end if\begin{array}{l} \textbf{Algorithm }\textrm{Extend}(C,g):\\ \textbf{Input. }\textrm{A stabilizer chain }C\textrm{ for the group generated by }S\textrm{ and a}\\ \qquad \textrm{permutation }g.\\ \textbf{Output. }\textrm{A stabilizer chain }C\textrm{ for the group generated by }S\cup\{g\}.\\ \textbf{Method.}\\ \begin{array}{ll} 1 & \textbf{if }\textrm{MembershipTest}(C,g)\textrm{ is passed}\textbf{ then}\\ 2 & \qquad \textbf{return }C\\ 3 & \textbf{end if}\\ 4 & \textbf{if }C\textrm{ is empty}\textbf{ then}\\ 5 & \qquad \beta \leftarrow \textrm{an element moved by }g\\ 6 & \qquad C.orbit[0] \leftarrow \beta \\ 7 & \qquad C.transversal[\beta] \leftarrow e\\ 8 & \textbf{end if}\\ 9 & \textrm{append }g\textrm{ to }C.generators\\ 10 & \textbf{for }t\in C.transversal\\ 11 & \qquad \textrm{ExtendTranserversal}(C,t\cdot g)\\ 12 & \textbf{end for}\\ 13 & \textbf{return }C \end{array}\\ \\ \textbf{Sub-Algorithm }\textrm{ExtendTranserversal}(C,t):\\ \textbf{Method.}\\ \begin{array}{ll} 1 & \beta \leftarrow C.orbit[0]\\ 2 & \gamma \leftarrow \beta^t\\ 3 & \textbf{if }\gamma\notin C.orbit\textbf{ then}\\ 4 & \qquad \textrm{append }\gamma\textrm{ to }C.orbit\\ 5 & \qquad C.transversal[\gamma] \leftarrow t\\ 6 & \qquad \textbf{for }s\in C.generators\\ 7 & \qquad \qquad \textrm{ExtendTranserversal}(C,t\cdot s)\\ 8 & \qquad \textbf{end for}\\ 9 & \textbf{else}\\ 10 & \qquad s' \leftarrow t\cdot C.transversal[\gamma]^{-1}\\ 11 & \qquad \textrm{Extend}(C.next,s')\\ 12 & \textbf{end if} \end{array} \end{array}

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 nn and the size ∣S∣|S| of the generating set be mm. Let the length of the resulting nonredundant base be ∣B∣|B|. In the ii-th stabilizer GiG_i from the outside in, let the number of generators be ∣Si−1∣|S_{i-1}|, and let the size of the transversal (or the orbit length) be ∣Ti∣|T_i|. 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 O(∑i=1∣B∣∣Si−1∣∣Ti∣+∣S∣)O(\sum_{i=1}^{|B|}|S_{i-1}||T_i|+|S|). A single sifting operation computes products with O(∣B∣)O(|B|) transversal representatives. Let τ\tau be the time to multiply by one representative. With direct storage, τ∈O(n)\tau\in O(n); with a Schreier tree, τ∈O(n2)\tau\in O(n^2). Thus, the total time for sifting is O(τ∣B∣∑i=1∣B∣∣Si−1∣∣Ti∣+τ∣B∣∣S∣)O(\tau|B|\sum_{i=1}^{|B|}|S_{i-1}||T_i|+\tau|B||S|).

For orbit-related information, the incremental implementation computes each pair (δ,s)∈Δi×Si−1(\delta,s)\in\Delta_i\times S_{i-1} in the state space at most once. Depending on the storage method, computing a transversal representative takes either O(n)O(n) or O(1)O(1) time, but in either case this does not exceed the cost of computing a Schreier generator. With direct storage this is O(n)O(n); with a Schreier tree it is O(n2)O(n^2). 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 O(τ∣B∣∑i=1∣B∣∣Si−1∣∣Ti∣+τ∣B∣∣S∣)O(\tau|B|\sum_{i=1}^{|B|}|S_{i-1}||T_i|+\tau|B||S|) obtained above.

For space complexity, the final data structure stores O(∑i=1∣B∣∣Si−1∣)O(\sum_{i=1}^{|B|}|S_{i-1}|) generators and O(∑i=1∣B∣∣Ti∣)O(\sum_{i=1}^{|B|}|T_i|) transversal representatives. With direct storage, both generators and representatives require O(n)O(n) space; with a Schreier tree, generators require O(n)O(n) space and representatives require only O(1)O(1) space.

As stated earlier, the length of a strictly increasing subgroup chain in the symmetric group on nn points is O(n)O(n), and this applies to both ∣B∣|B| and ∣Si∣|S_i|. Therefore, an implementation using direct storage has time complexity O(n5+mn2)O(n^5+mn^2) and space complexity O(n3)O(n^3). When log⁡∣G∣∈O(n)\log|G|\in O(n), a better estimate is time complexity9 O(n2log⁡3∣G∣+mnlog⁡∣G∣)O(n^2\log^3|G|+mn\log|G|) and space complexity O(n2log⁡∣G∣)O(n^2\log|G|). For random generating sets, experiments show that the actual complexity is significantly below Θ(n5)\Theta(n^5), approximately Θ(n4)\Theta(n^4).

Although a Schreier tree introduces an additional factor of nn in the time complexity compared with direct storage, when nn is large and the group itself is much smaller than the symmetric group on nn points, its space complexity is O(nlog⁡2∣G∣)O(n\log^2|G|), far smaller than the O(n2log⁡∣G∣)O(n^2\log|G|) 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 nn is small, the implementation directly fixes the base as B={n,n−1,⋯ ,1}B=\{n,n-1,\cdots,1\} instead of choosing it algorithmically. This makes the permutations in the kk-th layer from the inside out (excluding the empty structure) change only the first kk 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

References and notes

Footnotes

  1. 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. ↩

  2. 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. ↩

  3. 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). ↩

  4. This tree structure can be implemented using a sequence of pointers to parent vertices, so it is also called a Schreier vector. ↩

  5. The condition e∈Te\in T is not necessary for Schreier's lemma to hold. ↩

  6. The Nielsen–Schreier theorem states that a free group generated by nn generators has a subgroup of index kk that is a free group generated by k(n−1)+1k(n-1)+1 generators. ↩

  7. See Cameron, P. J., Solomon, R., & Turull, A. (1989). Chains of subgroups in symmetric groups. Journal of algebra, 127(2), 340-352. ↩

  8. This is not a strict term; different English sources may call it a siftee or a sifted element. ↩

  9. 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. ↩

This content comes from OI Wiki: Schreier–Sims algorithm. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.