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