半群、子群、商群一条流水线:从有限群到状态合并的工程直觉 半群、子群、商群这三个词摆在一起的时候很多人第一反应是把它们当成三个并列的知识点去背定义背完发现做题还是不会更别提用在真实问题里了。我一开始也是这么干直到后来在做一个分布式状态合并的小工具时被卡住——两个节点的状态合并结果依赖合并顺序同样一批数据换个顺序跑出来的结果不一样翻资料翻到最后发现答案就藏在“半群是不是交换的、有没有单位元、商掉一个等价关系之后运算还良不良定义”这几个问题上。从那以后我就换了个学法不把它们当成三个孤立名词而是当成一条流水线——先有一个带运算的集合检查它够不够格半群再往里挑出结构完整的子系统子群最后把这些子系统当成新的“点”重新组装成一个更小的结构商群。这条线走通了群论里一大半的构造你都能自己推出来而不是靠记忆。这篇文章写给三类人正在啃抽象代数、定义看得懂但例子举不出来的学生写代码时被类型类、合并函数、模运算折磨过、想搞清底层规律的工程师以及需要通过“有限群的程序验证”来做形式化建模的人。下面所有内容都围绕这条流水线展开代码可以直接跑例子可以用手算复现。1. 先把半群、子群、商群当成一条流水线来看1.1 三个概念各自回答一个什么样的问题我习惯给这三个概念各配一个提问。半群回答的是“什么样的运算才算靠谱”子群回答的是“在一个靠谱的体系里哪些子集自己就能独立成体系”商群回答的是“如果把某些元素当成同一个东西糊在一起剩下的结构还成不成立”。这三个问题的顺序不能乱因为后一个问题的成立与否完全依赖前一个问题的答案。举个具体场景你就明白了。假设你有一批任务要并行归约求总和每个任务算一部分最后合并。你希望“先合并前一半、再合并后一半”和“按任意顺序逐个合并”得到完全一样的结果。这个诉求翻译成数学语言就是合并运算必须是结合的也就是半群如果还要求存在一个“什么都不做”的元素使得任何东西跟它合并都保持原样那就是幺半群。你根本没提逆元的事因为求总和这个场景不需要撤销操作。但如果你做的是类似“撤销上一次编辑”的功能那你就需要逆元这时候才升级成群。我在实际项目里见过太多人一上来就说“我要用一个群”其实他要的只是个幺半群。选了群就得额外交付逆元存在性的证明成本还要处理“某些元素不可逆”的尴尬。搞清楚这条流水线第一个收益就是知道自己在哪一层停手。1.2 为什么我坚持从“有限运算表”入手建模抽象代数的教材通常从公理出发先给定义再给例子。这个顺序对证明友好对理解不友好。我更推荐反过来拿一个元素个数不多的有限集合把运算表整张画出来然后拿公理一条一条去卡。卡住了就是反例卡通了就是例子比看十遍定义管用。元素个数选在 3 到 8 之间最舒服。3 个元素有 9 个格子8 个元素有 64 个格子手画一张表最多半小时但这半小时里你会把封闭性、结合律、单位元、逆元全部过一遍而且会亲眼看到“为什么结合律不能省”。后面所有关于子群和商群的操作本质上都是在这张表上做行和列的重新排列与合并表在你脑子里越具体抽象概念就越不容易飘。我后面给的 Python 脚本核心思路也是这个把运算做成一个可调用的函数然后用暴力枚举把公理逐条验一遍。有限群的优势在于任何命题都可以用穷举来验证你不需要“想到一个巧妙证明”跑一遍就知道对错这对建立直觉极其有效。2. 半群封闭与结合是整个体系的地基2.1 半群的最小门槛只有两条一个非空集合 S配上一个二元运算 ·如果满足两条就叫半群第一封闭性任意两个元素运算的结果还在 S 里第二结合律对任意三个元素都有 (a·b)·c a·(b·c)。就这两条没有单位元没有逆元也没有交换律。很多人觉得这门槛低得离谱什么都算半群没意思。恰恰相反门槛低意味着覆盖面广而覆盖面广的东西往往是最难满足的——你以为随手抓一个运算就是半群实际上一抓一个准地违反结合律。几个真半群正整数在加法下任意两个正整数相加还是正整数结合律显然成立这是个半群但它没有单位元因为 0 不在正整数集合里。字符串在拼接下任意两个字符串拼起来还是字符串拼接满足结合律单位元是空串所以这是个幺半群。方阵在乘法下同阶方阵相乘还是同阶方阵矩阵乘法满足结合律单位元是单位阵这也是幺半群但只有可逆矩阵才构成群。集合在对称差下两个集合的对称差还是集合对称差满足结合律单位元是空集而且每个集合的逆元就是它自己所以它是群而且是个交换群。注意最后一句话的推论集合的对称差运算里每个元素都是自己的逆元。这个性质在工程里很有用——如果你把一个状态用集合表示合并操作定义为对称差那么合并两次相同的内容就等于没合并天然具备“撤销”能力不需要额外记录历史。这就是“群比半群多了什么”的直观体现。2.2 结合律破掉的时候事情会变得非常难看我特别推荐用浮点数加法当反面教材因为它离工程最近而且杀伤力最大。在标准的浮点表示下加法是封闭的但结合律不成立。你可以直接在解释器里敲先算 0.1 加 0.2再加 0.3换一种顺序先算 0.2 加 0.3再加 0.1。两次结果在小数点后十几位上会出现差异。这不是实现的 bug是浮点表示本身的舍入误差在累加顺序不同时被放大的必然结果。这个事实的工程后果很实在并行归约的合并树形状会影响最终结果的最后几位数据库里对一批浮点数求和按不同的索引顺序扫描可能得到不同的账如果你用浮点做累加计数器跑得越久误差越大。所以正经的数值计算会采用补偿求和、先按绝对值排序再求和、或者直接用定点/有理数表示。这里的教训是封闭性很容易满足结合律才是真正的分水岭。一个运算只要封闭就能定义出来但只有满足结合律你才能“随意重新分组”才能并行、才能分治、才能缓存中间结果。我后来判断一个合并函数能不能用于并行计算第一件事就是看它结不结合而不是看它快不快。还有一个更隐蔽的坑定长整数加法。看起来 32 位整数相加是封闭的实际上溢出会回绕回绕之后的语义变成了模 2 的 32 次方的加法而这个结构恰恰是个群每个元素都有逆元相反数单位元是 0。这解释了一个每天在用的现象补码的加减法可以完全不管符号位因为它们本来就是在模 2 的 32 次方的加法群里做运算。你看连最基础的整数运算都是个群只是平时没人这么讲。2.3 幺半群多一个单位元换来一整套折叠能力在半群上加一个单位元 e要求对任意 a 都有 e·a a·e a就成了幺半群。这个要求看起来微不足道但它带来的能力是质变的你可以定义“折叠”。折叠的意思是把一串元素用运算一个个合并起来。没有单位元的时候空序列没法折叠你只能规定“至少有一个元素”。有了单位元空序列的折叠结果就是单位元本身这让递归定义变得干净fold([]) efold([x] xs) x · fold(xs)。所有列表求和、求积、拼接、最大值、最小值背后都是这个模式。我见过最典型的设计失误是把某个业务的合并操作设计成半群然后在上层到处写“如果列表为空就特判返回空”。一旦你在十处地方写这种特判就说明底层结构选错了应该补上单位元升级成幺半群。补单位元的成本通常很低——加法和补个 0乘法补个 1拼接补个空串集合合并补个空集——但它能把调用方从“边界情况处理”里彻底解放出来。再往上还有一层如果运算还满足交换律那这个幺半群就可以做无序归约也就是并行计算时不用关心元素顺序任意切分、任意合并都行。分布式系统里做状态合并的那类数据结构本质上就是这个性质在撑腰——两个节点各自合并本地的一批更新再把结果合并等价于把两批更新混在一起合并前提就是运算既结合又交换而且幂等同一个更新合并两次和合并一次结果一样。这三个性质对应到代数结构上就是交换幂等幺半群也叫半格。你会发现一个分布式系统的合并语义能不能做对答案全在半群那一层就已经定了。3. 子群从局部封闭到陪集划分3.1 子群判定的三条线和一个高频误判群 G 的非空子集 H如果在 G 的运算下自己也构成群就叫子群。判定的时候不要真的去验证四条公理用下面这个等价条件最快H 非空且对任意 a、b ∈ H都有 a·b⁻¹ ∈ H。这一条同时打包了封闭性和逆元存在性证明起来也干净。高频误判是把“封闭”当成“子群”。在有限群里一个非空子集如果对运算封闭那它自动就是子群因为每个元素的幂次序列 a、a²、a³…… 在有限集合里必然重复重复之后能推出单位元和逆元都在里面。但在无限群里这个推论直接失效。最干净的反例取整数加法群它的子集正整数集合对加法封闭——两个正整数加起来还是正整数——但它不是子群因为正整数里没有 0也没有负号。很多人脑子里“封闭就是子群”的印象就是被有限群的例子惯出来的。所以我在代码里检查子群时一定会显式验证逆元不会偷懒只查封闭除非我明确知道群是有限的并且验证过这个捷径成立。3.2 陪集是把群切成等大的块给定群 G 和子群 H取一个元素 g把 g 和 H 里每个元素乘一遍得到集合 gH叫 H 的一个左陪集。把所有 g 都试一遍得到的这些左陪集有一个非常漂亮的性质任意两个左陪集要么完全相等要么完全不相交而且它们把所有元素不重不漏地盖住一遍。这等价于在群上定义了一个等价关系a 和 b 等价当且仅当 a⁻¹b ∈ H。等价类就是左陪集。这一步的思想很关键——陪集不是“随便切一刀”它是由子群诱导出来的一个等价关系而等价关系意味着你可以“把同一类的东西看成同一个对象”。由此得到那个用得最多的结论子群的阶整除群的阶商叫指数记作 [G:H]。这个结论的用法是反向的如果某个整数不整除群的阶那这个群里绝对不存在这个阶的子群连找都不用找。一个 6 阶群里去找 4 阶子群纯属浪费时间。这个筛子在实际计算里能省掉大量枚举。但请注意整除只是必要条件不是充分条件。存在阶能整除、但确实没有对应阶子群的群。经典反例是四元交错群阶是 12按整除性它“应该”有 6 阶子群实际上没有。我第一次遇到这个反例的时候挺意外因为它打破了“阶整除就有子群”的朴素直觉。所以正确的用法是不整除一定没有整除也不保证有得进一步分析。3.3 正规子群左右陪集什么时候能对上有了左陪集自然有右陪集 Hg。一般情况下 gH 和 Hg 不相等。什么时候相等对所有 g 都相等的时候这个子群就叫正规子群。正规子群有三种等价的刻画我按实用程度排序。第一种共轭不变对任意 g ∈ G 和 h ∈ H都有 g h g⁻¹ ∈ H。这个最适合写成代码两层循环暴力验证就行。第二种它等于某个同态的核。第三种它的所有左陪集和右陪集一一重合。还有两个能秒判的技巧。第一指数为 2 的子群一定正规。理由是陪集只有两个左陪集划分出来的两块和右陪集划分出来的两块都是“H 和它的补集”划分方式唯一自然重合。所以任何 6 阶群里的 3 阶子群、8 阶群里的 4 阶子群闭着眼睛都知道正规。第二交换群的所有子群都正规因为交换律让 g h g⁻¹ 直接化简成 h。非正规的例子必须手算一遍才有感觉。取三个元素的全排列群考虑只交换前两个元素生成的那个二阶子群元素是恒等和这个对换。拿第三个元素参与的对换去共轭它结果会变成另一个对换而那个对换不在原来的二阶子群里。所以这个子群不正规。这个计算我建议每个人都亲手做一遍看着元素“跑出集合”的那一下比看定义有说服力得多。正规性为什么重要因为它决定了你能不能把子群“除掉”。不正规的子群你连陪集乘法都定义不出来后面全崩。4. 商群在等价类上做运算到底靠不靠得住4.1 商群的四步构造与良定义的那一步商群的构造有固定套路我拆成四步。第一步取正规子群 N。第二步把 G 中所有 N 的左陪集收集起来作为新集合的元素记作 G/N。第三步定义运算(aN)·(bN) (ab)N也就是取两个陪集的代表元乘完再取陪集。第四步验证这个定义与代表元的选取无关。第四步是全部难点所在术语叫良定义。因为一个陪集有无穷多个代表元只要 N 不是平凡子群你换一个代表元比如把 a 换成 a n₁把 b 换成 b n₂乘积变成 a n₁ b n₂。你要证明它和 a b 落在同一个陪集里。证明的关键一步是把 n₁ 从 a 的左边“挪”到 b 的右边去而这需要借助正规性b⁻¹ n₁ b 仍然属于 N。你把这个因子插进去再提出来整体就变成 a b 乘以一个 N 中的元素落在 abN 里。到这里良定义才算成立。现在你可以看清正规性到底在干什么了它保证了共轭这个动作把 N 映射回 N 自己从而让“挪动因子”合法。如果 N 不正规b⁻¹ n₁ b 跑出去了整个证明在第一步就断了。这就是为什么教材要先讲正规子群再讲商群顺序不是随便排的。商群本身也是个群单位元是 N 自己也就是 eNaN 的逆元是 a⁻¹N结合律从原群继承。另外还有一个自然的映射把每个 g 映到 gN叫自然投影它是个满同态核恰好是 N。这个映射的存在让商群不只是个抽象对象而是一个可以从原群“算出来”的东西。4.2 用同态核反向造正规子群比正向找省事实践中比“先找正规子群再造商群”更常用的路径是先有一个同态然后取它的核。因为核必然是正规子群这是免费的。道理很简单如果 φ(g h g⁻¹) φ(g) φ(h) φ(g)⁻¹而 h 在核里意味着 φ(h) 是单位元那么整个式子算出来还是单位元所以 g h g⁻¹ 也在核里。共轭不变成立核自动正规。举个例子整数加法群到模 12 剩余类加法群的映射把每个整数映成它除以 12 的余数。这是个同态核是所有 12 的倍数。这个核是正规子群交换群里当然正规商群就是模 12 剩余类群。再举一个例子用三个元素的全排列群到二阶循环群的符号同态把偶排列映到 0、奇排列映到 1。核是全体偶排列构成 3 阶子群商群是 2 阶群。这同时用上了前面那个“指数为 2 必正规”的技巧两条路殊途同归可以互相验证。同构基本定理把这件事总结成一句话任何同态商掉它的核就同构于它的像。我习惯把它当成一个“因式分解”工具一个复杂的群通过某个同态被压扁压扁后的形状就是原群除以核。你想知道压扁损失了多少信息看核有多大你想知道压扁后剩什么看像是什么。这个视角在处理具体问题时比定义好用得多。4.3 代码实录从暴力枚举子群到自动构造商群下面这段脚本我自己用了很久跑的是有限群元素个数不超过 8 的时候速度完全够。核心思路就是把公理逐条翻译成循环。from itertools import combinations, permutations def compose(p, q): 置换复合 p∘q先作用 q再作用 p return tuple(p[q[i]] for i in range(len(q))) class Group: def __init__(self, name, elems, mul): self.name name self.elems list(elems) self.S set(self.elems) self.mul mul self.e self._identity() def _identity(self): for e in self.elems: if all(self.mul(e, a) a and self.mul(a, e) a for a in self.elems): return e raise ValueError(f{self.name}: 无单位元只能算半群/幺半群) def inv(self, a): for b in self.elems: if self.mul(a, b) self.e and self.mul(b, a) self.e: return b raise ValueError(f{a} 无逆元) def check(self): for a in self.elems: for b in self.elems: if self.mul(a, b) not in self.S: return False, f不封闭{a},{b} for a in self.elems: for b in self.elems: for c in self.elems: if self.mul(self.mul(a, b), c) ! self.mul(a, self.mul(b, c)): return False, f不结合{a},{b},{c} for a in self.elems: self.inv(a) # 找不到逆元会抛异常 return True, 满足群公理先验半群再验群。下面这条是我单独拆出来的因为它能直接告诉你“结合律在哪一步破的”调试浮点或自定义运算时特别有用def check_semigroup(elems, mul): S set(elems) for a in elems: for b in elems: if mul(a, b) not in S: return False, f不封闭{a} * {b} {mul(a, b)} for a in elems: for b in elems: for c in elems: left mul(mul(a, b), c) right mul(a, mul(b, c)) if left ! right: return False, f不结合({a}*{b})*{c}{left}{a}*({b}*{c}){right} return True, 半群成立子群枚举用组合暴力只对有限群有效def all_subgroups(G): res [] for k in range(1, len(G.elems) 1): for sub in combinations(G.elems, k): s set(sub) if G.e not in s: continue if all(G.mul(a, b) in s for a in sub for b in sub) and \ all(G.inv(a) in s for a in sub): res.append(s) return res正规性判定严格按共轭定义来不做任何优化def is_normal(G, H): for g in G.elems: gi G.inv(g) for h in H: if G.mul(G.mul(g, h), gi) not in H: return False return True最后是商群先把每个元素对应的左陪集算好用字典缓存避免重复计算def quotient(G, N): if not is_normal(G, N): raise ValueError(N 不正规商群不成立) table {g: frozenset(G.mul(g, n) for n in N) for g in G.elems} cs set(table.values()) rep {c: next(g for g in G.elems if table[g] c) for c in cs} def mul(x, y): return table[G.mul(rep[x], rep[y])] return Group(f{G.name}/{len(N)}, list(cs), mul)拿三个元素的全排列群跑一遍输出大致是这样G S3, |G| 6 子群总数6 阶 11 个 阶 23 个 阶 31 个 阶 61 个 正规子群{e}、A3、S3 商群 S3/A3 的阶 2与 C2 同构 阶 2 子群 (01) 正规性False这里有个值得注意的细节三个 2 阶子群全都不正规唯一非平凡正规子群是那个 3 阶子群。这不是巧合6 阶群里 3 阶子群的指数是 2必须正规2 阶子群指数是 3没有这种强制力。手动用共轭去算也能得到同样的结论。5. 常见错误与排查实录5.1 症状对照表八成问题落在这些地方症状真实原因处理办法子集对运算封闭但算不出逆元在无限群里封闭推不出子群显式验证逆元或用 a·b⁻¹ ∈ H 判据商群的运算结果依赖代表元拿非正规子群去构造先做共轭检验不正规就别谈商群左右陪集对不上划分出更多块子群不正规算 gH 和 Hg 比较或直接验共轭枚举了所有阶找子群一无所获阶不整除群阶先用整除性筛掉不可能的情况阶整除但确实没有该阶子群整除只是必要条件换判据或先看正规子群格子结合律“看着”成立跑起来结果飘浮点舍入破坏结合律换定点/有理数或固定归约顺序置换复合结果与预期相反复合顺序约定不一致全程固定“先右后左”写进注释折叠空列表时报错只到半群没有单位元补单位元升级成幺半群表里最容易被忽略的是最后一行。业务代码里到处写“列表为空就返回默认值”本质上是把单位元的位置藏在了调用方每多一个调用方就多一份重复。把单位元提到类型层面那些特判就能一次性消掉。5.2 一套可复用的排查流程我现在的习惯是固定顺序先验封闭再验结合再找单位元再找逆元最后才谈子群和商群。这个顺序不能反因为后一步的结论全部依赖前一步成立。封闭性出错最常见的原因是用错了元素集合比如只把生成元放进去而忘了闭包。结合律出错最常见的原因是自定义运算里混入了顺序敏感的操作比如依赖哈希表遍历顺序的合并函数。单位元找不到往往是因为集合不完整或者单位元其实存在但被错误地排除在集合之外。逆元找不到那就说明这个结构只是幺半群你应该回头想清楚业务上到底需不需要撤销能力。到了子群这一层先别急着枚举先用整除性把所有不可能的阶划掉能砍掉一大半工作量。剩下的候选阶里先看有没有指数为 2 的子群有的话它一定正规可以先拿来构造商群试试水。如果商群构造失败那问题一定出在正规性上回头验共轭就完了。到了商群这一层最容易踩的坑是把代表元当成了元素本身。写出abN的时候要在心里默念一句“这里每个字母都是整个陪集”一旦忘了这一点后面算出来的东西很容易自相矛盾。提示验证正规性时不要只测一个共轭元就下结论。我在早期代码里写过“取第一个元素试一下”的偷懒写法结果在某个 8 阶群上漏判商群构造出来之后单位元不唯一排查了整整一个下午才发现是判定函数的问题。6. 这些概念在工程里到底落在哪6.1 类型类里的半群与幺半群不是装饰很多主流语言的函数式库都直接提供了半群和幺半群的类型类名字可能叫 Semigroup、Monoid也可能藏在别的抽象后面。它们的约束写得非常直白半群只要求一个结合的结合运算幺半群再额外要求一个单位元。这个抽象的真正价值在于一旦某个类型被声明成幺半群你就免费获得了一整套通用函数折叠、归约、并行聚合、批量合并。而且这些函数的正确性不依赖具体类型只依赖那两条公理。这就是为什么并行集合库敢把归约操作做成无序的——它只要求元素构成交换幺半群剩下的随你怎么切。反过来声明的时候要慎重。如果你把一个实际不满足结合律的操作声明成半群编译器不会拦你但所有依赖这个假设的并行函数都会给出错误结果而且是那种时对时错、难以复现的错误。我在一个自定义的近似算法里就吃过这个亏合并两个近似结果本身就有舍入而且不满足结合律硬塞进归约函数之后并行和串行结果不一致定位了很久。后来老老实实退回到串行或者把合并操作改成先规约到精确表示再合并。6.2 固定宽度整数其实是商群在打工前面提过一句这里展开说。32 位无符号整数的加法本质上是在模 2 的 32 次方的剩余类群里做运算。这个群有 2 的 32 次方个元素单位元是 0每个元素的逆元是它的补数加一。理解这一点之后很多“看起来奇怪”的行为就顺了。溢出回绕不是错误是模运算的自然结果减法可以直接用加补数实现有符号数和无符号数的加减法可以用同一套电路。这些设计选择都建立在一个事实之上模 2 的幂的加法构成群而且这个群的结构极其规整——它的子群是那些模 2 的 k 次方的倍数构成的商群还是同类型的群。这个“子群和商群同构于同类结构”的现象在整数模运算里普遍存在。取模 12 的剩余类群它有个 4 阶子群0、3、6、9商掉之后得到的是模 3 的剩余类群它还有个 3 阶子群0、4、8商掉得到模 4 的剩余类群。你完全可以在代码里把这个过程跑出来看到 12 阶群被压成 3 阶和 4 阶两个小群这就是商群最具体的模样。6.3 对称性分析里的子群就是“允许的操作”凡是涉及对称性的地方群就一定会出现而子群对应的是“在整体对称性里有哪些操作能保持某个局部特征不变”。举个几何上的例子。正方形的对称操作一共 8 个4 个旋转加 4 个翻转。这 8 个操作构成一个群。现在你关注的是“只旋转不翻转”的那些操作它们构成 4 阶子群指数为 2所以一定正规商群是 2 阶群——这个商群的元素分别对应“保持手性”和“反转手性”两类操作。你不需要知道 8 个操作的具体细节只要知道商群是 2 阶的就能得出“手性只有两种状态”这个结论。再比如一个几何体如果同时具备正方形和三角形的对称性那它的对称群必须同时包含这两组操作以及它们的组合而包含关系在子群格上就是“向上取闭包”。做形状匹配、分子结构分类、晶体点群分析的时候实际在做的操作就是先算出候选的对称操作集合验证它闭合成群再找出所有子群用子群的阶和指数来给结构分类。这里面没有一步需要解方程全是集合运算加公理验证正好适合写成程序跑。我见过一个做图案生成的思路就是把基本图形按某个子群的作用铺开然后改变子群就换了一种铺法。旋转子群给出同心式的排列翻转子群给出镜像式的排列全群给出最自由的那种。这套玩法门槛不高但前提是你得真的理解子群和陪集在几何上是什么样子。6.4 状态合并为什么非得是交换幂等幺半群回到开头那个让我转变学习方式的场景。分布式环境下两个节点各自处理了一批更新需要把状态合并成一致的结果。这个合并函数要满足什么结合律必须有否则“先合哪两个”会影响结果节点之间没法达成一致。交换律必须有因为网络到达顺序不可控A 先到还是 B 先到必须结果相同。单位元必须有因为“没有任何更新”也得是一个合法状态不然空节点没法参与合并。幂等最好有因为消息可能重复投递同一个更新合并两次和合并一次应该一样。这四条凑齐对应的代数结构是交换幂等幺半群也叫半格。它的好处是合并操作的语义可以完全由“取上确界”来刻画实现上通常就是集合求并、取最大值、按位或这类操作。凡是能用半格建模的状态做最终一致性都非常省事因为最终状态只和“收到了哪些更新”有关和顺序、次数完全无关。而那些不满足这些性质的状态比如需要保序的计数器、需要精确线性累加的金额就必须引入额外的机制。你看选什么样的数据结构其实在数学层面早就被这几个公理框死了。理解半群、幺半群、群这条链最大的收获不是会做几道证明题而是面对一个合并逻辑时能立刻判断出它能不能并行、能不能重放、能不能撤销。我自己踩过最深的坑是在一个需要“撤销上一步”的功能里把状态设计成了只支持合并的幺半群结果每加一个撤销操作就得回放整条历史。后来把状态改成支持逆元的群结构撤销就变成了一次逆元运算代价小了一个量级。那次之后我再设计状态模型第一件事就是问自己这个东西需要逆元吗如果需要那我就得从一开始把它当成群来设计而不是事后打补丁。