等价类计数常见的环形模型

Burnside引理

把等价变换的所有置换列出,构成置换群。此时特别注意群的封闭性、且不重复不遗漏地覆盖等价类的所有的置换,这是最难的部分。然后求出每个置换分解为轮换的个数(轨道的个数),可以手推或用程序暴力计算(dfs或直接并查集合并连通分量)。那么本质不同的方案是群中每个置换下不变集合的 (染色时即 c^(每个置换的轨道数)) 的平均数。

Burnside引理

等价类计数

变换下的等价类个数的计数问题,考虑使用Burnside引理消除等价类条件,以一定复杂度的代价转换为普通计数问题。

其中非常常见的是n元环旋转(有时还有翻转)。

仅旋转时,群的阶为n,易观察知其中第i个置换(旋转i个单位)的轨道数为gcd(i,n)。即置换的轨道数d均为n的因子(也是稳定子轨道定理的结论)。根据欧拉函数的定义,还可得出轨道数为d的置换个数为φ(n/d)个。

因此,旋转的问题中,可在O(sqrt(n))的时间内枚举n的因子,再对因子算欧拉函数得到置换的个数。这一部分直接暴力计算一般已经足够快,此外还可以继续优化。

若还可对环进行翻转,增加以n条对称轴为对称中心的置换即可。这些翻转的置换都非常简单,只需要注意奇偶不同以及经过轴上的点不动。

poj 1286 / poj 2409

基本的环旋转、翻转,裸的Polya定理。下为poj 1286代码。

  1. /*
  2. Author: fffasttime
  3. Date:
  4.  */
  5. #include <iostream>
  6. #include <cstdio>
  7. #include <cmath>
  8. #include <algorithm>
  9. using namespace std;
  10. typedef long long ll;
  11. #define inc(i,n) for (int i=0;i<n;i++)
  12. const int N=100010;
  13.  
  14. typedef long long ll;
  15.  
  16. ll po(ll a, ll x){
  17. 	ll ans=1;
  18. 	while (x--) ans*=a;
  19. 	return ans;
  20. }
  21.  
  22. int main(){
  23. 	int n; while (cin>>n && n!=-1){
  24. 		if (n==0) {cout<<0<<'\n';continue;}
  25. 		ll sum=0;
  26. 		for (int i=0;i<n;i++) sum+=po(3,__gcd(i,n));
  27. 		if (!(n&1)){
  28. 			sum+=n/2*po(3,n/2+1);
  29. 			sum+=n/2*po(3,n/2);
  30. 		}
  31. 		else{
  32. 			sum+=n*po(3,n/2+1);
  33. 		}
  34. 		cout<<sum/n/2<<'\n';
  35. 	}
  36. 	return 0;
  37. }

poj 2154 / luogu 【模板】Polya定理

就是环旋转的情况,使用上述的一点数论优化一下。

  1. /*
  2. Author: fffasttime
  3. Date:
  4.  */
  5. #include <iostream>
  6. #include <cstdio>
  7. #include <cmath>
  8. #include <algorithm>
  9. using namespace std;
  10. typedef long long ll;
  11. #define inc(i,n) for (int i=0;i<n;i++)
  12. const int N=100010;
  13.  
  14. ll a[110];
  15.  
  16. bool np[40000];
  17. int pp[10000],ppc;
  18.  
  19. void init(){
  20. 	for (int i=2;i<40000;i++){
  21. 		if (!np[i]) pp[ppc++]=i;
  22. 		for (int j=i+i;j<40000;j+=i)
  23. 			np[j]=1;
  24. 	}
  25. }
  26.  
  27. ll qpow(ll a, ll x, ll p){
  28. 	ll ans=1;
  29. 	for (;x;a=a*a%p,x>>=1)
  30. 		if (x&1) ans=ans*a%p;
  31. 	return ans;
  32. }
  33.  
  34. int p[20],pc;
  35. ll getphi(int x){
  36. 	ll ans=1;
  37. 	for (int i=0;i<pc;i++){
  38. 		if (p[i]*p[i]>x) break;
  39. 		if (x%p[i]==0){
  40. 			x/=p[i]; ans=ans*(p[i]-1);
  41. 			while (x%p[i]==0) x/=p[i],ans=ans*p[i];
  42. 		}
  43. 	}
  44. 	if (x>1) ans=ans*(x-1);
  45. 	return ans;
  46. }
  47. ll P;
  48. int main(){
  49. 	int T; cin>>T;
  50. 	init();
  51. 	while (T--){
  52. 		int n0; cin>>n0>>P; int n=n0, r=sqrt(n+1);
  53. 		pc=0;
  54. 		for (int j=0;pp[j]<=r;j++){
  55. 			int i=pp[j];
  56. 			if (n%i==0) {
  57. 				p[pc++]=i;
  58. 				while (n%i==0) n/=i;
  59. 			}
  60. 		}
  61. 		if (n>1) p[pc++]=n;
  62. 		n=n0; ll ans=0;
  63. 		for (int i=1;i<=r;i++) if (n%i==0){
  64. 			ans+=getphi(n/i)*qpow(n,i-1,P);
  65. 			if (i*i<n) ans+=getphi(i)*qpow(n,n/i-1,P);
  66. 			ans%=P;
  67. 		}
  68. 		//cout<<ans<<'\n';
  69. 		cout<<ans<<'\n';
  70. 	}	
  71. 	return 0;
  72. }

hdu 1812

也是Polya模板题,这题不是环旋转而是方阵旋转、翻转,不过也比较简单。旋转、翻转共8个置换,手推一下即可。

  1. Huge qpow(Huge a, int x){
  2. 	Huge ans; ans.set("1");
  3. 	for (;x;a*=a,x>>=1)
  4. 		if (x&1) ans*=a;
  5. 	return ans;
  6. }
  7.  
  8. int main(){
  9. 	int n;
  10. 	char c0[10];
  11. 	while (cin>>n>>c0){
  12. 		Huge sum; sum.set("0");
  13. 		Huge c; c.set(c0);
  14. 		Huge s2,s3; s2.set("2"); s3.set("3");
  15. 		sum+=qpow(c,n*n);
  16. 		if (n&1){
  17. 			sum+=s2*qpow(c,n*n/4+1);
  18. 			sum+=qpow(c,n*n/2+1);
  19. 			sum+=s2*s2*qpow(c,(n*n+n)/2);
  20. 		}
  21. 		else{
  22. 			sum+=s2*qpow(c,n*n/4);
  23. 			sum+=s3*qpow(c,n*n/2);
  24. 			sum+=s2*qpow(c,(n*n+n)/2);
  25. 		}
  26. 		sum.d2(); sum.d2(); sum.d2();
  27. 		cout<<sum<<'\n';
  28. 	}
  29. 	return 0;
  30. }

poj 2888 带限制的等价类计数

同样是旋转变换视为等价。但这里有相邻不能取某些颜色的限制。因此不能再用Polya定理,只能直接用Burnside引理。根据Burnside引理,我们需要直接计算各个置换下不动的染色方案数。此时第i个旋转的轨道数仍为gcd(i,n),轨道长度d=n/gcd(i,n)。考虑原先使用Polya定理时,c种染色不带限制,即长度为d的段可以任意染色,共c^d种方案。而此时可把长度为d的段取出,而段的首尾也需要满足限制条件,可视为长度为d的环。

于是问题转化为:对长度为d的环染c种颜色,某些颜色不能相邻,求方案数,旋转视为不同方案。这是一类比较经典的矩阵加速递推的计数问题,与https://www.luogu.org/problem/P1357类似。该递推还可使用路径计数解释:以颜色为节点,相邻合法的颜色连一条边。则邻接矩阵的k次幂即在图上走k步,选出长度为k的合法染色。长度为d的环即走d步后回到自身,即邻接矩阵d次方对角线的和。

这里枚举置换还是跟上面一样的套路,所以总复杂度乘上子问题的复杂度,O(sqrt(n) m^3 log(n))。

  1. /*
  2. Author: fffasttime
  3. Date:
  4.  */
  5. #include <iostream>
  6. #include <cstdio>
  7. #include <cmath>
  8. #include <algorithm>
  9. #include <cstring>
  10. using namespace std;
  11. typedef long long ll;
  12. #define inc(i,n) for (int i=0;i<n;i++)
  13. const int N=100010;
  14.  
  15. const int P=9973;
  16.  
  17. int qpow(int a, int x){
  18. 	int ans=1;
  19. 	a%=P;
  20. 	for(;x;a=a*a%P,x>>=1)
  21. 		if (x&1) ans=ans*a%P;
  22. 	return ans;
  23. }
  24.  
  25. int n,m,k;
  26.  
  27. struct Mat{
  28. 	int m[11][11];
  29. 	void cl(){memset(m,0,sizeof m);}
  30. 	void I(){cl();inc(i,n)m[i][i]=1;}
  31. }b;
  32.  
  33. Mat operator*(const Mat &a, const Mat &b){
  34. 	Mat c; c.cl();
  35. 	inc(i,n) inc(j,n) inc(k,n)
  36. 		c.m[i][j]+=a.m[i][k]*b.m[k][j],c.m[i][j]%=P;
  37. 	return c;
  38. }
  39. Mat qpow(Mat a, int x){
  40. 	Mat ans; ans.I();
  41. 	for (;x;a=a*a,x>>=1)
  42. 		if (x&1) ans=ans*a;
  43. 	return ans;
  44. }
  45. int calc(int d){
  46. 	Mat a=b;
  47. 	Mat ans=qpow(a,d);
  48. 	int ret=0;
  49. 	inc(i,n) ret+=ans.m[i][i],ret%=P;
  50. 	return ret;
  51. }
  52.  
  53. int phi(int n){
  54. 	int ans=n;
  55. 	for (int i=2;i*i<=n;i++)
  56. 		if (n%i==0){
  57. 			ans=ans/i*(i-1);
  58. 			while (n%i==0) n/=i;
  59. 		}
  60. 	if (n>1) ans=ans/n*(n-1);
  61. 	return ans%P;
  62. }
  63.  
  64. int main(){
  65. 	int T; cin>>T;
  66. 	while (T--){
  67. 		cin>>m>>n>>k;
  68. 		inc(i,n) inc(j,n) b.m[i][j]=1;
  69. 		inc(i,k){
  70. 			int u,v; scanf("%d%d",&u,&v);
  71. 			u--; v--;
  72. 			b.m[u][v]=0;
  73. 			b.m[v][u]=0;
  74. 		}
  75. 		int ans=0;
  76. 		for (int i=1;i*i<=m;i++) if(m%i==0){
  77. 			ans+=phi(m/i)*calc(i);
  78. 			if (i*i!=m) ans+=phi(i)*calc(m/i);
  79. 			ans%=P;
  80. 		}
  81. 		printf("%d\n",ans*qpow(m,P-2)%P);
  82. 	}
  83. 	return 0;
  84. }

hdu 2481 轮图生成树

这题除了增加了旋转等价的条件外,几乎和bzoj 1002轮状病毒一样,原问题递推为a[i]=3*a[i-1]-a[i-2]+2。有了上一题的套路,把bzoj 1002作为子问题即可。注意这里没有逆元,必须使用a/b%c=a%bc/b的方式计算。

  1. /*
  2. Author: fffasttime
  3. Date:
  4.  */
  5. #include <iostream>
  6. #include <cstdio>
  7. #include <cmath>
  8. #include <algorithm>
  9. #include <cstring>
  10. using namespace std;
  11. typedef long long ll;
  12. #define inc(i,n) for (int i=0;i<n;i++)
  13. const int N=100010;
  14.  
  15. ll P=100000;
  16. ll qmul(ll x,ll y){
  17. 	ll t=(x*y-(ll)((long double)x/P*y+0.5)*P);
  18. 	return t<0 ? t+P : t;
  19. }
  20.  
  21. ll qpow(ll a, ll x){
  22. 	ll ans=1;
  23. 	a%=P;
  24. 	for(;x;a=qmul(a,a),x>>=1)
  25. 		if (x&1) ans=qmul(ans,a);
  26. 	return ans;
  27. }
  28.  
  29. ll n;
  30.  
  31. struct Mat{
  32. 	ll m[3][3];
  33. 	void cl(){memset(m,0,sizeof m);}
  34. 	void I(){cl();inc(i,3)m[i][i]=1;}
  35. };
  36. Mat b={{{3,-1,2},{1,0,0},{0,0,1}}};
  37.  
  38. Mat operator*(const Mat &a, const Mat &b){
  39. 	Mat c; c.cl();
  40. 	inc(i,3) inc(j,3) inc(k,3)
  41. 		c.m[i][j]+=qmul(a.m[i][k],b.m[k][j]),c.m[i][j]%=P;
  42. 	return c;
  43. }
  44. Mat qpow(Mat a, ll x){
  45. 	Mat ans; ans.I();
  46. 	for (;x;a=a*a,x>>=1)
  47. 		if (x&1) ans=ans*a;
  48. 	return ans;
  49. }
  50. ll calc(int d){
  51. 	d--;
  52. 	Mat ans=qpow(b,d);
  53. 	ll ret=ans.m[0][0]+ans.m[0][2];
  54. 	return ret%P;
  55. }
  56. ll phi(int n){
  57. 	ll ans=n;
  58. 	for (ll i=2;i*i<=n;i++)
  59. 		if (n%i==0){
  60. 			ans=ans/i*(i-1);
  61. 			while (n%i==0) n/=i;
  62. 		}
  63. 	if (n>1) ans=ans/n*(n-1);
  64. 	return ans%P;
  65. }
  66.  
  67. int main(){
  68. 	//for (int i=1;i<10;i++) cout<<calc(i)<<'\n';
  69. 	while (cin>>n>>P){
  70. 		P*=n;
  71. 		ll ans=0;
  72. 		for (int i=1;i*i<=n;i++) if(n%i==0){
  73. 			ans+=qmul(phi(n/i),calc(i));
  74. 			if (i*i!=n) ans+=qmul(phi(i),calc(n/i));
  75. 			ans%=P;
  76. 		}
  77. 		//cout<<ans<<'\n';
  78. 		printf("%lld\n",ans/n);
  79. 	}
  80. 	return 0;
  81. }

spoj 422 矩阵转置交换次数

这题最大的困难之处在于它的置换特征非常不明显,需要仔细分析。由于矩阵长宽为2的幂次,首先寻找转置前后数组下标的二进制表示有什么特点。实际上是把当前下标二进制表示下a+b前后两段互换。而此交换可视为把一个01环向左循环旋转a个单位。我们需要求的是最少交换次数,对矩阵而言,一个01串可视为对矩阵一个元素的变换。由于一个轮换r需要r-1次交换操作才能完成,我们要求的答案就是总01串个数-轮换个数(即有多少个循环)。

然后注意到这样的轮换(轨道)个数即在左旋转a意义下等价的01串个数,也就是Polya定理要求的东西。想到这一点后就和上面的模板题差不太多了。这里旋转群的旋转间隔为gcd(a,a+b),旋转置换群的阶为(a+b)/gcd(a,a+b)。唯一卡人的地方是这里n只有1e6而询问非常多,需要nlogn筛出n所有的因子,O(d(n))回答,d为因子个数函数。1e6内的d(n)不超过240,而平均只有10~20,因此不会超时。

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. typedef long long ll;
  4.  
  5. const int P=1000003;
  6.  
  7. ll qpow(ll a, ll x){
  8. 	ll ans=1;
  9. 	for (;x;a=a*a%P,x>>=1)
  10. 		if (x&1) ans=ans*a%P;
  11. 	return ans;
  12. }
  13.  
  14. const int maxn=1000010;
  15. bool isp[maxn];
  16. int p[maxn],phi[maxn],pc,pow2[maxn];
  17. int fac[15000010],nxt[15000010],facc;
  18. int head[maxn];
  19. void add(int x, int d){
  20. 	nxt[facc]=head[x];
  21. 	fac[facc]=d;
  22. 	head[x]=facc++;
  23. }
  24.  
  25. void gennumfunc(){
  26. 	memset(isp,1,sizeof(isp));
  27. 	isp[1]=0;  phi[1]=1;
  28. 	for (int i=2;i<maxn;i++){
  29. 		if (isp[i]) p[pc++]=i,phi[i]=i-1;
  30. 		for (int j=0;j<pc && i*p[j]<maxn;j++){
  31. 			isp[i*p[j]]=0;
  32. 			if (i%p[j]==0){
  33. 				phi[i*p[j]]=phi[i]*p[j];
  34. 				break;
  35. 			}
  36. 			phi[i*p[j]]=phi[i]*(p[j]-1); //f(ab)=f(a)f(b), when (a,b)=1
  37. 		}
  38. 	}
  39. 	pow2[0]=1;
  40. 	for (int i=1;i<maxn;i++) pow2[i]=pow2[i-1]*2%P;
  41. 	for (int i=1;i<maxn;i++)
  42. 		for (int j=i;j<maxn;j+=i)
  43. 			add(j,i);
  44. }
  45. int main(){
  46. 	gennumfunc();
  47. 	//cout<<clock()<<'\n';
  48. 	int T; cin>>T;
  49. 	while (T--){
  50. 		ll a,b; scanf("%lld%lld",&a,&b); 
  51. 		if (a*b==0){
  52. 			puts("0");
  53. 			continue;
  54. 		}
  55. 		ll g=__gcd(a,a+b);
  56. 		ll n=(a+b)/g;
  57. 		ll ans=0;
  58. 		for (int e=head[n];e;e=nxt[e]){
  59. 			int d=fac[e];
  60. 			ans+=1ll*pow2[d*g]*phi[n/d]%P;
  61. 			ans%=P;
  62. 		}
  63. 		printf("%lld\n",(qpow(2,n*g)+P-ans*qpow(n,P-2)%P)%P);
  64. 	}	
  65. 	return 0;
  66. }

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注