群论——从入门到放弃(群、置换、Burnside引理和Polya定理)

前言

正如大家所知,我是一个蒟蒻,所以我滚来学群论了QAQ
群论其实是个很厉害的东西,不知道比反演什么的简单到哪里去了。
啊,学渣苦,学渣累。——Friedrich Taylor

什么是群?

首先我们要知道什么是群。
群的定义:给定一个集合G={a,b,c,…}和集合上的二元运算” ”,满足以下四条性质:
1.封闭性:a,bG,cG,ab=c
2.结合律: a,b,cG,(ab)c=a(bc)
3.单位元: eGaG,ae=ea=a
4.逆元: aG,bG,ab=ba=e,b=a1
则称集合G在运算” ”下是一个群,可以简称为G是群。
顺便普及一下关于群的两个定理
1.如果(G,)是群,则 gG,gG=Gg=G
其中 gG={gh|hG}
2.如果(G, )是群,H是G的非空子集,且(H,)也是群,那么称H为G的子群。
根据定理2可以判断子集是否为一个子群: HH=HH1=H 等价于H是G的子群。

什么是置换?什么是置换群?

在学习了群的基础概念后,我们还需要一个知识补丁。
n个元素之间的置换 (1a12a23a3......nan) 表示1被1到n中的某个数 a1 取代,直到n被1-n中的某个数 an 取代,且 a1,a2,...,an 互不相同。
置换群的元素是置换,运算是置换连接。
e.g.(13213244)(14233241)=(13213244)(32142341)=(12243341)
貌似我们可以验证置换群的确是群。
具体的简单感性认知如下
1.封闭性: (1a12a2......nan)(a1b1a2b2......anbn)=(1b12b2......nbn)
2.结合律: {(1a12a2......nan)(a1b1a2b2......anbn)}(b1c1b2c2......bncn)=(1a12a2......nan){(a1b1a2b2......anbn)(b1c1b2c2......bncn)}
3.单位元:
e=(1122.......nn)
4.逆元:
(1a12a2......nan)1=(a11a22......ann)

Polya定理

我们都知道大名鼎鼎的Polya定理:
L=1|G|si=1mc(gi)
其中 L 表示本质不同的方案数,|G|表示置换数, c(gi) 表示置换i中循环节的个数,m表示染色的方案数。
首先介绍一下循环的概念:
(a1,a2,...,an)=(a1a2a2a3......an1anana1) 被称为n阶循环
两个循环 (a1,a2,...an) (b1,b2,...bn) 互不相交,当且仅当 aibj ,其中 i,j=1,2,...,n .
例如: (1321324554)=(123)(45) 对于这个置换,其循环节数为2.
e.g. 等边三角形三顶点分别用红绿蓝三种颜色着色,有多少种不同的方案?其中的置换操作为:
(1)沿中心旋转 0 , 120 , 240
(2)沿三条中线翻转
解:易知对于沿中心旋转 0 ,置换为 (112233) ,循环节数为3
沿中心旋转 120 ,置换为 (122331) ,循环节数为1
沿中心旋转 240 ,置换为 (132132) ,循环节数为1
沿三条中线翻转的置换分别为 (112332) (122133) (122133) ,循环节数均为2
根据Polya定理可以知道不同的方案数 L
16(33+31+31+32+32+32)=10

如何证明Polya定理?Burnside!

Burnside引理是群论中的一个结论,可以用来计算等价类的个数,即
L=1|G|sj=1D(aj)
其中 D(aj) 表示在置换 aj 下不变的染色方案的个数。
同样我们可以用Burnside引理来解决上述等边三角形的问题
沿中心旋转 0 时,所有的 33 种染色方案均不变
沿中心旋转 120 240 时,只有三点颜色相同的染色方案才不变色
分别沿三条中线对折时,分别有 32 种方案不变色
所以L为 16(33+31+31+32+32+32)=10
相信大家都应该发现了:要得到在置换下稳定不动的方案,只需把置换的每个循环节染上相同的颜色即可,所以 D(aj)=mc(aj)
那么我们如何证明Burnside引理呢?
我们先引进一个定理( x ):
|Ek||Zk|=|G|,k=1,2,...,n
Ek :设 G 是1,2,…,n的置换群,若k是1,2,…,n中的某个元素, k G作用下的轨迹,即 k G作用下能够变化成的所有元素的集合,称之为 k 的等价类。
Zk:设 G 是1,2,…,n的置换群,若k是1,2,…,n中的某个元素, G 中使k保持不变的置换的全体,记做 Zk ,称之为 G 中使k不动的置换类。
我们通过又一个例题来最终”推导”出Burnside引理
e.g. 对2 2的方块进行黑白染色,能得到多少不同的图像?
其中的置换操作为:
旋转0,90,180,270
解:我们很容易根据burnside引理或者是Polya定理亦或是手推都能得到最后的方案一定是6种。那么我们继续无聊但有必要地推导一下:
总方案数: 24 =16种
设方案1为四格均白,则有
|E1||Z1|=14=4
设方案a为只有右上角一个黑格,其馀皆白,则有
|Ea||Za|=41=4
设方案b为右上左下皆为黑格,左上右下均为白格,则有
|Eb||Zb|=22=4
所以定理 x 是可以直观感知的
现在我们推一推Burnside
接下来我们研究每种方案在各个置换下不变的次数之和
ai为第i种置换,n表示方案数,s表示置换数
Sij={0,aiZjjai1,aiZjjai
Si,1+Si,2+...Si,n=D(ai)
以及 S1,i+S(2,i)+...+S(s,i)=Zk
由此可得 nj=1Zj=si=1nj=1Si,j=si=1D(ai)
也就是 nj=1Zj=si=1D(ai)
或者我们继续假设n个方案中总共有L个等价类
那么 n=E1+E2+...+EL
同时,当 i,j 属于同一等价类时,有 |Zi|=|Zj|
所以可以如是推导:
si=1D(ai)=nj=1Zj=Li=1jEi|Zk|=Li=1|Ei||Zi|=Li=1|G|=L|G|
很明显我们求的就是L
所以 L=1|G|sj=1D(aj) ,Burnside引理得证

一些简单得如南极冰盖瞬间融化一样水的习题

(1)Cipher(Central Europe 1995)
题面见链接(http://poj.org/problem?id=1026 )
题意:对于一个长度为n的字符串,有一个数组表示第i个字符放到那个位置。输入多个字符串,问这样操作k次后的字符串的形态
题解:暴力肯定是药丸的。
我们可以这样做:对于每个数组,求其循环节,再用总操作数依次模每个字符所在循环节长度后再暴力求解即可

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<ctime>
#include<cmath>
#include<algorithm>
#include<cctype>
#include<iomanip>
using namespace std;
inline long long read(){
    long long i=0,f=1;
    char ch;
    for(ch=getchar();!isdigit(ch);ch=getchar())
        if(ch=='-') f=-1;
    for(;isdigit(ch);ch=getchar())
        i=(i<<3)+(i<<1)+(ch^48);
    return i*f;
}
int buf[1024];
void write(long long x){
    if(!x){putchar('0');return ;}
    if(x<0){putchar('-');x=-x;}
    while(x){buf[++buf[0]]=x%10,x/=10;}
    while(buf[0]) putchar(buf[buf[0]--]+48);
    return ; 
}
#define stan 222
int sze[stan],trans[stan],tmp,n,k,pos,len;
char word[stan],ans[stan];
signed main(){
    n=read();
    while(n!=0){
        memset(sze,0,sizeof(sze));
        for(int i=1;i<=n;++i)
            trans[i]=read();
        for(int i=1;i<=n;++i){
            tmp=i;
            while(!sze[i]||tmp!=i){
                ++sze[i];
                tmp=trans[tmp];
            }
        }
        k=read();
        while(k!=0){
            memset(word,0,sizeof(word));
            memset(ans,0,sizeof(ans));
            gets(word+1);
            len=0;
            for(int i=1;i<=n;++i){
                if(word[i]==0) len=1;
                tmp=k%sze[i],pos=i;
                while(tmp--)
                    pos=trans[pos];
                if(len) ans[pos]=' ';
                else ans[pos]=word[i];
            }
            ans[n+1]=0;
            puts(ans+1);
            k=read();
        }
        puts("");
        n=read();
    }
    return 0;
}

(2)cow sorting(USACO 2007 February Gold)
题面依旧见链接(http://poj.org/problem?id=3270)
题意:给你n个数,按升序排序,可以交换任意两个数且每次交换的代价是这两个数的和,求最小代价。
题解:考虑一种比较优秀的方法:求出这个序列的循环节,那么对于这个循环节,设其为 a1,a2,...ax ,若数列只有该循环节,最小代价为
a1(x1)+(a2+a3+...+ax)
但还有一种情况:如果把 a1 换做整个数列最小的数 amin 后再进行上述操作,则花费为 2(a1+amin)+amin(x1)+(a2+a3+...+ax)
对于每个循环节,取①、②的最小值即可

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<ctime>
#include<cmath>
#include<algorithm>
#include<cctype>
#include<iomanip>
using namespace std;
inline long long read(){
    long long i=0,f=1;
    char ch;
    for(ch=getchar();!isdigit(ch);ch=getchar())
        if(ch=='-') f=-1;
    for(;isdigit(ch);ch=getchar())
        i=(i<<3)+(i<<1)+(ch^48);
    return i*f;
}
int buf[1024];
void write(long long x){
    if(!x){putchar('0');return ;}
    if(x<0){putchar('-');x=-x;}
    while(x){buf[++buf[0]]=x%10,x/=10;}
    while(buf[0]) putchar(buf[buf[0]--]+48);
    return ; 
}
#define stan 111111
int m,n,a[stan],pos[stan],inner[stan],mini;
long long ans;
signed main(){
    while(scanf("%d",&n)!=EOF){
        memset(inner,0,sizeof(inner));
        mini=999999999;
        for(int i=0;i<n;++i){
            a[i]=read();
            mini=min(a[i],mini);
            pos[a[i]]=i;
        }
        sort(a,a+n);
        for(int i=0;i<n;++i){
            if(i==pos[a[i]]||inner[a[i]]) continue;
            int temp_pos=i,sze=0;
            while(!inner[a[temp_pos]]){
                inner[a[temp_pos]]=true;
                temp_pos=pos[a[temp_pos]];
                ++sze;
                if(!inner[a[temp_pos]]) ans+=a[temp_pos];
            }
            ans+=min(a[i]*(sze-1),(a[0]+a[i])*2+a[0]*(sze-1));
        }
        write(ans);puts(" ");
    }
    return 0;
}

(3)Let it Bead(Ulm Local 2000)
题面依然见链接(http://poj.org/problem?id=2409)
题意:一个有n个元素的环,有m种颜色,求不同染色方案数
其中的置换操作为:顺时针旋转 1,2,...,n1 个元素
题解:此处用到一个结论:第i个置换的循环节个数为 gcd(n,i)
然后就是愉快的暴力套Polya

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<ctime>
#include<cmath>
#include<algorithm>
#include<cctype>
#include<iomanip>
using namespace std;
inline long long read(){
    long long i=0,f=1;
    char ch;
    for(ch=getchar();!isdigit(ch);ch=getchar())
        if(ch=='-') f=-1;
    for(;isdigit(ch);ch=getchar())
        i=(i<<3)+(i<<1)+(ch^48);
    return i*f;
}
int buf[1024];
void write(long long x){
    if(!x){putchar('0');return ;}
    if(x<0){putchar('-');x=-x;}
    while(x){buf[++buf[0]]=x%10,x/=10;}
    while(buf[0]) putchar(buf[buf[0]--]+48);
    return ; 
}
int m,n;
long long ans;
long long gcd(long long a,long long b){
    if(b) return gcd(b,a%b);
    else return a;
}
long long ksm(long long a,long long b){
    long long ret=1;
    while(b){
        if(b&1) ret*=a;
        a*=a;
        b>>=1;
    }
    return ret;
}
signed main(){
    m=read();n=read();
    while(n!=0&&m!=0){
        ans=0;
        if(n==0||m==0){
            puts("0");
        }else{
            for(int i=1;i<=n;++i)
                ans+=ksm(m,gcd(n,i));
            if(n&1)
                ans+=n*ksm(m,(n+1)/2);
            else
                ans+=(n>>1)*(ksm(m,n/2)+ksm(m,n/2+1));
            write(ans/(2*n));
            puts(" ");
        }
        m=read();n=read();
    }
    return 0;
}

(4)Magic Bracelet(POJ2888)
题面见链接(http://poj.org/problem?id=2888)
题意:n个元素m种颜色,某些颜色不能匹配,求染色方案数对9973取模。其中的置换操作为顺时针旋转 1,2,...n1 个元素。
题解:因为有限制所以只能用Burnside引理。
但是因为n达到了 109 ,显然不能暴力枚举
观察发现 Ni=1e[gcd(i,N)=k]=ϕ(N/k)
所以我们只需要枚举 1 n中能整除 n <script type="math/tex" id="MathJax-Element-106">n</script>的数即可
同时因为n过大我们也需要矩阵快速幂优化
注意这道题卡模运算常数

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<ctime>
#include<cmath>
#include<algorithm>
#include<cctype>
#include<iomanip>
#define mod 9973
using namespace std;
inline int read(){
    int i=0,f=1;
    char ch;
    for(ch=getchar();!isdigit(ch);ch=getchar())
        if(ch=='-') f=-1;
    for(;isdigit(ch);ch=getchar())
        i=(i<<3)+(i<<1)+(ch^48);
    return i*f;
}
int buf[1024];
void write(int x){
    if(!x){putchar('0');return ;}
    if(x<0){putchar('-');x=-x;}
    while(x){buf[++buf[0]]=x%10,x/=10;}
    while(buf[0]) putchar(buf[buf[0]--]+48);
    return ; 
}
int n,m,k,T,a,b;
struct matrix{
    int mat[11][11];
    inline void I()
    {
        memset(mat,0,sizeof(mat));
        for(int i=1;i<=m;i++)mat[i][i]=1;
    }
    inline matrix operator * (const matrix &b)
    {
        matrix c;
        for(int i=1;i<=m;i++)
            for(int j=1;j<=m;j++)
            {
                int sum=0;
                for(int k=1;k<=m;k++)
                    sum+=mat[i][k]*b.mat[k][j];
                c.mat[i][j]=sum%mod;        
            }
        return c;
    }
    inline matrix operator ^ (int x)
    {
        matrix res,tmp=*this;
        res.I();
        for(;x>0;x>>=1,tmp=tmp*tmp)
            if(x&1)res=res*tmp;
        return res;
    }
}basic;
int getphi(int x){
    int ret=x;
    for(int i=2;i*i<=x;++i)
        if(x%i==0){
            ret-=ret/i;
            while(x%i==0) x/=i;
        }
    if(x>1) ret-=ret/x;
    return ret%mod;
}
int calc(matrix x){
    int ret=0;
    for(int i=1;i<=m;++i)
        ret+=x.mat[i][i];
    return ret%mod;
}
int inv(int n){
    n%=mod;
    int b=mod-2,ret=1;
    while(b){
        if(b&1) ret=(ret*n)%mod;
        n=(n*n)%mod;
        b>>=1;
    }
    return ret;
}
int getans(int n){
    int ret=0;
    for(int i=1;i*i<=n;++i)
        if(n%i==0){
            ret=(ret+calc(basic^i)*getphi(n/i))%mod;
            if(i*i<n) ret=(ret+calc(basic^(n/i))*getphi(i))%mod;
        }
    return ret*inv(n)%mod;
}
signed main(){
    T=read();
    while(T--){
        n=read();m=read();k=read();
        for(int i=1;i<=m;++i)
            for(int j=1;j<=m;++j)
                basic.mat[i][j]=1;
        for(int i=1;i<=k;++i){
            a=read();b=read();
            basic.mat[a][b]=basic.mat[b][a]=0;
        }
        write(getans(n));
        puts("");
    }
    return 0;
}

小结

啊,终于水完了。
群论还是很有趣的
这篇文章(我们姑且这么称呼这一大摞字)主要涉及的是关于群论中Polya定理的证明、理解以及群论的常识。
在信息学竞赛中,群论的主要运用是Polya定理以及Burnside引理进行组合计数。而作为研究贪心算法的数学基础的拟阵在本文中毫无提及。
感谢现代群论的创始者迦罗瓦先生(Évariste Galois,1811-1832)。
本文的脉络思路主要参考了林厚从先生《信息学奥赛之数学一本通》(2016年版)关于群论一章的内容。该书的编者之一,闫书弈先生于NOI2017以627的高分夺rank1,%%%
这里甩一个课件
哈工程沈晶老师的PPT:(https://wenku.baidu.com/view/2c7a737b02768e9951e7387a.html?from=search)

  • 12
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值