(原稿发送于 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) 是不动点的置换组成的集合。则:
警告 :证明比较繁琐,可以选择不看。
证明:如果 (|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) 中各个置换的不动点数的平均数。即:
证明:根据上面的定理,每个等价类中的点的 (|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) ,也就是如下方程的解的个数怎么求。
当 (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('
');;
}
return 0;
}
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("4980.in", "r", stdin);
#endif
return zyt::work();
}