版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖。如要转贴,必须注明原文网址
https://www.cnblogs.com/Colin-Cai/p/18774816.html
作者:窗户
QQ/微信:6679072
E-mail:[email protected]
我准备讲有限Abel群,总觉得,对于一个程序员来说,离散数学的各个科目无论是从训练思维还是从实用角度都是不错的。总是觉得程序员应该重视理论方面的学习,其中自然也包括数学。当然,讲解过程中也包含着一些程序,毕竟程序才是程序员的根。
映射
我们可以定义\=\\{\\{a\\},\\{a,b\\}\\}
为何这样定义,就表示a/b的有序性了,自己想想,注意,按照以上定义,
当然,你可以用别的定义方法,只要能保证唯一性。
集合A和集合B的笛卡尔积(Cartesian Product),定义如下:
A\\times B=\\{\\|a\\in A,b\\in B\\}
也就是遍历A的元素和B的元素组成序偶的所有可能。
比如\\{1,2\\}和\\{a,b,c\\}的笛卡尔积是\\{\<1,a\>,\<1,b\>,\<1,c\>,\<2,a\>,\<2,b\>,\<2,c\>\\}
映射 (mapping),又叫函数(function),这个概念为大家所熟知,此处还是得形式化描述一下
集合A到集合B的映射f,是指A\\times B的子集,满足
\\forall a\\in A \\exists!b\\in B:\\\in f
换成大白话,就是对于任意A里的元素a,f(a)都是B里的元素,存在且唯一。
其中,A是f的定义域,B是f的值域。
半群
后面,我们看一种特殊的函数,对于集合A,
它的定义域是A\\times A,值域为A
这种函数我们称为集合A上的二元运算,比如我们常见的加法、减法、乘法、除法(当然,无论是对于整数集、有理数集、实数集、复数集)......
如果集合A上的二元运算f满足结合律,也就是
\\forall a,b,c\\in A:f(\<\,c\>) = f(\\>)
则称集合A在二元运算f下构成一个半群(semigroup)
这样写,不是我们习惯的写法,我们一般把满足结合律的二元运算叫乘法,用中缀表达式更习惯,那么乘法满足交换律,则为
\\forall a,b,c\\in A:(a\\cdot b)\\cdot c=a\\cdot (b\\cdot c)
现实中很多这样的半群例子,比如整数集在乘法下构成半群,非0整数集在乘法下构成半群。
再比如2阶实数矩阵集在矩阵乘法下构成半群。
当然,整数在加法下也构成半群,但可能会有点困惑,加法、乘法......
实际上,数学上,要注意的是形式的不变性,至于叫什么,不重要,真不重要。
群
如果一个半群满足以下两点:
(1) 该半群里有一个元e,对于半群里任何元x,都有
x=x\\cdot e=e\\cdot x
(2)对于半群里任意一个元x,都存在一个x',使得
x\\cdot x' = x' \\cdot x = e
那么,我们称这个半群为群(group)。
其中,满足第一个条件的e为该群的幺元 ,或称单位元 ;第二个条件里的x和x'互为逆元。
举几个实际的群的例子:
实数集在加法上为一个群,其中幺元是0(任何数加上0值不变),每个元的逆元是其相反数(任何数和其相反数相加等于0);
非零实数集在乘法上为一个群,其中幺元是1(任何数乘以1值不变),每个元的逆元是其倒数(任何数和其倒数相等于1),此处注意实数集在乘法上并不是一个群,因为0不存在逆元;
对于一个具体的正整数n,实数n阶非奇异矩阵(也就是行列式值不为0)构成的集合在矩阵乘法上为一个群,其中幺元是I_{n},每个元的逆元是其逆阵。
群里元素的数量叫做群的阶。
相同阶的群
本节是想写程序看看给定阶数的群有哪些。
作为抽象代数的重要分支,群论不是简单几句就可以说清楚的,本系列其实也只会讲群论的一小部分。所以本节主要是暴力求解。
我们想暴力求给定n阶群(也就是群里元素的个数为n)的群有哪些,那么我们设这些元素为S_0,S_1,...S_{n-1},在不引起误解的时候,我们可以用0,1,...S_{n-1}来代表S_0,S_1,...S_{n-1},嗯,都到了抽象代数这样的程度,其实符号未必重要。
我们用一个n\\times n的方阵A来代表这个群,其实也就是这个群上的乘法表,其中
S_a\\cdot S_b = S_{A_{a,b}}
我们就用Python来实现吧,就用自带的array库用一维数组{B}来模拟方阵吧。
A_{a,b} = B_{a\*n+b}
先建立高阶的暴力求解框架,如下:
def make_search_all_groups_func(get_all_maybe_groups, is_group, print_group):
def f(n):
for s in get_all_maybe_groups(n):
if is_group(n, s):
print_group(n, s)
return f
get_all_maybe_groups是用来产生可能是group的二元运算,因为待选对象可能很多,gen_all_maybe_groups一般应该是个generator。
is_group是用来判定这个二元运算是不是可以作为群的乘法表,
如果是,就打印出这个群,当然,这个群可以用乘法表来代表。
那么,这个打印群,我们可以这样写:
def print_group(n, s):
a = 0
b = 0
print('group:')
for r in s:
print('S%d x S%d = S%d' % (a, b, r))
if b < n - 1:
b += 1
else:
a += 1
b = 0
print('', end='', flush=True)
以上不难,那么接下来的问题在于如何遍历所有可能的二元运算,简单的想想,这应该是n\\times n个\\{0,1...n-1\\}来做笛卡尔积,
好在Python有itertools库可以做笛卡尔积,
itertools.product(range(n), range(n) ...)
可惜是个不固定参数的调用,不过Python是可以支持的,支持的方法就是这个*,展开参数,很像Lisp的apply函数。
import array
import itertools as it
def get_all_maybe_groups_v1(n):
return map(lambda s:array.array('i', s), it.product(*[range(n)]*(n**2)))
前面加个map将每个元素转为array,之所以变成array,在于array寻址效率高。
然后就是判定是否为群了,判定是否为群,需要经过三步:
(1)判断二元运算是否满足结合律
(2)判断是否有幺元
(3)判断是否每个元都有逆元
那么写成代码可以如下:
def is_group_v1(n, s):
if not assoc_low(n, s): #结合律检验
return False
e = get_ident_element(n, s) #找幺元
if e is None:
return False
if not each_can_inverse(n, s, e): #看是否每个元都有逆元
return False
return True
分别实现三个函数。
结合律也是笛卡尔积遍历所有可能,分别检验
def assoc_low(n, s):
mul = lambda a, b : s[a * n + b] #乘法
for a, b, c in it.product(range(n), range(n), range(n)):
if mul(mul(a, b), c) != mul(a, mul(b, c)):
return False
return True
再来找幺元,看是否有一个元,所有元和它左乘右乘都不改变,
def get_ident_element(n, s):
mul = lambda a, b : s[a * n + b] #乘法
for i in range(n):
flag = True
for j in in range(n):
if mul(i, j) != j or mul(j, i) != j:
flag = False
break
if flag:
return i
return None
最后再来看每个元有没有逆元,依然是遍历,
def each_can_inverse(n, s, e):
mul = lambda a, b : s[a * n + b] #乘法
for i in range(n):
flag = False
for j in range(n):
if mul(i, j) == e and mul(j, i) == e:
flag = True #找到了逆元,标记一下找到了
break
if not flag:
return False #当前i没有找到逆元
return True
这样,我们实现了一个搜索版本
search_all_groups_v1 = make_search_all_groups_func(get_all_maybe_groups_v1, is_group_v1, print_group)
结果我们调用search_all_groups_v1(4)希望搜索4阶群,就发现计算非常慢了。
我们是不是可以再快一点呢?
很多时候,此类搜索我们发现一些定理就可以加快搜索速度。
我们先证明一个命题:
对于任意群\
\\forall a,b,c\\in G:a\\cdot b=a\\cdot c \\rightarrow b = c
\\forall a,b,c\\in G:b\\cdot a=c\\cdot a \\rightarrow b = c
其实,
a \\cdot b = a \\cdot c
\\rightarrow a\^{-1}\\cdot(a \\cdot b) = a\^{-1}\\cdot(a \\cdot c)
\\rightarrow (a\^{-1}\\cdot a) \\cdot b = (a\^{-1}\\cdot a) \\cdot c
\\rightarrow e \\cdot b = e \\cdot c
\\rightarrow b = c
其中,a\^{-1}是a的逆元,e是群的幺元。
同理,
b \\cdot a = c \\cdot a
\\rightarrow (b \\cdot a) \\cdot a\^{-1} = (c \\cdot a) \\cdot a\^{-1}
\\rightarrow b \\cdot (a \\cdot a\^{-1}) = c \\cdot (a \\cdot a\^{-1})
\\rightarrow b \\cdot e = c \\cdot e
\\rightarrow b = c
于是我们知道,对于群里的任何一个元素,乘以不同的元素得到的结果都不一样,
那么再细细一想,对于群里任何一个元素a,乘以S_{0},S_{1}...S_{n-1}得到的a \\cdot S_{0}, a \\cdot S_{1}...a \\cdot S_{n-1}是S_0,S_1...S_{n-1}的一个排列,
被S_{0},S_{1}...S_{n-1}乘得到的S_0 \\cdot a ,S_1 \\cdot a ...S_{n-1} \\cdot a 也是S_0,S_1...S_{n-1}的一个排列。
乘法表这样一个矩阵里的每个数,其在所属行和所属列里是独一无二的。
利用这个性质,我们筛选二元运算时,就可以不要用笛卡尔积了,这样轻松的筛掉了绝大多数不可能是群乘法的二元运算。
另外,我们可以一上来就让S_0来做群的幺元,于是n\\times n的乘法表已经固定了其中的2n-1项,
\begin{pmatrix}
&S_0 & S1 & ... & S_{n-1}\\
&S_1 & ...& \\
&...& ...&\\
&S_{n-1} & ... & \\
\end{pmatrix}
于是,乘法表里只有(n-1)\^2项需要去待定。
每加一项都要判断在这一行或这一列中没有相同的元,我们按照字典顺序(dictionary order)去依次遍历所有的可能。
不得不说,字典顺序是个很方便的遍历方法,如果你还不熟悉,那么还是多练习一下比较好。
根据上面,我们写了一个新的版本来待定乘法表
def get_all_maybe_groups_v2(n):
#0是幺元,先固定2n-1项
arr = array.array('i', n * n * [0])
for i in range(n):
arr[i] = i
arr[n * i] = i
#从1行1列开始遍历
row, col = 1, 1
FORWARD, BACKWARD = True, False
while True:
#初始的时候,搜索的方向默认为向后,只有成功找到了一个值才能改为向前
direction = BACKWARD
#依次从当前值开始搜索到最小的值,满足行/列无重复
for i in range(arr[row * n + col], n):
flag = True
for j in it.chain(range(row * n, row * n + col), range(col, row * n + col, n)):
if arr[j] == i:
#行列上有重复,就报错标志置起来
flag = False
break
if flag:
#成功的找到了新值,flag设为真用来代表找到了
direction = FORWARD
arr[row * n + col] = i
break
if direction == FORWARD:
#找到了当前的值,那么可以继续往前,坐标往前进一个
col += 1
if col >= n:
col = 1
row += 1
if row == n:
#此时,已经满了,得到了一个新的候选二元运算
yield arr
#后退两行加两个元素
#为什么可以后退这么多来,实际需要证明一下,有兴趣就想想如何证明吧
#因为只动最后两行没有其他可能解
#直觉能后退更多一点,不过连我自己也没多想
row = n - 3
col = n - 2
#既然是字典顺序,当前搜索的值至少要从下一个开始
arr[row * n + col] += 1
#后面的其他值都清为0,这样才是字典顺序,不会漏掉候选者
arr[row * n + col + 1] = 0
for i in range((n - 2)*n+1, (n - 1)*n):
arr[i] = 0
for i in range((n - 1)*n+1, n*n):
arr[i] = 0
else: #direction == BACKWARD
#没有找到当前的值,只能坐标往前退一步了
#字典顺序下,当前值清为0,而退一步之后的位置则要加1,这才是紧接着的下一个字典序
arr[row * n + col] = 0
col -= 1
if col == 0:
col = n - 1
row -= 1
#退无可退,都退到第0行固定的那些值上去了,说明遍历完了
if row == 0:
return
arr[row * n + col] += 1
还可以继续优化,比如判断结合律是否可以提前到遍历乘法表的每一个值的时候就判一下呢?
其实完全可以的,这样速度又可以秒杀这个版本,有兴趣就自己来写写吧。
优化很多时候就是无底洞,你可以不断的用新的定理提高此类遍历/验证的效率。
当然,如果你具备扎实的群论基础,比如你至少明白什么叫Sylow定理,那么这个写法甚至会有巨大的改变。
不过,在我的这一系列文章中,不会把群论深入到这样的深度。