【知识总结】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\) 是不动点的置换组成的集合。则:
警告 :证明比较繁琐,可以选择不看。
证明:如果 \(|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\) 中各个置换的不动点数的平均数。即:
证明:根据上面的定理,每个等价类中的点的 \(|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\) ,也就是如下方程的解的个数怎么求。
当 \(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\) 个置换的情况。直接套用上面的结论,答案是:
给 \(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();
}