Schreier–Sims 算法
引入
Schreier–Sims 算法 是计算群论(computational group theory)的一种算法,以数学家 Otto Schreier 和 Charles Sims 的名字命名。该算法能够在多项式时间内解决诸如找到有限置换群的阶数、查看给定置换是否包含在所给群中等许多问题。Schreier–Sims 算法最早由 Sims 在 1970 年基于 Schreier 引理引入。在 1981 年1,Donald Knuth 进一步改进了该算法的运行时间。后来,该算法又发展出来一种更快的随机化版本。计算机代数系统(例如 GAP 和 Magma)通常使用该算法的高度优化过的 Monte Carlo 版本2。
记号
本文依照计算群论文献的惯例,将群作用记作右作用,这意味着置换的复合由左向右进行。本文涉及的群作用都可以视为置换作用,尽管部分算法对于更广泛的群作用也成立。相应地,群作用的集合默认为
概述
Schreier–Sims 算法主要试图解决这样一个问题:
- 给定大小为
的集合 上的一些置换组成的集合 ,如何在计算机中高效地存储由 生成的置换群 ,并完成一系列对该群的查询任务?
显然,这样的群
类似于利用 Gauss 消元法 构建出向量空间的一组 线性基,Schreier–Sims 算法的思路是找到有限置换群
- 算法的输入是
的一个生成集 ,其中有若干个置换; - 如果群
不平凡,总能找到在群 作用下位置会发生变化的点 ,即 ; - 找到点
的轨道 ,并对轨道中的每个点 ,都找到群 中一个置换 ,它能够将点 移动到 ; - 找到点
的稳定化子 的一个生成集 ; - 递归地对
调用该算法,直到得到平凡的群 。
这个思路的合理性在于,点
当然,算法的实现还有很多细节需要梳理,这是本文的主要内容。但是在那之前,首先要考察调用该算法之后得到的结果,看看算法将群存储为怎样的结构,能够解决怎样的查询问题。为此,应当明晰一些概念。
稳定化子链
假设对群
而且,每一个链中的子群都是一个稳定化子
因而,Schreier–Sims 算法可以看作就是在计算这样一个 稳定化子链(stabilizer chain)。
基和强生成集
如果集合
这就是说,基中的每个点都蕴含着关于群中的元素的有效信息。这样的基称为 无冗余的(nonredundant)。算法总是输出无冗余的基。这样的基对应的稳定化子链是严格递降的。
同时,算法得到的生成集的并集
也是群
当然,算法还得到了一系列轨道
数据结构
本文会提供一系列伪代码。伪代码中,群的稳定化子链(或基和强生成集)存储在数据结构
这个结构中的数据成员分别是当前群的生成集
在伪代码中,该数据结构中的成员可以分别由
应用
在获得群的基和强生成集后,能够解决一系列关于群的查询问题。其中,最基础的,也是算法竞赛中最常遇到的,是查询群的阶数和查询某个置换是否属于给定群的问题。
群的阶数
如果已知群
所以,只要将所有陪集代表系
成员判定
已知群
这个问题可以递归地解决。要判定
现在将该过程写成如下伪代码:
下文会看到,成员判定问题也是本文所讨论的 Schreier–Sims 算法的实现中的一个重要组成部分。
轨道、陪集代表系和稳定化子的计算
要实现 Schreier–Sims 算法,首先要解决如下子问题:3
- 给定群
的生成集 和一个点 ,如何求出轨道 、相应的陪集代表系 和稳定化子 的生成集?
这就是本节要解决的问题。
轨道和陪集代表系的存储
要求得轨道和陪集代表系,只要直接搜索就好了。伪代码如下:
具体实现的时候,使用广度优先搜索和深度优先搜索都是可以的。搜索遍历到的状态数目是
由于在置换群的语境下,轨道无非是至多
问题在于使用什么样的数据结构存储相应的陪集代表系
直接存储
最简单的方法,当然是直接存储陪集代表系
Schreier 树
另外一种常见的做法是实现一个树形结构用于存储陪集代表系。它称为 Schreier 树(Schreier tree)或 Schreier 向量(Schreier vector)4。它以
在具体实现的时候,需要根据实际情况权衡算法的时空复杂度。对于算法竞赛可能涉及的情形,
在伪代码中,本文不会区分具体的陪集代表系的实现,而只假设存储陪集代表系
Schreier 引理
在获得了轨道
Schreier 引理
设群
是子群
证明
首先,根据陪集代表元的定义可知,
反过来,对于任何
成立。令
而对于每个
因为陪集代表系
算法
只要对上面的伪代码稍作修改,就能在计算轨道和陪集代表系的同时得到相应的稳定化子的生成集:
伪代码中,对于每对
个。对于一般的情形,这个上界是紧的。6但是,对于实际要处理的有限群,这个上界相当地宽松:这些新得到的 Schreier 生成元大多数并都是之前得到的生成元的重复,或者可以由之前的生成元复合而成。
因为 Schreier–Sims 算法的基本流程可以实现为递归地调用上述计算轨道和稳定化子的算法,所以其实此时就已经得到了 Schreier–Sims 算法的一种朴素实现。但是,如果不加以筛选,Schreier 生成元的规模的增长速度是指数级的:反复应用
Sims 的工作提供了限制 Schreier 生成元的规模的增长速度的方法,它能够保证最终得到的强生成集
Schreier–Sims 算法
为解决上述问题,本节讨论 Schreier–Sims 算法的一种增量实现,它得到的强生成集的大小是
筛选
Schreier–Sims 算法对上述朴素算法的核心优化十分简明:它要求在向稳定化子的生成集添加任何 Schreier 生成元之前,都首先需要经过 筛选(sifting)。所谓筛选,就是首先判定新的 Schreier 生成元是否已经存在于已有的生成元生成的子群中,然后只添加那些尚不存在的生成元。为此,只需要使用前文的成员判定算法
但是,能够这样做的前提是,基于当前群的已经产生了的 Schreier 生成元,早就构建好了它们生成的群的稳定化子链(或基和强生成集)。这意味着,每次向稳定化子的生成集中添加一个新的 Schreier 生成元,都需要动态地维护内层的稳定化子链,以用于之后的筛选。但是,当前层每插入一个生成元,可能会产生多个 Schreier 生成元,也就可能会多次更新内层结构;而内层结构的每次更新,都可能会引发更内层结构的多次更新。
似乎之前提到的指数级爆炸的问题依然存在。其实不然。因为提前做好了筛选,只有待添加的生成元真的会引发某一层结构的扩大的时候,该层结构才会更新。这说明,单层结构更新的次数实际上等于该层结构存储的群严格增长的次数。但是,大小为
这个估计还可以进一步改进。因为此处出现的群
此处提到的筛选方法是 Sims 提出的,也称为 Sims 筛(Sims filter)。还有一种更为复杂的筛选方法,是由 Jerrum 提出的,也称为 Jerrum 筛(Jerrum filter),它能够保证得到的强生成集的大小是
对于筛选过程,有一个小优化是,在
过程
现在可以描述 Schreier–Sims 算法的具体过程:首先,初始化一个空结构
算法的核心在于向当前的
考虑如何将
这意味着,当加入新的生成元
向结构
这样就得到了完整的 Schreier–Sims 算法。
另一种实现
上述的实现已经是正确的,但是
将此处的伪代码和上节的相比,就可以知道它是正确的。而且,两者复杂度并无差异。
复杂度
为了分析 Schreier–Sims 算法的复杂度,需要一些记号。设置换的长度为
最初输入的生成元和算法中得到的 Schreider 生成元都需要进行筛选,因而筛选过程执行的总次数是
对于轨道等信息的计算,因为是增量实现,状态空间中的每对
至于空间复杂度,算法最后得到的数据结构中存储了
前文已经说明,
虽然相较于直接存储,用 Schreiner 树会在时间复杂度中引入额外的
参考实现
此处提供一个 Schreier–Sims 算法的参考实现。因为
参考实现
#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;
}
习题
参考资料与注释
- 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
-
Knuth 的论文是在 1991 年发表的,但是他的改进在 1981 年就通过会议广泛地宣传。论文是基于他的会议讲稿写作的。 ↩
-
不要与 Monto Carlo 方法混淆。此处的 Monte Carlo 算法是指出错概率恒定且任意小的随机算法。 ↩
-
这个问题以及本节的算法都并不需要假设所讨论的群作用是置换作用,因而可以应用于更广泛的场景。比如,如果将这些算法应用于共轭作用,同样可以求得轨道(共轭类)、陪集代表系和稳定化子(中心化子)。 ↩
-
因为这个树形结构可以通过一列指向父节点的指针来实现,所以也称作 Schreier 向量。 ↩
-
这个
的条件对于 Schreier 引理的成立不是必要的。 ↩ -
Nielsen–Schreier 定理 说明,对于由
个生成元生成的 自由群,它的指数为 的子群是由 个生成元生成的自由群。 ↩ -
参见 Cameron, P. J., Solomon, R., & Turull, A. (1989). Chains of subgroups in symmetric groups. Journal of algebra, 127(2), 340-352. ↩
-
这并不是什么严格的术语,在不同的英文文献中可能称作 siftee 或者 sifted element。 ↩
-
Knuth 的论文给出的上界还要再少一个对数因子,这需要对群的稳定化子链的基础轨道长度做更仔细的估计。 ↩
创建日期: 2024年11月27日