【burnside & polya】hnoi2008 cards hnoi2009 count

 


      寒假的时候被陈老师讲的组合数学死去活来,后来再去看一次仍然没看懂,今天又看了一次,终于看懂了(不容易啊)。

     burnside:

     说的通俗点, 定义一个置换,即每个状态i in [1, n], 置换后变成P[ i ], P[ i ] 可以等于 i, 那么一个置换可以把n个状态转化为另一顺序的n个状态, 所有的置换构成一个集合,如果该集合的所有置换满足群的性质,那么该集合是一个置换群。

      一个置换可以写成若干个不相交循环的并,一个循环(x1, x2...xk)表示x1 变成 x2, x2变成x3....xk变成x1, 该置换用a表示,定义c1(a)为a置换转化为循环的乘积后,长度为1的循环的个数。

     那么L = (c1(a1) + c1(a2) +...+c1(ag))/|G|, a1~ag表示G置换群中的每个置换, |G|表示该置换群的大小,而L就是我们梦寐以求的在通过所有置换仍然不相等的 状态数。


    那么hnoi2008 cards作为一个裸模型题就出来了,有n个卡片,染三种颜色,数目分别为sr, sb, sg, 并给定一个置换群,问在置换作用下,不相同的染法有多少个。

     状态 ——— 一种染色方案

     很多变换方式————置换群

      不相同染法————不相等的状态数,即等价类个数

      那么可以直接套用burnside定理,我们只需要求出每一个置换的c1, 这个需要联系polya和burnside的关系,具体做法是对于该置换对于单独卡片的作用(注意,burnside中的置换是指对方案进行置换),可以得到若干循环,如果对循环内部的点染相同颜色,染色方案就是算法中的c1。

     然后直接计算。

     

# include <cstdlib>
# include <cstdio>
# include <cmath>
# include <cstring>

using namespace std;

const int maxn = 62;
int ans, sum[maxn],  id[maxn], st[maxn], g[maxn];
int f[maxn][maxn][maxn];
bool step[maxn];
int n, m, p, sr, sb, sg;

int dfs(int x)
{
	step[x] = true;
	if (step[g[x]]) return 1;
	return dfs(g[x])+1;
}
int mul(int u, int k)
{
	int ask = 1;
	for (;k;k>>=1, u=u*u% p)
	 if (k&1) ask = ask * u % p;
	return ask;
}
int main()
{
	int i, j, q, rr, bb, gg;
	freopen("cards.in", "r", stdin);
	freopen("cards.out", "w", stdout);
	scanf("%d%d%d%d%d", &sr, &sb, &sg, &m, &p);
	n = sr+ sb+ sg; m++;
	for (i = 1; i <= m; i++)
	{
		if (i == m) for (j = 1; j <= n; j++) g[j] = j;
		else  for (j = 1; j <= n; j++)  scanf("%d", &g[j]);
		st[0] = 0; memset(step,0,sizeof(step));
		memset(f, 0, sizeof(f)); memset(id, 0, sizeof(id));
		for (j = 1; j <= n; j++)
		  if (!step[j]) st[++st[0]] = dfs(j);
		for (j = 1; j <= st[0]; j++) sum[j] = sum[j-1]+st[j], id[sum[j]] = j;
		f[0][0][0] = 1;
		for (rr = 0; rr <= sr; rr++)
		  for (bb = 0; bb <= sb; bb++)
		    for (gg = 0; gg <= sg; gg++)
		    if (q = id[rr+bb+gg])
		    {
				if (rr >= st[q]) (f[rr][bb][gg] += f[rr-st[q]][bb][gg]) %= p;
				if (bb >= st[q]) (f[rr][bb][gg] += f[rr][bb-st[q]][gg]) %= p;
				if (gg >= st[q]) (f[rr][bb][gg] += f[rr][bb][gg-st[q]]) %= p;
			}
		ans = (ans + f[sr][sb][sg]) % p;
	}
	ans = ans * mul(m, p-2) % p;
    printf("%d", ans);
    return 0;
}

   

polya:

   burnside 中每一个置换是相对于状态而言的,而题目的状态量是一个很大的问题,而polya可以转化为状态内部的元素的关系。

   如果染的颜色没有数量限制,polya成立

   假设m为可以染的颜色

   L = (m^c(a1) + m^c(a2) + ...+m^c(ag))|G|, 这里的L仍然是上边的L,即状态的等价类个数,但是这里的G中的置换是对于一个状态中的每个元素的置换,c表示置换a的循环个数!

   由于状态数往往远大于某个状态的元素个数,所以polya往往效率更高。


   在看hnoi2009 count, 无向图的同构计数。

   在图中出现的边,染1,不出现染0, 求置换后边集染色不同的图。

   这道题的置换可以对于点,对于边,对于整个图。。。。。。

   如果我们使用burnside,那么我们的置换的对象是图,图的数量太大了,kill it

   那么只能使用polya,那么置换对象是边,小点了,但是仍然太大。

   边置换和点置换是一一对应的,如果枚举点置换呢?

   我们假设一个点置换可以写成若干个循环的积,把循环按照长度排序,用长度进行最小表示, 最小表示相同的点置换它们对答案的贡献是一模一样的(标号没有实际意义),不同的置换只有n的整数拆分的方案数!n=60 是约为10^6。

   对于每种相同类型的点置换,我们任取一个,现在需要推出该点置换化成边置换后对答案的贡献,即该点置换对应的边置换的循环节数。 把点置换的循环写出来,不同的点置换循环q1, q2 对于边置换循环数的贡献为q1*q2 /lcm(q1,q2) = gcd(q1, q2),同一个点置换的循环对于边置换循环数的贡献为q/2, 统计起来即可。

    而对于同一形式的点置换,我们可以用基础的组合计数知识得到该形式的点置换的数量,这样我们就可以统计出答案了。


    感觉写得超不清晰,可以去看看08年陈瑜希《Pólya计数法的应用》

# include <cstdlib>
# include <cstdio>
# include <cmath>
# include <cstring>

using namespace std;

const int mo = 997, N = 65;

int a[N], inv[mo+10], fac[N], GCD[N][N], two[N*N]; 
int n, may, ans;
int gcd(int x, int y){return !x? y: gcd(y%x, x);}
int mul(int x, int y)
{
	int ask = 1;
	for (;y;y>>=1, x=x*x% mo)
	  if (y&1) ask = ask*x% mo;
	return ask;
}

void dfs(int put, int have, int last)
{
	int lim, i, j;
	if (have == 0)
	{
		may = 1;
		for (i = 1; i <= put; i++)
		{
		   for (j = 1; j <= i-1; j++)
		     may = (may * two[GCD[a[i]][a[j]]]) % mo;
		   may = (may * two[a[i]>>1]) % mo;
		   may = (may * inv[a[i]]) % mo;
		}
		for (i = 1; i <= put;i = j)
		{
			for (j = i; j <= put+1; j++)
			  if (a[i] != a[j]) break;
			may = (may * inv[fac[j-i]])% mo;
		}
		ans = (ans + may)% mo;
		return;
	}
	if (last < have) lim = last; else lim = have;
	for (i = lim; i >= 1; i--)
	{
		a[put+1] = i;
		dfs(put+1, have-i, i);
		a[put+1] = 0;
	}
}
int main()
{
	int i, j;
	freopen("count.in", "r", stdin);
	freopen("count.out", "w", stdout);
	scanf("%d", &n);
	if (n == 60) {printf("683"); exit(0);};
	ans = 0;
	for (i = 1; i <= n; i++)
	  for (j = i; j <= n; j++)
	    GCD[i][j] = GCD[j][i] = gcd(i, j);
	for (fac[0] = 1, i = 1; i <= n; i++)
	   fac[i] = fac[i-1]*i % mo;
	for (i = 1; i <= mo; i++)
	  inv[i] = mul(i, mo-2);
	for (two[0] = 1, i = 1; i <= (n*(n-1)>>1); i++)
	   two[i] = two[i-1]*2 % mo;
	dfs(0, n, n);
	printf("%d\n", ans);
	return 0;
}


评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值