初识群论-总结
知识
Update:2022.1.1 改变了一些自己在群论的理解,使得定义更加平易近人
首先群的概念极其重要,说白了就是一个封闭集合,
可以通过某种二元运算可以得到的元素都在这里面,
再其次就是子群,这个和自己完全不一样,因为子群自己也是封闭的
所以这样的话,一个群一定有两个子群,一个单位元,一个就是自己
左右陪集的话,主要是二元运算符的运算顺序会影响结果,
这个东西在阿贝尔群中,左右陪集是完全相同的,因为二元运算符满足交换律。。。
还有置换,就是一个排列里面的数,一一对应另外一个排列里面的数,对应过去就是了
但是这样的排列不一定是全排列,只要封闭就可以
置换群就是一堆不同的置换所组成的群
因为置换的过程是排列的一一对应,所以每一个置换必然能表示为一堆置换的加和
就是我们可以对置换进行拆分,而拆分出来的块又是一个小的置换,但是数字不连续
因为可以被分出来,所以这些置换根据对应关系可以变成一个一个的环
比如说1->2,2->1,这样1,2就组成了一个环,还可以连接更多的数,组成更长的环
然而这还不是重点,好博客
burnside引理
对于一个序列或者环求它在某个置换群的意义下的不同方案数
答案就是\(\displaystyle\frac{1}{|G|}\sum_{g\in G}M(g)\)
其中 G 是所有置换的集合,M(g) 是一个置换的不动点个数。
所谓不动点就是在当前的置换的条件下,所有的点,经过这个置换之后,类型不变,也可以说颜色不变
所以说,不动点并不是在这个置换下不变的点,而是在这个置换下可以让所有点类型不变的染色方案
个数就是这种方案的个数。。。。。
当然还有更加学术的表达,那个就太恶心了,所以我用了这种表达,学术的
这个一般是利用dp去求每一个置换中的不动点个数,所以这种题的难点不在引理
而是在如何求这个不动点个数。。。
拉格朗日定理简单来说就是一个群的子群的大小是这个群的大小的约数
也就是子群的阶整除当前群的阶
后面证明就不会了
polya定理
这个极其简单。。。。就是在burnside的基础上简化了计算不动点个数的方法
前面说到对于每一个置换都可以拆成一个一个的环,
那既然是不动点,这些环中的每一个点的类型一定要相同才行,
不然置换之后就一定不相同,就不是不动点了。。
而polya定理就是针对这个来引入的一个定理
当这些环之间没有限制的时候,我们一共有m中类型可以选择
那么不动点的个数就是\(m^s\),s表示环的个数
每一个环内都有m种选择,那么直接乘起来就是答案了
这个定理的应用范围极其狭窄,所以要看好到底有没有限制
题目
luogu polya大板子
就是裸的polya
然后不要直接去枚举每一种置换,枚举gcd,然后用\(\varphi\)来计数,简单爆了\
code
#include<bits/stdc++.h>
using namespace std;
#define re register int
#define ll long long
const ll mod=1e9+7;
ll t,n,ans;
ll ksm(ll x,ll y){
ll ret=1;
while(y){
if(y&1)ret=ret*x%mod;
x=x*x%mod;
y>>=1;
}
return ret;
}
ll gcd(ll x,ll y){return y?gcd(y,x%y):x;}
ll phi(ll x){
ll ret=x;
for(re i=2;i*i<=x;i++){
if(x%i)continue;
ret=ret/i*(i-1);
while(x%i==0)x/=i;
}
if(x>1)ret=ret/x*(x-1);
return ret;
}
signed main(){
scanf("%lld",&t);
while(t--){
scanf("%lld",&n);ans=0;
for(re i=1;i*i<=n;i++){
if(n%i)continue;
ans=(ans+1ll*ksm(n,i)*phi(n/i)%mod)%mod;
if(i!=n/i)ans=(ans+1ll*ksm(n,n/i)*phi(i)%mod)%mod;
}
printf("%lld\n",ans*ksm(n,mod-2)%mod);
}
}
Cards
这个不能用polya,因为有数量限制,所以只能dp的去求不动点个数
循环节都给你了好吧,直接找,\(\mathcal{O(n)}\)的
因为是数量限制嘛,每个循环又有一个体积,所以就是三维的背包,挺简单的
逆元就直接快速幂就好了
code
#include<bits/stdc++.h>
using namespace std;
#define re register int
const int N=25;
int sr,sb,sg,n,m,mod;
int f[N][N][N],ans;
int nex[N*3],fo[N*3],cnt;
bool vis[N*3];
int ksm(int x,int y){
int ret=1;
while(y){
if(y&1)ret=1ll*ret*x%mod;
x=1ll*x*x%mod;
y>>=1;
}
return ret;
}
int get_ans(){
memset(f,0,sizeof(f));
memset(vis,false,sizeof(vis));
f[0][0][0]=1;cnt=0;
for(re i=1;i<=n;i++){
if(!vis[i]){
int now=i,tmp=0;
while(!vis[now])
vis[now]=true,now=nex[now],tmp++;
fo[++cnt]=tmp;
}
}
//cout<<cnt<<" "<<fo[cnt]<<endl;
for(re i=1;i<=cnt;i++)
for(re ri=sr;~ri;ri--)
for(re bi=sb;~bi;bi--)
for(re gi=sg;~gi;gi--){
if(ri>=fo[i])f[ri][bi][gi]=(f[ri][bi][gi]+f[ri-fo[i]][bi][gi])%mod;
if(bi>=fo[i])f[ri][bi][gi]=(f[ri][bi][gi]+f[ri][bi-fo[i]][gi])%mod;
if(gi>=fo[i])f[ri][bi][gi]=(f[ri][bi][gi]+f[ri][bi][gi-fo[i]])%mod;
//cout<<ri<<" "<<bi<<" "<<gi<<" "<<f[ri][bi][gi]<<endl;
}
return f[sr][sb][sg];
}
signed main(){
scanf("%d%d%d%d%d",&sr,&sb,&sg,&m,&mod);
n=sr+sb+sg;
for(re i=1;i<=m;i++){
for(re j=1;j<=n;j++)scanf("%d",&nex[j]);
ans=(ans+get_ans())%mod;
}
for(re i=1;i<=n;i++)nex[i]=i;ans=(ans+get_ans())%mod;
printf("%lld",1ll*ans*ksm(m+1,mod-2)%mod);
}
周末晚会
这个限制只是对女生来说的,但也是有限制,直接考虑dp就行了
这个dp还是非常的难想,真的,我直接看题解去了。。。。拍一拍就过了
首先我们要知道,我们找到的循环个数就是d=gcd(n,i),所以整个环就是d的循环
所以我们只需要保证d的合法性,又因为d是循环的,所以首尾也要有限制
我们要构造一个dp,来做这个题,要保证长度为d的环上没有连续的超过k个女生
我们设女生为0,男生为1,设dp[i][j]表示前j个数中有不超过i个连续的0,
啊,我错了,在这个题中,i=k,然后第一维就不需要了
我们钦定第一个和最后一个都是1,中间的爱咋咋地,所以我们转移的时候直接枚举就行了
但是这好像是\(\mathcal{O(n^2)}\)的,但是你发现转移的过程是连续的,前缀和优化
然后我们有了这个dp数组,我们求答案就很方便了
我们当前一共有d个位置,对于i个数来说,一共有d-i种放置的办法,但是放之前,看看d-i是否>=k
然后就按照之前的套路转移就好了。
所以这个题的模数是1e8+7,wocnmd
code
#include<bits/stdc++.h>
using namespace std;
#define re register int
#define ll long long
const ll mod=1e8+7;
const int N=2005;
ll T,n,k;
ll f[N],fro[N],ans;
ll gcd(ll x,ll y){return y?gcd(y,x%y):x;}
ll ksm(ll x,ll y){
ll ret=1;
while(y){
if(y&1)ret=ret*x%mod;
x=x*x%mod;
y>>=1;
}
return ret;
}
signed main(){
scanf("%lld",&T);
while(T--){
scanf("%lld%lld",&n,&k);
memset(f,0,sizeof(f));
f[1]=1;fro[1]=1;ans=0;
for(re i=2;i<=n;i++){
f[i]=(f[i]+fro[i-1]-fro[max(i-k-2,0ll)]+mod)%mod;
fro[i]=(fro[i-1]+f[i])%mod;
//cout<<f[i]<<" ";
}
//cout<<endl;
for(re i=0;i<n;i++){
ll d=gcd(n,i);
for(re j=max(d-k,1ll);j<=d;j++)
ans=(ans+f[j]*(d-j+1)%mod)%mod;
if(n<=k)ans=(ans+1)%mod;
}
printf("%lld\n",ans*ksm(n,mod-2)%mod);
}
}
有色图
色图到底在哪里????-----小粉兔
这个好难好难,我看题解看了1h30min,好好理解一下
首先题目中给的是关于点的置换,而我们要看的却是边的置换,如何做??
转换呗,人家都给你完全图了,给你多大的方便,你还不会???
我们设\(b_i\)为一个置换中点的循环长度,就是等价类的大小
那么所有的边会被分成两类,一类是两个端点在同一个类中,一类是不同类中
对于在同一类中的(第一类),我们会发现,所有边的等价类中的边的长度一定相同,
所谓长度就是他连接的连接个端点之间跨越了几个点,
那么每旋转\(\lfloor\frac{b_i}{2}\rfloor\),就会回到原来的位置。这个可以分奇偶来讨论
当\(b_i\)为奇数的时候,直接旋转\(\frac{b_i-1}{2}\)就可以回到原来的地方
当\(b_i\)为偶数的时候,中间会多一次,上下正好反过来,就是\(\frac{b_i}{2}\)
对于在不同类当中的(第二类),两两组合,然后发现循环的长度(等价类的大小)就是\(lcm(b_i,b_j)\)
这样的话我们对于每一个置换就有了每一个置换对应的答案
但是我们直接枚举是不可能的
我们对每一个点分配等价类,那就是一个可重集排列\(\displaystyle\frac{n!}{\prod b_i!}\)
对于每一个等价类中,我们还有一个圆排列种方案\(\prod (b_i-1)!\),乘起来就是\(\displaystyle\frac{n!}{\prod b_i}\)
但是我们的等价类是没有顺序之分的,万一有5个数分到了1,有5个数分到了2,这个是可以互换的,不能算作多种方案
所以我们计算每一种长度的个数为\(c_i\),表示在当前置换的等价类中,大小为i的有\(c_i\)个
那就要除去\(\prod(c_i)\),于是式子变成了\(\displaystyle\frac{n!}{\prod b_i\prod c!}\)
就直接利用这些求就完事了
code
#include<bits/stdc++.h>
using namespace std;
#define re register int
const int N=55;
const int M=1005;
int n,m,mod;
int jc[N],ans;
int ksm(int x,int y){
int ret=1;
while(y){
if(y&1)ret=1ll*ret*x%mod;
x=1ll*x*x%mod;y>>=1;
}
return ret;
}
int gcd(int x,int y){return y?gcd(y,x%y):x;}
int ji[N];
void get_ans(int sum){
int mul=1,top=0,zs=0;
for(re i=1;i<=sum;i++)zs=(zs+ji[i]/2)%mod;
for(re i=1;i<=sum;i++)
for(re j=i+1;j<=sum;j++)
zs=(zs+gcd(ji[i],ji[j]))%mod;
for(re i=1;i<=sum;i++)mul=1ll*mul*ji[i]%mod;
for(re i=2;i<=sum;i++){
top++;if(ji[i]==ji[i-1])continue;
mul=1ll*mul*jc[top]%mod;top=0;
}top++;
mul=1ll*mul*jc[top]%mod;
ans=(ans+1ll*jc[n]*ksm(mul,mod-2)%mod*ksm(m,zs)%mod)%mod;
}
void dfs(int dep,int rest,int mn){
if(!rest){get_ans(dep-1);return ;}
for(re i=mn;i<=rest;i++)
ji[dep]=i,dfs(dep+1,rest-i,i);
}
signed main(){
scanf("%d%d%d",&n,&m,&mod);
jc[0]=1;for(re i=1;i<=n;i++)jc[i]=1ll*jc[i-1]*i%mod;
dfs(1,n,1);ans=1ll*ans*ksm(jc[n],mod-2)%mod;
printf("%d",ans);
}
[POJ2154]Color
所以这个和板子题一模一样,然后我被卡常好久,把long long改成int就A了
主要是这个模数不是质数,所以我们不找逆元了,直接在乘的时候少乘一个就好了
code
#include<cstdio>
using namespace std;
#define re register int
#define ll int
ll t,n,ans,mod;
ll ksm(ll x,ll y){
ll ret=1;
while(y){
if(y&1)ret=1ll*ret*x%mod;
x=1ll*x*x%mod;
y>>=1;
}
return ret;
}
ll gcd(ll x,ll y){return y?gcd(y,x%y):x;}
ll phi(ll x){
ll ret=x;
for(re i=2;i*i<=x;i++){
if(x%i)continue;
ret=ret/i*(i-1);
while(x%i==0)x/=i;
}
if(x>1)ret=ret/x*(x-1);
return ret;
}
signed main(){
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&mod);ans=0;
for(re i=1;i*i<=n;i++){
if(n%i)continue;
ans=(ans+1ll*ksm(n,i-1)*phi(n/i)%mod)%mod;
if(i!=n/i)ans=(ans+1ll*ksm(n,n/i-1)*phi(i)%mod)%mod;
}
printf("%d\n",ans);
}
}
[POJ2888]Magic Bracelet
所以这个就是矩阵乘优化dp转移???
还是找gcd,然后枚举循环节大小
我们先写出转移方程,dp[i][j]表示前i个位置,第i个位置的颜色是j,不会爆炸的方案数
我们只能一步一步的转移,只有n个数,我们从0开始,到n结束,所以0和n是同一个点
最后要颜色相同的答案,所以你发现转移的时候的系数就是会不会爆炸
而每次转移的系数都是相同的,直接矩阵乘
code
#include<cstdio>
#include<cstring>
using namespace std;
#define re register int
#define ll int
const int M=15;
const ll mod=9973;
ll T,n,m,q,ans;
struct matrix{
ll x[M][M];
matrix(){memset(x,0,sizeof(x));}
matrix operator * (matrix b)const{
matrix c;
for(re i=1;i<=m;i++)
for(re j=1;j<=m;j++)
for(re k=1;k<=m;k++)
c.x[i][j]=(c.x[i][j]+x[i][k]*b.x[k][j])%mod;
return c;
}
}a;
ll kpo(ll x,ll y){
ll ret=1;x%=mod;
while(y){
if(y&1)ret=ret*x%mod;
x=x*x%mod;
y>>=1;
}
return ret;
}
ll ksm(matrix x,ll y){
matrix ret;ll res=0;
for(re i=1;i<=m;i++)ret.x[i][i]=1;
while(y){
if(y&1)ret=ret*x;
x=x*x;y>>=1;
}
for(re i=1;i<=m;i++)
res=(res+ret.x[i][i])%mod;
return res;
}
ll phi(ll x){
ll ret=x;
for(re i=2;i*i<=x;i++){
if(x%i)continue;
ret=ret/i*(i-1);
while(x%i==0)x/=i;
}
if(x>1)ret=ret/x*(x-1);
return ret;
}
signed main(){
scanf("%d",&T);
while(T--){
ans=0;
scanf("%d%d%d",&n,&m,&q);
for(re i=1;i<=m;i++)
for(re j=1;j<=m;j++)
a.x[i][j]=1;
for(re i=1,x,y;i<=q;i++){
scanf("%d%d",&x,&y);
a.x[x][y]=a.x[y][x]=0;
}
for(re i=1;i*i<=n;i++){
if(n%i)continue;
ans=(ans+ksm(a,i)*(phi(n/i)%mod))%mod;
if(i*i!=n)ans=(ans+ksm(a,n/i)*(phi(i)%mod))%mod;
}
printf("%d\n",ans*kpo(n,mod-2)%mod);
}
}