【知识总结】Burnside 引理和 Polya 定理

(原稿发送于 2019 年 7 月 10 日,大部分内容更新于 2020 年 5 月 5 日至 6 日)

我第一次听说 Polya 原理是 NOIP2017 以前,但我觉得太难想着以后再学;

NOIP2018 以前我觉得会考这玩意,下定决心学,后来咕了;

WC2019 以前我觉得会考这玩意,下定决心学,后来咕了;

SNOI2019 以前我觉得会考这玩意,下定决心学,后来咕了;

CTS2019 以前我觉得会考这玩意,下定决心学,后来咕了;

APIO2019 以前我觉得会考这玩意,下定决心学,后来咕了;

THUSC2019 以前我觉得会考这玩意,下定决心学,后来咕了;

今天距离 NOI2019 还有不到一周,我又下定决心学,赶紧抓着 jumpmelon 让他给我讲,然后他两句话就说完了。

摔.jpg

由于 Polya 原理是我临时抱瓜脚学的,所以这篇博客里只有不严谨的结论,严谨了表述和证明什么的 NOI 以后 看心情 再补。

基于上述原因,这篇博客的吐槽环节可能比正文(不含代码)还长

问题:有一个 (n) 个点的环和 (m) 种颜色,求给每一个点染一种颜色的本质不同的方案数。两种方案本质不同当且仅当它们不能通过旋转互相得到。

结论:方案数是 (frac{1}{n}sum_{i=1}^n m^{gcd(n,i)})

Updated: 2020 年 5 月 5 日

这都快 NOI2020 了,我终于想起来更新了 qwq 。

主要参考资料:充满对称性的计数——Burnside引理与Polya定理 - 知乎

在无特殊说明的情况下,下文中所有序列的长度均为 (N)

定义

这些定义根据本文所需简化了,可能与严谨的定义有出入。

置换:若序列 (A={a_1,a_2,dots,a_N}) 经过一次变换后变为 (A'={a_{p_1},a_{p_2},dots,a_{p_N}}) ,其中 (P={p_1,p_2,dots,p_N})([1,N]) 的一个排列,则这个操作称为置换操作,记作 (A*P=A') ,这个排列称为置换。

不动点(P) 中满足 (i=P_i)(i) 的数量称为 (P) 的不动点数。

置换群:若一个置换的集合 (S) 满足以下条件,那么这个置换的集合称为置换群:

  • 封闭性:对于 (P_1in S)(P_2in S) ,有(P_1*P_2in S)
  • 存在单位元:存在一个置换使得任意序列都是它的不动点。
  • 存在逆元:对于任意序列 (a) 能通过置换操作成为的 (a')(a') 也能通过置换操作成为 (a) (即存在逆元)。

(置换本身满足结合律,所以上述置换群的定义似乎和常见的群的定义差不多)

等价类:对于置换群 (S) ,若序列的集合 (E) 中的所有序列都可以通过若干次置换操作成为 (E) 中任意一个元素,则 (E) 称为一个等价类。根据置换群的定义,(E) 中的任意一个元素通过若干次置换操作后的结果一定在 (E) 中。等价类在 OI 题目中常常表述为「本质相同的方案」之类。

一个我也不知道叫什么的定理

好像叫什么「轨道 - 中心化子定理」

(G) 为置换群,对于序列中任意一个元素 (k)(E_k) 是它所在的等价类(根据定义显然每个元素在且仅在一个等价类中),(Z_k) 是所有 (k) 是不动点的置换组成的集合。则:

[|G|=|E_k| imes|Z_k| ]

警告 :证明比较繁琐,可以选择不看。

证明:如果 (|E_k|=1) ,那么显然对于 (G) 中的任意一个置换都有 (p_k=k) ,即 (k) 是所有置换的不动点,(|Z_k|=|G|) ,上式显然成立。

否则,考虑 (E_k) 中一个不同于 (k) 的点 (u) ,任选一个 (P_k=u) 的置换 (P) (即 (k) 通过 (P) 置换到 (u) ),那么显然 (P otin Z_k) 。令 (Z_{k,u}={x|x=P*P',P'in Z_k}) 。根据置换群的封闭性, (Z_{k,u}subset G) 。此外 (Z_{k,u}) 还有一些性质:

(|Z_{k,u}|=|Z_k|) 。根据置换的可逆性,若 (P_1 eq P_2) ,则 (P*P_1 eq P*P_2) ,因此 (P*P',P'in Z_k) 互不相等,所以 (|Z_{k,u}|=|Z_k|)

任意一个 (P_k=u)(P) 构造出的 (Z_{k,u}) 都相同。若 (P_k=u)(P'_k=u) ,任取 (Qin Z_k) ,考虑 ((P'^{-1}*P*Q)_k) 。因为 (P_k=P'_k) ,所以在两边置换前先进行一次 (P') 的逆置换仍然是相等的,即 ((P'^{-1}*P)_k=(P'^{-1}*P')_k=k) 。那么 ((P'^{-1}*P*Q)_k=Q_k=k) ,即 (P'^{-1}*P*Qin Z_k)

(Q'=P'^{-1}*P*Q) ,则任意一个 (P*Q) 都能对应到一个 (P'*Q',Q'in Z_k) ,即一个由 (P') 构造出的 (Z_{k,u}) 中的元素。由于 (P)(P') 的任意性,所以反之亦然。

(Z_{k,u}cap Z_k=empty) 。假设置换 (Qin Z_{k,u}cap Z_k) ,因为 (Q=P*P') 相当于从 (k) 先变换到 (u) ,再从 (u) 变换到 (P'_u) (即 (Q_k) )。

(Q_k=k) 说明 (P'_u=k) ,又因为 (P'in Z_k) 所以 (P'_k=k) ,这样 (P') 中就有两个 (k) ,与排列的定义矛盾。因此不存在这样的 (Q)

(Z_{k,u}cap Z_{k,v}=empty, u,vin E_k)

假设 (P_k=u,P'_k=v)(Q,Q'in Z_k) 。那么 (P*Qin Z_{k,u},P'*Q'in Z_{k,v})

(P*Q=P'*Q') ,两边同时乘上 (Q^{-1}) (显然 (Q^{-1}_k=Q_k=k)(Q^{-1}in Z_k) )得到 (P=P'*Q'*Q^{-1}) ,那么 (P_k=(P'*Q'*Q^{-1})_k=P_k=(P'*(Q'*Q^{-1}))_k)

由于 (Q'in Z_k,Q^{-1}in Z_k) ,那么 (Q'*Q^{-1} in Z_k) ,即通过 ((Q'*Q^{-1})) 置换对 (k) 没有影响。那么就有 (P'_k=P_k=u) ,与 (P'_k=v) 矛盾,故这种情况不存在。

结论

综上所述,对于所有 (u eq k,uin E_k)(Z_{k,u}) 的大小均为 (|Z_k|) 且互不相交。显然,所有不在 (Z_k) 中的置换 (P) 都属于且仅属于 (Z_{k,P_k}) ,再加上 (Z_k) 中的置换,就有 (|G|=|E_k| imes |Z_k|)

バーンサイド Burnside 引理

(C(P)) 表示置换 (P) 的不动点个数,则对于置换群 (G) ,等价类个数 (L)(G) 中各个置换的不动点数的平均数。即:

[L=frac{1}{|G|}sum_{Pin G} C(P) ]

证明:根据上面的定理,每个等价类中的点的 (|Z_k|) 都相等,因此每个等价类中 (|E_k|) 个点的 (|Z_k|) 之和就是 (|G|) 。那么,所有点的 (|Z_k|) 之和,也就是 (sum_{Pin G} C(P)) 之和,就是等价类数乘上 (|G|)

一个常见的模型

有一个长为 (n) 的圆环,每个位置可以填 ([1,K]) 中任意一个整数,求本质不同的填数方案数。通过旋转和翻转能够互相得到的方案视为本质相同。

(严谨地定义一下:对于序列 ({a_0,a_1,dots,a_{n-1}}) ,旋转 (k) 位就是把 (a_i) 变成 (a_{i+kmod n}) ,翻转就是把 (a_i) 变成 (a_{-imod n}) 。)

这里包含 (2n) 个置换:不翻转并旋转 (k) 位和翻转并旋转 (k)((0leq k<n)) 。这些置换显然构成了一个置换群。把每种染色方案看作上述序列中的一个点(共 (K^n) 个),则每种方案能通过这些置换得到的方案都是本质相同的方案。这样就把题目转化成了等价类计数问题,可以直接套用 Burnside 引理。

但是,使用 Burnside 引理需要算出每个置换的不动点数,所以需要对每个置换分别枚举 (K^n) 种填数方案,并检查这些方案是否是不动点,时间复杂度太高了。对于这个问题,可以使用 Polya 定理来优化。

ポリア Polya 定理

我并不是特别确定这个定理指的是哪个具体的公式。我更愿意把它看作一种求不动点数的一般思路。

对于一个置换 (P) ,把序列的每个位置看成一个点,从 (i)(P_i) 连边,这样一定形成的是若干个有向环。在上述问题中,如果某种方案对于 (P) 是不动点,那么它一定满足每个环上的点填的数都相同(显然这样才能保证置换后不变),而环与环之间则互不影响。因此,置换 (P) 的不动点数就是 (K^m) ,其中 (m) 是环的数量。

特殊地,考虑上述那个模型。对于不翻转的置换,如果旋转 (k) 位,那么环的数量就是 (gcd(n,k)) (所有点按照模 (gcd(n,k)) 的余数分类。证明略)。而翻转后再旋转 (k) 位的置换的环的数量则比较复杂。

根据上面对「翻转」的定义,第 (i) 位的数翻转后再旋转 (k) 位后会到第 (-i+kmod n) 位。因为 (-(-i+k)+k=i) ,所以这个置换最多做两次就会回到本身,也就是环长最多是 (2) 。那么,环的数量就是 (frac{n-a}{2}+a) ,其中 (a) 是自环的数量,也就是满足 (-i+k=imod n)(i) 的数量。接下来考虑 (a) ,也就是如下方程的解的个数怎么求。

[2i=kmod n ]

(n) 是奇数时存在 (2) 的逆元,因此有且只有一个解。环的数量是 (frac{n-1}{2}+1)

(n) 是偶数时,若 (k) 是偶数则存在 (frac{k}{2})(frac{k+n}{2}) 两个解,若 (k) 是奇数则无解。环的数量是 (frac{n-2}{2}+2=frac{n}{2}+1)(k) 是偶数)或 (frac{n}{2})(k) 是奇数)。

例题:洛谷 4980 Polya 定理

相当于只考虑不翻转的 (n) 个置换的情况。直接套用上面的结论,答案是:

[egin{aligned} ans&=frac{1}{n}sum_{i=1}^n n^{gcd(n,i)}\ &=frac{1}{n}sum_{d|n}n^dsum_{i=1}^{frac{n}{d}}[gcd(frac{n}{d},i)=1]\ &=frac{1}{n}sum_{d|n}n^dvarphi(frac{n}{d}) end{aligned}]

(n) 分解质因子然后按照质因子搜索它的所有因数,搜索时维护一下 (varphi) 就好了

代码:

#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
using namespace std;

namespace zyt
{
	template<typename T>
	inline bool read(T &x)
	{
		char c;
		bool f = false;
		x = 0;
		do
			c = getchar();
		while (c != EOF && c != '-' && !isdigit(c));
		if (c == EOF)
			return false;
		if (c == '-')
			f = true, c = getchar();
		do
			x = x * 10 + c - '0', c = getchar();
		while (isdigit(c));
		if (f)
			x = -x;
		return true;
	}
	template<typename T>
	inline void write(T x)
	{
		static char buf[20];
		char *pos = buf;
		if (x < 0)
			putchar('-'), x = -x;
		do
			*pos++ = x % 10 + '0';
		while (x /= 10);
		while (pos > buf)
			putchar(*--pos);
	}
	typedef long long ll;
	typedef pair<int, int> pii;
	const int SQRTN = 4e4 + 10, P = 1e9 + 7;
	pii prime[SQRTN];
	int pcnt, n, ans;
	int power(int a, int b)
	{
		int ans = 1;
		while (b)
		{
			if (b & 1)
				ans = (ll)ans * a % P;
			a = (ll)a * a % P;
			b >>= 1;
		}
		return ans;
	}
	int inv(const int a)
	{
		return power(a, P - 2);
	}
	void get_prime(int n)
	{
		pcnt = 0;
		for (int i = 2; i * i <= n; i++)
			if (n % i == 0)
			{
				prime[pcnt++] = pii(i, 0);
				while (n % i == 0)
					++prime[pcnt - 1].second, n /= i;
			}
		if (n > 1)
			prime[pcnt++] = pii(n, 1);
	}
	void dfs(const int pos, const int mul, const int pmul, const int div)
	{
		if (pos == pcnt)
		{
			ans = (ans + (ll)mul / div * pmul * power(n, n / mul)) % P;
			return;
		}
		dfs(pos + 1, mul, pmul, div);
		for (int i = 1, tmp = prime[pos].first; i <= prime[pos].second; i++, tmp *= prime[pos].first)
			dfs(pos + 1, mul * tmp, pmul * (prime[pos].first - 1), div * (prime[pos].first));
	}
	int work()
	{
		int T;
		read(T);
		while (T--)
		{
			ans = 0;
			read(n);
			get_prime(n);
			dfs(0, 1, 1, 1);
			write((ll)ans * inv(n) % P), putchar('
');;
		}
		return 0;
	}
}
int main()
{
#ifndef ONLINE_JUDGE
	freopen("4980.in", "r", stdin);
#endif
	return zyt::work();
}
原文地址:https://www.cnblogs.com/zyt1253679098/p/12840127.html