原文地址:http://sighingnow.github.io/algorithm/polya_theory.html
概念及定理
首先是群的概念:
设
G
是一个集合,
- 封闭性:对于任何 a,b∈G 有 a∗b∈G ;
- 结合律:对任何 a,b,c∈G 有 (a∗b)∗c=a∗(b∗c) ;
- 单位元:存在 e∈G ,使得对所有的 a∈G ,都有 a∗e=e∗a=a ;
- 逆元:对于每个元素
a∈G
,存在
x∈G
,使得
a∗x=x∗a=e
,这个时候记
x
为
a−1 ,称为 a 的逆元,那么则称(G,∗) 为一个群。
例: G={0,1,2,3,4....n−1} ,那么它在 mod n 加法下是一个群。
群元素的个数有限,称为有限群,且其中元素的个数称为阶,记为 |G| ,群元素的个数无限,称为无限群。
若对于群元素中的任意两个元素 a,b 都有 ab=ba 那么称 G 为交换群,简称Abel群。
置换:设
例:设
排列,则称
同一置换用这样的表示法有
n!
种,但其对应的关系不变。
假设循环
π
只这样一个置换,满足
π:a1→a2,a2→a3,…ak→a1
,但是对于其他元素保持不变,即:
a→a
,可将
π
记为
称为
k
阶循环,
则可以表示为 (124)(35)(6) ,置换的循环节数是上面的循环个数,上面的例题的循环节数为 3 。
定义:设
X
的元素形成的等价类称为
构成若干个等价类,等价类的个数记为
L
。
Zk ( K 不动置换类):设G 是 1…n 的置换群。若 K 是1…n 中某个元素, G 中使K 保持不变的置换的全体,记以 Zk ,叫做 G 中
使K 保持不动的置换类,简称 K 不动置换类。Ek (等价类):设 G 是1…n 的置换群。若 K 是1…n 中某个元素, K 在G 作用下的轨迹,记作 Ek 。即 K 在G 的作用下所能
变化成的所有元素的集合。这个时候有: |Ek|∗|Zk|=|G| 成立 (k=1,2,.....n) 。C(π) :对于一个置换 π∈G ,及 a∈X ,若 π(a)=a ,则称 a 为
π 的不动点。 π 的不动点的全体记为 C(π) 。例如 π=(123)(3)(45)(6)(7),X={1,2,3,4,5,6,7} ;
那么 C(π)={3,6,7} 共 3 个元素。
Burnside引理:
L=1|G|∗(Z1+Z2+Z3+Z4+⋯+Zk).
L=L1|G|∗(C(π1)+C(π2)+C(π3)+⋯+C(πn)),k∈X,π∈G.Polya定理:
设 G={π1,π2,π3,…,πn} 是 X={a1,a2,a3,…,an} 上一个置换群,用 m 中颜色对
X 中的元素进行涂色,那么不同的涂色方案
数为:1|G|∗(mC(π1)+mC(π2)+mC(π3)+⋯+mC(πk))其中 C(πk) 为置换 πk 的循环节的个数。polya定理求循环节个数
const int MAXN=1001; int n, perm[MAXN], visit[MAXN]; //sum求循环节个数, Perm用来存储置换,即一个排列 int gcd(int n, int m) { return m==0?n:gcd(m,n%m); } void Polya() { int pos,sum=0; memset(visit, 0x00, sizeof(visit)); for(int i=0; i<n; i++) { if(!visit[i]) { sum++; pos=i; for(int j=0;!visit[perm[pos]];j++) { pos=perm[pos]; visit[pos]=1; } } } return sum; }
考虑旋转和翻转
1.旋转置换.
我们假设依次顺时针旋转 1∼n 个,则循环个数为 gcd(i,n) ;
2.翻转置换
当 n 为偶数时,分两种情况:
- 一种是中心轴在两个对称对象上,则循环个数为
n/2+1 ,这样的置换有 n/2 个。 - 另一种是对称轴两边分别有 n/2 个对象,则循环个数为 n/2 ,这样的置换也有 n/2 个。
- 一种是中心轴在两个对称对象上,则循环个数为
当 n 为奇数时,对称轴就只能在一个对象上,则循环个数为
n/2+1 。例题:POJ 2409
题目链接:POJ 2409: http://poj.org/problem?id=2409
A bracelet is a ring-like sequence of s beads each of which can have one of c distinct colors. The ring is closed, i.e. has no beginning or end, and has no direction. Assume an unlimited supply of beads of each color. For different values of s and c, calculate the number of different bracelets that can be made.
理解:给定颜色种数和环上的珠子总数,问有多少种染色方案(通过旋转和翻转相同的算同一种)。可见,是简单的考虑旋转和翻转的Polya计数模型。题解:
#include <cstdio> using namespace std; inline long long gcd(long long a, long long b) { return b==0?a:gcd(b, a%b); } long long llpow(long long a, long long b) { return b==0?1:llpow(a, b-1)*a; } long long polya(long long n, long long m) { long long sum = 0; for(int i = 1; i <= m; ++i) { sum += llpow(n, gcd(m, i)); } if(m & 1) { sum += m*llpow(n, (m>>1)+1); } else { sum += (m>>1)*llpow(n, (m>>1)+1) + (m>>1)*llpow(n, m>>1); } return sum/2/m; // 去掉翻转和旋转的重复 } int main(int argc, char **argv) { long long n, m; while(scanf("%lld%lld", &n, &m) != EOF && m+n > 0) { printf("%lld\n", polya(n, m)); } return 0; }
例题:POJ 2154
题目链接:POJ 2154: http://poj.org/problem?id=2154
题目大意:将正 n 边形的
n 个顶点用 n 种颜色染色,问有多少种方案(答案mod p ,且可由旋转互相得到的算一种)。分析:顺时针旋转 i 格的置换中,循环的个数为
gcd(i,n) ,每个循环的长度为 n/gcd(i,n) 。如果枚举旋转的格数 i ,复杂度显然较高。
因此,考虑不枚举i ,反过来枚举 L 。由于L|N ,枚举了 L ,再计算有多少个i 使得 0≤i≤n−1 并且 L=gcd(i,n) 。即 gcd(i,n)=n/L 。不妨设 a=nL=gcd(i,n) ,设 i=a∗t ,则当且仅当 gcd(L,t)=1 时,有:
Gcd(i,n)=gcd(a∗L,a∗t)=a
因为 0≤i<n ,所以 0≤t<na=L ,所以满足这个条件的 t 的个数为Euler(L) .欧拉函数:
ϕ(x)=x(1−1p1)(1−1p2)(1−1p3)…(1−1pk)其中 p1,p2,p3,…,pk 为 x 的所有质因数,
x 是不为 0 的整数。ϕ(n) 表示不超过 n 且与n 互素的正整数的个数。#include<iostream> #include<cstring> #include<cstdio> using namespace std; const int maxn = 36000; int n, mod, ans, prim[35000]; bool flag[maxn + 20]; void get_prim() { memset(flag, 0x00, sizeof (flag)); for(int i = 2; i <= 1000; i++) { if(!flag[i]) { for(int j = i * i; j <= maxn; j += i) { flag[j] = true; } } } for(int i = 2, k = 0; i <= maxn; i++) { if(!flag[i]) { prim[k++] = i; } } } int eular(int n) { // phi function. int i = 0, ans = 1; for(i = 0; prim[i] * prim[i] <= n; i++) { if(n % prim[i] != 0) { continue; } ans *= prim[i] - 1; n /= prim[i]; while(n % prim[i] == 0) { ans *= prim[i]; n /= prim[i]; } } if(n > 1) { ans *= n - 1; } return ans % mod; } int pow_mod(int c, int k, int mod) { int ans = 1; c = c % mod; while(k) { if(k & 1) { ans = (c * ans) % mod; } k >>= 1; c = (c * c) % mod; } return ans; } int main() { get_prim(); int i, T; scanf("%d", &T); while(T-- && scanf("%d%d", &n, &mod)) { ans = 0; for(i = 1; i * i <= n; i++) { if (i * i == n) { //枚举循环长度l,找出相应的i的个数:gcd(i,n)=n/l. ans = (ans + pow_mod(n, i - 1, mod) * eular(i)) % mod; } else if(n % i == 0) { //有长度为l的循环,就会有长度为n/l的循环。 ans = (ans + pow_mod(n, n / i - 1, mod) * eular(i) + eular(n / i) * pow_mod(n, i - 1, mod)) % mod; } } printf("%d\n", ans); } }
参考