Schreier–Sims 算法 是计算群论(computational group theory)的一种算法,以数学家 Otto Schreier 和 Charles Sims 的名字命名。该算法能够在多项式时间内解决诸如找到有限置换群的阶数、查看给定置换是否包含在所给群中等许多问题。Schreier–Sims 算法最早由 Sims 在 1970 年基于 Schreier 引理引入。在 1981 年 1,Donald Knuth 进一步改进了该算法的运行时间。后来,该算法又发展出来一种更快的随机化版本。计算机代数系统(例如 GAP 和 Magma)通常使用该算法的高度优化过的 Monte Carlo 版本 2。
记号
本文依照计算群论文献的惯例,将群作用记作右作用,这意味着置换的复合由左向右进行。本文涉及的群作用都可以视为置换作用,尽管部分算法对于更广泛的群作用也成立。相应地,群作用的集合默认为 X={1,2,⋯,n},其中的元素则称为点。置换 g 作用在点 x 上得到的结果记作 xg,有时也称置换 g 将点 x 移动到点 xg。最后,置换群 G 作用下,点 x 的轨道记作 xG={xg:g∈G},它的稳定化子则记作 Gx={g∈G:xg=x}。稳定化子的概念还可以推广到集合 B⊆X,它的稳定化子定义为 GB=⋂x∈BGx。
概述
Schreier–Sims 算法主要试图解决这样一个问题:
给定大小为 n 的集合 X 上的一些置换组成的集合 S,如何在计算机中高效地存储由 S 生成的置换群 G=⟨S⟩,并完成一系列对该群的查询任务?
显然,这样的群 G 的规模可能很大,且远远大于集合 X 和生成集 S 的规模。比如,n 次对称群 Sn=⟨(123⋯n),(12)⟩ 的大小为 n!,但是它可以仅由两个置换生成。存储群中的每一个元素是不现实的。
类似于利用 Gauss 消元法 构建出向量空间的一组 线性基,Schreier–Sims 算法的思路是找到有限置换群 G 的一组「基」:
也是群 G 的生成集,且成立 ⟨Sˉ∩G(i)⟩=G(i)。满足这个条件的生成集称为群 G 相对于基 B 的 强生成集(strong generating set)。因而,Schreier–Sims 算法也可以看作是在计算群 G 的 基和强生成集(base and strong generating set, BSGS)。这个说法和稳定化子链的说法是等价的,本文不加以区分。
当然,算法还得到了一系列轨道 Δi=βiG(i−1) 和相应的陪集代表系 Ti。这些轨道称为群 G 的 基础轨道(fundamental orbits)。当本文提及群 G 的稳定化子链或者基和强生成集时,总是默认相应的基础轨道和陪集代表系都已经一并求出。
数据结构
本文会提供一系列伪代码。伪代码中,群的稳定化子链(或基和强生成集)存储在数据结构 C 中:
C=(S,Δ,T,C′).
这个结构中的数据成员分别是当前群的生成集 S、基础轨道 Δ、相应的陪集代表系 T 和存储为嵌套子结构的稳定化子 C′。当然,稳定化子 C′ 也是这样的一个结构。整个群实际存储在一个层状结构中,每层都描述了稳定化子链中的一个群。最内层是空的结构体,表示 G(k)={e}。
所以,只要将所有陪集代表系 Ti 的大小(或者等价地,基础轨道 Δi 的长度)乘起来就可以得到群 G 的阶数。
成员判定
已知群 G 的基和强生成集,也可以判定某个置换 h 是否属于群 G。这称为 成员判定(membership testing)问题。
这个问题可以递归地解决。要判定 h∈G(i−1),首先要找到 G(i) 的包含 h 的陪集的代表元 t∈Ti。如果能够找到,那么设 h′=ht−1,就有 h=h′t 且 h∈G(i−1) 等价于 h′∈G(i);问题就转化为判定 h′∈G(i)。如果找不到这样的 t,或者已经递归到了 G(k)={e} 但是 h=e,就可以得出结论,h∈/G。其实,这个过程不仅判定了 h∈G,而且在 h∈G 的情形下,还能够将 h 表示为一系列陪集代表元的乘积 tk⋯t2t1,其中,ti∈Ti。对于群 G 中的元素,这样的表示存在且唯一。这再次证明了上面关于群的阶数的公式是正确的。
现在将该过程写成如下伪代码:
Algorithm MembershipTest(C,h):Input. A stabilizer chain C for a group G and a permutation h.Output. Whether h∈G.Method.123456789101112while C is not empty β←C.orbit[0]δ←βhif δ∈C.orbit thent←C.transversal[δ]h←ht−1else return falseend ifC←C.nextend whilereturn h=e
给定群 G 的生成集 S 和一个点 β,如何求出轨道 βG、相应的陪集代表系 T 和稳定化子 Gβ 的生成集?
这就是本节要解决的问题。
轨道和陪集代表系的存储
要求得轨道和陪集代表系,只要直接搜索就好了。伪代码如下:
Algorithm OrbitTransversal(S,β):Input. A generating set S for a group G and a point β.Output. The orbit Δ=βG and the transversal T.Method.123456789101112Δ←[β]T[β]←efor δ∈Δfor s∈Sγ←δsif γ∈/Δ thenappend γ to ΔT[γ]←T[δ]⋅send ifend forend forreturn Δ,T
因为陪集代表系 T 对应的子群就是稳定化子 Gβ,所以求出陪集代表系 T 后再结合群 G 的生成集 S 就能得到稳定化子 Gβ 的生成集。
算法
只要对上面的伪代码稍作修改,就能在计算轨道和陪集代表系的同时得到相应的稳定化子的生成集:
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.123456789101112131415Δ←[β]T[β]←eS′←[e]for δ∈Δfor s∈Sγ←δsif γ∈/Δ thenappend γ to ΔT[γ]←T[δ]⋅selseappend T[δ]⋅s⋅T[γ]−1 to S′end ifend forend forreturn Δ,T,S′
对于筛选过程,有一个小优化是,在 MembershipTest(C,h) 的实现中,并不输出布尔值,而是输出最后得到的「筛渣」8h(即用 return h 代替伪代码中的第 10 和第 14 行)。如果「筛渣」h=e,就说明成员判定失败,此时可以直接将「筛渣」h 而不是原来的 h 添加到当前层。此处的「筛渣」h 已经除去了若干个陪集代表元的因子,因而移动了更少的元素,所以会减少局部的计算量。这个优化对于整体的复杂度没有任何影响。
过程
现在可以描述 Schreier–Sims 算法的具体过程:首先,初始化一个空结构 C,用于存储群的稳定化子链。然后,逐个向结构 C 中添加生成集 S 中的生成元,最后得到的结构 C 就是群 ⟨S⟩ 的稳定化子链。伪代码如下:
Algorithm SchreierSims(S):Input. A generating set S for a group G.Output. The stabilizer chain C for the group G.Method.12345C←[]for s∈SC←Extend(C,s)end forreturn C
算法的核心在于向当前的 C 中添加新的生成元 s 这一步,即子程序 Extend(C,s)。正如前文所述,添加置换 s 之前和之后,都需要保证 C 是稳定化子链。这样,在添加置换 s 之前,可以首先做筛选。如果发现 s 不在已有的群中,就 增量地 计算轨道、陪集代表元和 Schreier 生成元。此处的「增量」的含义是,已经计算过的,不要重复计算。这样才能保证正确的复杂度。
考虑如何将 OrbitTransversalStabilizer(S,β) 改造为增量版本。算法搜索的状态空间是 Δ×S。在添加新的生成元 s 之后,状态空间将变成 Δ′×(S∪{s})。两者的差集就是
(Δ×{s})∪((Δ′∖Δ)×(S∪{s})).
这意味着,当加入新的生成元 s 的时候,首先需要计算新的生成元 s 与旧的轨道和相应的陪集代表元的组合;如果在这个过程中还得到了新的轨道的元素,就再考虑这些元素与所有生成元(无论新旧)的组合;过程重复到轨道不再延长为止。
向结构 C 中添加置换 g 的伪代码如下:
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.123456789101112131415161718192021222324252627282930313233if MembershipTest(C,g) is passed thenreturn Cend ifif C is empty thenβ←an element moved by gC.orbit[0]←βC.transversal[β]←eend ifappend g to C.generatorsΔ←C.orbitfor δ∈Δγ←δgif γ∈/C.orbit thenappend γ to C.orbitC.transversal[γ]←C.transversal[δ]⋅gelses′←C.transversal[δ]⋅g⋅C.transversal[γ]−1C.next←Extend(C.next,s′)end ifend forfor δ∈C.orbit∖Δfor s∈C.generatorsγ←δsif γ∈/C.orbit thenappend γ to C.orbitC.transversal[γ]←C.transversal[δ]⋅selses′←C.transversal[δ]⋅s⋅C.transversal[γ]−1Extend(C.next,s′)end ifend forend forreturn C
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.12345678910111213if MembershipTest(C,g) is passed thenreturn Cend ifif C is empty thenβ←an element moved by gC.orbit[0]←βC.transversal[β]←eend ifappend g to C.generatorsfor t∈C.transversalExtendTranserversal(C,t⋅g)end forreturn CSub-Algorithm ExtendTranserversal(C,t):Method.123456789101112β←C.orbit[0]γ←βtif γ∈/C.orbit thenappend γ to C.orbitC.transversal[γ]←tfor s∈C.generatorsExtendTranserversal(C,t⋅s)end forelses′←t⋅C.transversal[γ]−1Extend(C.next,s′)end if
虽然相较于直接存储,用 Schreiner 树会在时间复杂度中引入额外的 n 的指数,但对于 n 很大,但是群本身远小于 n 次对称群的情形,它的空间复杂度是 O(nlog2∣G∣) 的,远小于直接存储的 O(n2log∣G∣)。但是在算法竞赛中,很难遇到这样使用 Schreiner 树存储更优的情形。
参考实现
此处提供一个 Schreier–Sims 算法的参考实现。因为 n 规模较小,实现中直接指定基 B={n,n−1,⋯,1} 而不是通过算法选择它。这样做的好处是,自内向外第 k 层(不计空结构体)的群中的置换只就会改变前 k 个元素,方便后续计算。代码中的另一项优化是,在存储陪集代表元的时候,存储的实际上是它的逆置换,这简化了置换的运算。
参考实现
#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;}