【知识总结】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_1\in S\)\(P_2\in S\) ,有\(P_1*P_2\in S\)
  • 存在单位元:存在一个置换使得任意序列都是它的不动点。
  • 存在逆元:对于任意序列 \(a\) 能通过置换操作成为的 \(a'\)\(a'\) 也能通过置换操作成为 \(a\) (即存在逆元)。

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

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

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

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

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

\[|G|=|E_k|\times|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\notin 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\neq P_2\) ,则 \(P*P_1\neq 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\) ,任取 \(Q\in 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*Q\in 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\) 。假设置换 \(Q\in 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,v\in E_k\)

假设 \(P_k=u,P'_k=v\)\(Q,Q'\in Z_k\) 。那么 \(P*Q\in 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\neq k,u\in E_k\)\(Z_{k,u}\) 的大小均为 \(|Z_k|\) 且互不相交。显然,所有不在 \(Z_k\) 中的置换 \(P\) 都属于且仅属于 \(Z_{k,P_k}\) ,再加上 \(Z_k\) 中的置换,就有 \(|G|=|E_k|\times |Z_k|\)

バーンサイド Burnside 引理

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

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

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

一个常见的模型

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

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

这里包含 \(2n\) 个置换:不翻转并旋转 \(k\) 位和翻转并旋转 \(k\)\((0\leq 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+k\bmod n\) 位。因为 \(-(-i+k)+k=i\) ,所以这个置换最多做两次就会回到本身,也就是环长最多是 \(2\) 。那么,环的数量就是 \(\frac{n-a}{2}+a\) ,其中 \(a\) 是自环的数量,也就是满足 \(-i+k=i\bmod n\)\(i\) 的数量。接下来考虑 \(a\) ,也就是如下方程的解的个数怎么求。

\[2i=k\mod 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\) 个置换的情况。直接套用上面的结论,答案是:

\[\begin{aligned} ans&=\frac{1}{n}\sum_{i=1}^n n^{\gcd(n,i)}\\ &=\frac{1}{n}\sum_{d|n}n^d\sum_{i=1}^{\frac{n}{d}}[gcd(\frac{n}{d},i)=1]\\ &=\frac{1}{n}\sum_{d|n}n^d\varphi(\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('\n');;
		}
		return 0;
	}
}
int main()
{
#ifndef ONLINE_JUDGE
	freopen("4980.in", "r", stdin);
#endif
	return zyt::work();
}
posted @ 2020-05-07 00:23  Inspector_Javert  阅读(530)  评论(1编辑  收藏  举报