【学习笔记】Burnside引理和Polya定理
reference:
https://blog.csdn.net/xym_CSDN/article/details/53456447
https://blog.csdn.net/thchuan2001/article/details/65641566
https://blog.csdn.net/ta201314/article/details/51050846
https://www.cnblogs.com/pks-t/p/9508866.html
https://www.cnblogs.com/yyf0309/p/Burnside.html
https://oi-wiki.org/math/permutation-group
本文主要介绍了和 Burnside 引理有关的一些简单的群论知识。
本文的目的仅是引入和简单证明 Burnside 引理和 Pólya 定理,因此关于群论的部分概念的介绍会比较简略和不太规范,因为大部分是我到处在网上找资料学的,如果有不当之处,欢迎指出。
1 群
1.1 群的定义
若集合 \(S\neq\varnothing\) 和 \(S\) 上的运算 \(\cdot\) 构成的代数结构 \((S,\cdot)\) 满足以下性质:
-
封闭性: \(\forall a,b\in S,a\cdot b\in S\)
-
结合律: \(\forall a,b,c\in S,(a\cdot b)\cdot c=a\cdot(b\cdot c)\)
-
单位元: \(\exists e\in S,\forall a\in S,e\cdot a=a\cdot e=a\)
-
逆元: \(\forall a\in S,\exists b\in S,a\cdot b=b\cdot a=e\) ,称 \(b\) 为 \(a\) 的逆元,记为 \(a^{-1}\)
则称 \((S,\cdot)\) 为一个群(注意到 \(S\) 一定是非空的)。
一些其他定义:
- 阿贝尔群:即交换群,满足交换律的群。
- 半群:由集合 \(S\neq \varnothing\) 和 \(S\) 上的运算 \(\cdot\) 构成的代数结构 \((S,\cdot)\),满足封闭性和结合律。
- 有限群:元素个数有限的群称为有限群,而有限群的元素个数称作有限群的阶。
- 环:由集合 \(S\neq \varnothing\) 和 \(S\) 上的两个运算 \(+,\cdot\) 构成的代数结构 \((S,+,\cdot)\),满足 \((S,+)\) 是阿贝尔群,\((S\setminus \{0\},\cdot)\) 是半群(其中 \(0\) 为 \((S,+)\) 的单位元)。
- 域:由集合 \(S\neq \varnothing\) 和 \(S\) 上的两个运算 \(+,\cdot\) 构成的代数结构 \((S,+,\cdot)\),满足 \((S,+),(S\setminus \{0\},\cdot)\) 是阿贝尔群(其中 \(0\) 为 \((S,+)\) 的单位元)。
1.2 群的简单性质
关于群,有一些比较简单的想法:
-
一个群中的单位元唯一。
证明:假设有两个单位元 \(e_1,e_2\),有 \(e_1=e_1e_2=e_2\)。
-
如果 \(a\cdot x=e\),我们称 \(a\) 是 \(x\) 的左逆元;如果 \(x\cdot b=e\),我们称 \(b\) 是 \(x\) 的右逆元。
可以证明,在一个群中,左逆元和右逆元是一样的。证明:不妨设 \(c\cdot a=e\),那么 \(x\cdot a=(c\cdot a)\cdot (x\cdot a)=c\cdot (a\cdot x)\cdot a=c\cdot a=e\),即 \(a\) 也是 \(x\) 的右逆元。
-
一个群中 \(x\) 的逆元唯一。
证明:如果有 \(x\) 两个逆元 \(a,b\),那么我们有 \(a=a\cdot x\cdot b=b\)。
-
群中有消去律存在。即 \(\forall a,b,x\in G, ax=bx \Leftrightarrow a=b\)。
证明:两边同乘逆元。
在下面的讨论中,我们默认是在有限群上讨论。
1.3 子群及其衍生
子群:对于一个群 \(G(S,\cdot)\),若 \(T\subseteq S\),且 \(H(T,\cdot)\) 也是一个群,那么称 \((T,\cdot)\) 是 \((S,\cdot)\) 的一个子群,记为 \(H\leq S\)。
生成子群:对于 \(S\) 的一个非空子集 \(T\),我们求出 \(G\) 的所有使 \(T \subseteq T'\) 的子群 \((T',\cdot)\) 的交 \(G'\),\(G'\) 叫做 \(T\) 的生成子群,同时 \(T\) 也是 \(G'\) 的生成集合,记为 \(\langle T\rangle\)。当 \(T=\{x\}\),我们也写作 \(\langle x\rangle\)。
循环群:可由一个元素生成的群。
陪集:对于群 \(G\) 的一个子群 \(H\)。
- 如果 \(H \leq G\),对于 \(a\in G\),定义 \(H\) 的一个左陪集为 \(_aH=\{ah\arrowvert h\in H\}\)。
- 如果 \(H\leq G\),对于 \(a\in G\),定义 \(H\) 的一个右陪集为 \(H_a=\{ha\arrowvert h\in H\}\)。
注意陪集不一定是一个群,因为陪集显然可能没有单位元。
1.3.1 陪集的性质
陪集有一些重要的性质,我们下面只讨论右陪集的情况(左陪集同理):
-
\(\forall a\in G,|H|=|H_a|\)。
证明:如果 \(h_1\neq h_2\in H\),那么 \(h_1a\neq h_2a\)。对于不同的 \(h\),\(ha\) 互不相同,因此 \(|H|=|H_a|\)。
-
\(\forall a\in G,a\in H_a\)。
证明:因为 \(H\) 是群,所以 \(e\in H\),所以 \(ea\in H_a\) 即 \(a\in H_a\)。
-
\(H_a=H \Leftrightarrow a\in H\)
证明:从左推到右,因为 \(a\in H_a\)。从右推到左,由群的封闭性 \(H_a \subseteq H\),而 \(|H|=|H_a|\),所以 \(H_a=H\)。
-
\(H_a=H_b \Leftrightarrow ab^{-1}\in H\)。
注意这个性质的右边也可以写成 \(a\in H_b\),\(b\in H_a\),\(a^{-1}b\in H\)。证明:从左推到右,\(a\in H_a\Rightarrow a\in H_b\Rightarrow ab^{-1}\in H\)。从右推到左,\(H_{ba^{-1}}=H\),故 \(H_a=H_b\)。
-
\(H_a\cap H_b\neq \varnothing \Rightarrow H_a=H_b\)。
这句话的意思是 \(H\) 的任意两个陪集要么相等,要么没有交集。证明:考虑 \(c\in H_a\cap H_b\),那么 \(\exist h_1,h_2\in H,h_1a=h_2b=c\),那么 \(ab^{-1}=h_1^{-1}h_2\in H\),故 \(H_a=H_b\)。
1.3.2 拉格朗日定理
若 \(H \leq G\),那么 \(|H|\) 整除 \(|G|\)。更准确地
其中 \([G:H]\) 表示 \(G\) 中 \(H\) 不同的陪集数。
证明:根据陪集的性质,\(H\) 的所有陪集大小相等且互不相交。
1.3.3 一些推论和应用
对于某个元素 \(a\in G\),我们称 \(a\) 的周期 \(o(a)=\min\{x\arrowvert a^x=e,x\in\mathbb N^*\}\),在有限群内这个周期一定存在,否则我们令 \(o(a)=+\infty\)。
那么对于有限群 \(G\),有以下推论:
-
对于 \(a\in G\),有 \(o(a)|\ |G|\)。
证明:\(o(a)=|\langle a\rangle|\),显然 \(\langle a\rangle \leq G\),由拉格朗日定理可知 \(o(a)|\ |G|\)。
-
对每个 \(a\in G\),都有 \(a^{|G|}=e\)。
证明:由前面的推论显然。
-
若 \(|G|\) 为素数,则 \(G\) 是循环群。
证明:对于 \(a \neq e\),有 \(|\langle a\rangle|\neq 1\) 整除 \(|G|\),也就是 \(|\langle a\rangle|=|G|\),因为 \(\langle a\rangle\leq G\),所以 \(\langle a\rangle=G\)。
有一些美妙的应用:
-
费马小定理:若 \(p\) 是质数,那么 \(\forall a\not\equiv 0\pmod p,a^{p-1}\equiv 1\pmod p\)。
证明只要考虑群 \((\{1,2,\cdots,p-1\}, \times \bmod p)\)。
-
欧拉定理:若 \(\gcd(a,p)=1\),那么 \(a^{\phi(p)}\equiv 1\pmod p\)。
证明只要考虑群 \((\{x \arrowvert x\in[1,p),\gcd(x,p)=1\}, \times \bmod p)\)。
2 置换群
2.1 置换的定义
有限集合到自身的双射(即一一对应)称为置换。不可重集合 \(S=\{a_1,a_2,\cdots,a_n\}\) 上的置换可以表示为
表示将 \(a_i\) 映射为 \(a_{p_i}\),即 \(f(a_i)=a_{p_i}\)。其中 \(p_1,p_2,\cdots,p_n\) 是 \(1 \sim n\) 的一个排列。
如果我们没有强制 \(a_1,a_2,\cdots,a_n\) 的排列顺序,那么显然这些列的顺序是不要紧的。
显然 \(S\) 上的所有置换的数量为 \(n!\)。
2.2 置换的乘法
对于两个置换,\(f=\left(\begin{array}{l}{a_{p_{1}}, a_{p_{2}}, \dots, a_{p_{n}}} \\{a_{q_{1}}, a_{q_{2}}, \dots, a_{q_{n}}}\end{array}\right)\) 和 \(g=\left(\begin{array}{c}{a_{1}, a_{2}, \ldots, a_{n}} \\{a_{p_{1}}, a_{p_{2}}, \ldots, a_{p_{n}}}\end{array}\right)\),\(f\) 和 \(g\) 的乘积记为 \(f\circ g\),其值为
即 \((f\circ g)(x)=f(g(x))\),简单来说就是先经过了 \(g\) 的映射再经过了 \(f\) 的映射。
2.3 置换群
易证,集合 \(S\) 上的所有置换关于置换的乘法满足封闭性、结合律、有单位元(恒等置换/单位置换,即每个元素映射成它自己)、有逆元(交换置换表示中的上下两行),因此构成一个群。
通常我们把在 \(\{1,2,\cdots,n\}\) 上的所有置换构成的群称为 \(n\) 元对称群,记为 \(S_n\)。
这个群的任意一个子群即称为置换群 。
2.4 循环置换
循环置换(也叫轮换)是一类特殊的置换,可表示为
若两个循环置换不含有相同的元素,则称它们是不相交的。有如下定理:
任意一个置换都可以分解为若干不相交的循环置换的乘积,例如
该定理的证明也非常简单。如果把元素视为图的节点,映射关系视为有向边,则每个节点的入度和出度都为 1,因此形成的图形必定是若干个环的集合,而一个环即可用一个循环置换表示。
2.5 置换的奇偶性
这个蛮提一下,虽然和重点没啥关系,但是挺有意思的。
我们知道排列有奇偶性,置换也有奇偶性。
排列的奇偶性:
- 定义排列的奇偶性和排列的逆序对数的奇偶性相同。
- 我们知道交换排列的两个相邻元素会使整个排列的逆序对数的奇偶性取反,而交换当前两个 \(p_i,p_j\) 可以用 \(2|i-j|-1\) 次交换相邻元素实现,因此交换任意两个不同元素也会使整个排列的逆序对数取反。
置换的奇偶性:
- 我们称对换为大小为 \(2\) 的非单位置换(即交换某两个元素)。定义置换的奇偶性与该置换变成单位置换所需的对换次数的奇偶性相同。
- 不难发现,对于置换 \(\left(\begin{array}{c} {1,2,\cdots,n} \\ {p_{1}, p_{2}, \cdots, p_{n}} \end{array}\right)\),其奇偶性与排列 \(p_1,p_2,\cdots,p_n\) 的奇偶性相同。
- 我们发现,大小为 \(n\) 的轮换,可以用 \(n-1\) 次对换变成单位置换,而置换又可以分解为若干不相交的轮换。若一个置换可以分解为 \(d\) 个不相交的轮换,那么这个置换的奇偶性与 \(n-d\) 的奇偶性相同。
3 轨道-稳定子定理
定义 \(A,B\) 是两个有限集合,\(X=B^A\) 表示所有从 \(A\) 到 \(B\) 的映射,\(G\) 是作用在 \(A\) 上的一个置换群。
(比如给正方体六个面染色,\(A\) 就是正方体六个面的集合,\(B\) 就是所有颜色的集合,\(X\) 就是不考虑本质不同的方案集合,即 \(|X|=|B|^{|A|}\) )
我们定义,对于每个 \(x\in X\)
其中 \(G^x\) 称为 \(x\) 的稳定子,\(G(x)\) 称为 \(x\) 的轨道。
轨道-稳定子定理:
证明:首先可以证明 \(G^x\) 是 \(G\) 的一个子群,因为
- 封闭性:若 \(f,g\in G\) ,则 \((f\circ g)(x)=f(g(x))=f(x)=x\) ,所以 \(f\circ g\in G^x\)。
- 结合律:显然置换的乘法满足结合律。
- 单位元:因为 \(I(x)=x\) ,所以 \(I\in G^x\) ( \(I\) 为恒等置换)。
- 逆元:若 \(g\in G^x\) ,则 \(g^{-1}(x)=g^{-1}(g(x))=(g^{-1}\circ g)(x)=I(x)=x\) ,所以 \(g^{-1}\in G^x\)。
由拉格朗日定理得 \(|G|=|G^x|\cdot[G:G^x]\)。下面只要证明 \(|G(x)|=[G:G^x]\)(直观理解这是很显然的,但是我们还是要证明一下)
- 令 \(\varphi(g(x))=\ _gG^x\),下面证明 \(\varphi\) 是单射,则 \(|G(x)|\leq [G:G^x]\)。
- 若 \(g(x)=f(x)\) ,两边同时左乘 \(f^{-1}\) ,可得 \((f^{-1}\circ g)(x)=I(x)=x\) ,所以 \(f^{-1}\circ g\in G^x\) ,由陪集的性质可得 \(_gG^x=\ _fG^x\)。
- 反过来可证,若 \(_gG^x=\ _fG^x\) ,则有 \(g(x)=f(x)\) 。
- 以上两点说明对于一个 \(g(x)\) ,恰有一个左陪集与其对应;且对于每个左陪集,至多有一个 \(g(x)\) 与之对应。
- 即 \(\varphi\) 是一个从 \(G(x)\) 到左陪集的单射。
- 令 \(\varphi'(\ _gG^x)=g(x)\),同理证明 \(\varphi'\) 是单射,则 \(|G(x)|\geq [G:G^x]\)。
- 证明和上面类似。
4 Burnside 引理
定义 \(A,B\) 是两个有限集合,\(X=B^A\) 表示所有从 \(A\) 到 \(B\) 的映射,\(G\) 是作用在 \(A\) 上的一个置换群(跟上面一样)。\(X/G\) 表示作用在 \(X\) 上产生的所有等价类的集合(若 \(X\) 中的两个映射能经过 \(G\) 中的置换作用后相等,则它们在同一等价类中)。
\(X/G\) 其实就是,对于所有的 \(x\in X\),不同轨道的集合,这些轨道必定是不交的。因此我们也将 \(|X/G|\) 叫做 \(X\) 关于 \(G\) 的轨道数。
Burnside 引理:
其中 \(X^g=\{x\arrowvert g(x)=x,x\in X\}\),我们称 \(X^g\) 是 \(X\) 在置换 \(g\) 下的不动点集合。
文字描述:\(X\) 关于置换群 \(G\) 的轨道数,等于 \(G\) 中每个置换下不不动点的个数的算术平均数。
证明:(Burnside 引理本质上是更换了枚举量,从而方便计数)
根据轨道-稳定子定理,我们有 \(|G|=|G^x|\cdot |G(x)|\),所以
至此我们就证明了 Burnside 引理。
注意当 \(X\subseteq B^A\) 时,Burnside 引理也是成立的。也就是说,我们给 \(A\) 到 \(B\) 的映射加上一些条件,Burnside 引理仍然成立。其原因就是上面的证明没有用到 \(X=B^A\)。
5 Pólya 定理
是 Burnside 引理的一种特殊形式。
前置条件与 Burnside 引理相同,内容修改为
\(c(g)\) 表示置换 \(g\) 拆出的不相交轮换数量。
证明:在 Burnside 引理中,\(g(x)=x\) 的充要条件是 \(x\) 将 \(g\) 中每个轮换内的元素都映射到了 \(B\) 中的同一个元素,所以 \(|X^g|=|B|^{c(g)}\),即可得 Pólya 定理。
注意只有当 \(X=B^A\) 成立时(也就是当 \(X\) 是 \(A\) 到 \(B\) 的所有映射时),Pólya 定理才成立,否则不一定成立。
6 应用
6.1 环上的计数问题
6.1.1 「FJWC2020 Day2」手链强化
来源:FJWC2020 Day2T2
给一个有 \(n\) 个珠子的手链染色,一共有 \(k\) 种颜色,每个珠子可以选择染这 \(k\) 种颜色之一,也可以选择不染色。不能存在两个相邻的珠子都被染色。
求有多少种本质不同的染色方案。两种方案本质相同,当且仅当通过旋转能相同(不包括翻转)。
对 \(10^9+7\) 取模。
\(n,k \leq 10^9\)
时空限制:\(1\texttt s/512\texttt{MB}\)
这题属于 Burnside 引理的简单应用。因为是第一个例题,所以讲得比较详细。
首先我们考虑旋转对环的影响,我们发现旋转相当于给 \(n\) 个珠子作用上了一个置换,而所有旋转方式产生的置换群共有 \(n\) 个置换:\((2,3,\cdots,n-1,n,1)^k~(k\in\{0,1,2,\cdots,n-1\})\)。
利用 Burnside 引理,所有染色方案关于置换群的轨道数,就等于每个置换下不动点个数的算数平均数。因此我们只要枚举每个置换,计算一下每种置换下的不动染色方案数。
我们可以将每个置换看成顺时针旋转 \(k\) 个位置,要求在这个置换下不动的染色方案数,就相当于限制,对于每个 \(i\),都有 \(c_i=(c_i+k-1)\bmod n+1\),其中 \(c_i\) 表示在不旋转的情况下,第 \(i\) 个珠子的染色情况。
我们不妨将强制相同的珠子并成一个集合,那么相当于每个集合都强制染同一种颜色。等价的说法是,将这个置换分解为轮换,属于同一个轮换的珠子需要染成同一个颜色,不属于同一个轮换的就没有相交部分,没有这样的限制。
用线性同余方程的理论可以证明,这样操作之后,共有 \(\gcd(k,n)\) 个集合,每个集合有 \(\frac{n}{\gcd(k,n)}\) 个珠子,其中第 \(i\) 个珠子就属于第 \((i-1)\bmod \gcd(k,n)+1\) 个集合。
并且第 \(i\) 个集合里面每个珠子的相邻两个珠子属于的集合就是 \(i-1\) 和 \(i+1\)。特别地,\(1\) 和 \(\gcd(k,n)\) 这两个集合也是相邻的。因此对相邻珠子染色的限制,现在可以看成对相邻集合染色的限制。而且我们可以简单地将所有集合看成一个大小为 \(\gcd(k,n)\) 的环。
我们记 \(f(d)\) 表示给大小为 \(d\) 的环染色的方案数(此时不考虑旋转同构)。那么答案就是
求 \(f(d)\) 就是一件很简单的事情了。先考虑给链染色(先不考虑首尾相连的问题),设 \(g(i,0/1)\) 表示考虑了前 \(i\) 个珠子,最后一个是否染色的方案数。那么有
只要枚举第一个珠子是否染色。不染色的方案数是 \(g(i-1,0)+g(i-1,1)\),染色的方案数是 \(g(i-2,0)\cdot k\)。
因此 \(f(i)=g(i-1,0)+g(i-1,1)+k\cdot g(i-2,0)\)。
提前将 \(n\) 质因数分解,然后用 dfs 枚举约数 \(d\),顺便在 dfs 的过程中求出 \(\varphi (d)\),用矩阵快速幂优化递推来求 \(g\)。
时间复杂度 \(\mathcal O(\sqrt n+\sigma_0(n)\log n)\)。
#include <bits/stdc++.h>
const int mod = 1e9 + 7;
const int MaxN = 1e6 + 5;
inline void add(int &x, const int &y)
{
x += y;
if (x >= mod)
x -= mod;
}
struct matrix
{
int r, c;
int mat[3][3];
matrix(){}
matrix(int _r, int _c):
r(_r), c(_c) {memset(mat, 0, sizeof(mat));}
inline void init()
{
for (int i = 1; i <= r; ++i)
mat[i][i] = 1;
}
inline matrix operator * (const matrix &rhs) const
{
matrix res(r, rhs.c);
for (int i = 1; i <= r; ++i)
for (int k = 1; k <= c; ++k)
for (int j = 1; j <= rhs.c; ++j)
res.mat[i][j] = (res.mat[i][j] + 1LL * mat[i][k] * rhs.mat[k][j]) % mod;
return res;
}
inline matrix operator ^ (int p) const
{
matrix res(r, c), x = *this;
res.init();
for (; p; p >>= 1, x = x * x)
if (p & 1)
res = res * x;
return res;
}
}T(2, 2), F0(2, 1);
int n, K;
int cnt, p[MaxN], c[MaxN];
int ans;
inline int qpow(int x, int y)
{
int res = 1;
for (; y; y >>= 1, x = 1LL * x * x % mod)
if (y & 1)
res = 1LL * res * x % mod;
return res;
}
inline int F(int n)
{
if (n == 1)
return 1;
matrix F2 = (T ^ (n - 2)) * F0;
matrix F1 = T * F2;
int res = 1LL * F2.mat[1][1] * K % mod;
add(res, F1.mat[1][1]);
add(res, F1.mat[2][1]);
return res;
}
inline void dfs(int t, int cur = 1, int phi = 1)
{
if (t > cnt)
{
add(ans, 1LL * F(cur) * phi % mod);
return;
}
int cur_phi = p[t] - 1, pri = p[t], nxt = 1;
for (int i = 2; i <= c[t]; ++i)
cur_phi *= p[t];
for (int i = 0; i <= c[t]; ++i)
{
dfs(t + 1, cur * nxt, phi * cur_phi);
if (i == c[t] - 1)
cur_phi = 1;
else
cur_phi /= pri;
nxt *= pri;
}
}
int main()
{
freopen("bracelet.in", "r", stdin);
freopen("bracelet.out", "w", stdout);
std::cin >> n >> K;
T.mat[1][1] = T.mat[1][2] = F0.mat[1][1] = 1;
T.mat[2][1] = K;
int x = n;
for (int i = 2; i * i <= n; ++i)
if (x % i == 0)
{
p[++cnt] = i;
while (x % i == 0)
x /= i, ++c[cnt];
}
if (x > 1)
{
++cnt;
p[cnt] = x;
c[cnt] = 1;
}
dfs(1);
ans = 1LL * ans * qpow(n, mod - 2) % mod;
std::cout << ans << std::endl;
return 0;
}
6.1.2 「MtOI2018」魔力环
你需要用 \(m\) 个黑珠子和 \(n-m\) 个白珠子串成一个环,使得这个环上不会出现一段连续的黑珠子,其长度超过 \(k\),求通过旋转环不会相同的方案数 \(\bmod 998244353\)。\(m \leq n \leq 10^5\),\(k \leq 10^5\)
时空限制:\(1\texttt s/512\texttt{MB}\)
首先利用 Burnside 引理的套路,答案就是
其中 \(f(n,m)\) 表示用 \(m\) 个黑珠子和 \(n-m\) 个白珠子串成一个环的方案数(不考虑旋转同构)。
特别地我们需要特判 \(m=n\) 的情况。
考虑断环为链,首先特判掉 \(m \leq k\) 的情况,此时 \(f(n,m)=\binom nm\)。
否则我们考虑枚举和 \(1\) 和 \(n\) 之间的分界线相邻的黑珠子连续段长度 \(i(0 \leq i \leq k)\),这样有 \(i+1\) 种摆放方式,然后强制这个连续段的两段连着白珠子。这样剩下的部分就可以看成链上的问题了。
现在等价于将 \(m-i\) 个黑珠子插进 \(n-m\) 个白珠子间的缝隙中,等价于求将 \(m-i\) 个黑珠子划分为 \(n-m-1\) 段的方案数(一段可以为空),并且每段的长度不能超过 \(k\)。
那么我们设 \(g(n,m)\) 表示将 \(n\) 个珠子分成 \(m\) 段,每段不能超过 \(k\) 的方案数。可以容斥计算,强制一些段超过 \(k\)
那么
枚举约数 \(d\) 之后,计算 \(g(n,m)\) 的时间复杂度是 \(\mathcal O(\frac n k)\),那么计算 \(f(d,*)\) 的时间复杂度就是 \(\mathcal O(d)\)。
因此总的时间复杂度就是 \(\mathcal O(\sigma(n))\),即 \(n\) 的约数和。
#include <bits/stdc++.h>
const int mod = 998244353;
inline void add(int &x, const int &y)
{
x += y;
if (x >= mod)
x -= mod;
}
inline void dec(int &x, const int &y)
{
x -= y;
if (x < 0)
x += mod;
}
const int MaxN = 2e5 + 5;
int n, m, K;
int phi[MaxN];
int fac[MaxN], ind[MaxN], fac_inv[MaxN];
inline int C(int n, int m)
{
if (n < m || n < 0 || m < 0) return 0;
return 1LL * fac[n] * fac_inv[m] % mod * fac_inv[n - m] % mod;
}
inline void fac_init(int n)
{
fac[0] = fac_inv[0] = ind[1] = 1;
for (int i = 1; i <= n; ++i)
{
if (i > 1)
ind[i] = 1LL * ind[mod % i] * (mod - mod / i) % mod;
fac[i] = 1LL * fac[i - 1] * i % mod;
fac_inv[i] = 1LL * fac_inv[i - 1] * ind[i] % mod;
}
}
inline void sieve_init(int n)
{
static bool sie[MaxN];
static int pri[MaxN], cnt;
phi[1] = 1;
for (int i = 2; i <= n; ++i)
{
if (!sie[i])
{
pri[++cnt] = i;
phi[i] = i - 1;
}
for (int j = 1; j <= cnt && 1LL * pri[j] * i <= n; ++j)
{
int x = pri[j] * i;
sie[x] = true;
if (i % pri[j] == 0)
{
phi[x] = phi[i] * pri[j];
break;
}
else
phi[x] = phi[i] * (pri[j] - 1);
}
}
}
inline int g(int n, int m)
{
int res = 0, lim = std::min(m / (K + 1), n);
for (int i = 0; i <= lim; ++i)
{
int cur = 1LL * C(n, i) * C(m - i * (K + 1) + n - 1, n - 1) % mod;
if (i & 1)
dec(res, cur);
else
add(res, cur);
}
return res;
}
inline int f(int n, int m)
{
if (K >= m)
return C(n, m);
else if (n == m + 1)
return 0;
int res = 0;
for (int i = 0, t = n - m - 1; i <= K; ++i)
res = (res + 1LL * (i + 1) * g(t, m - i)) % mod;
return res;
}
int main()
{
std::cin >> n >> m >> K;
if (n == m)
return puts(K >= n ? "1" : "0"), 0;
sieve_init(n);
fac_init(n << 1);
int ans = 0;
for (int i = 1; i <= n; ++i)
if (n % i == 0 && m % (n / i) == 0)
add(ans, 1LL * phi[n / i] * f(i, m / (n / i)) % mod);
std::cout << 1LL * ans * ind[n] % mod << std::endl;
return 0;
}
本题还有另外一个版本:
不限制总的黑珠子的个数,即一共有 \(n\) 个珠子,每个珠子可以染成黑色或白色,要求不存在一段连续的黑珠子长度超过 \(k\)。\(1 \leq k \leq n \leq 10^7\)
同样答案是
其中 \(f(d)\) 表示给长为 \(d\) 的环染色的方案数(不考虑旋转同构)。同样需要特判 \(n=k\)。
和上面类似的思路,枚举 \(1,n\) 交界处相邻的黑色连续段长度,然后断环为链,不过这个时候的链就没有限制黑珠子的总数了,可以任意染色,我们记 \(g(n)\) 表示给长度为 \(n\) 的链染色的方案数。
那么用容斥,我们有
含义就是用 \(2g(n-1)\) 减去末尾恰好 \(k+1\) 个黑珠子的方案数(超过 \(k+1\) 个的前面已经扣掉了)。
这样的时间复杂度仍然是 \(\mathcal O(\sigma(n))\)。
不过计算 \(f\) 的过程可以用前缀和优化,那么时间复杂度就可以优化到 \(\mathcal O(n)\) 了。
#include <bits/stdc++.h>
const int mod = 1e8 + 7;
inline void add(int &x, const int &y)
{
x += y;
if (x >= mod)
x -= mod;
}
inline void dec(int &x, const int &y)
{
x -= y;
if (x < 0)
x += mod;
}
inline int qpow(int x, int y)
{
int res = 1;
for (; y; y >>= 1, x = 1LL * x * x % mod)
if (y & 1)
res = 1LL * res * x % mod;
return res;
}
const int MaxN = 1e7 + 5;
int n, K;
int phi[MaxN], f[MaxN];
inline void sieve_init(int n)
{
static bool sie[MaxN];
static int pri[MaxN], cnt;
phi[1] = 1;
for (int i = 2; i <= n; ++i)
{
if (!sie[i])
{
pri[++cnt] = i;
phi[i] = i - 1;
}
for (int j = 1; j <= cnt && 1LL * pri[j] * i <= n; ++j)
{
int x = pri[j] * i;
sie[x] = true;
if (i % pri[j] == 0)
{
phi[x] = phi[i] * pri[j];
break;
}
else
phi[x] = phi[i] * (pri[j] - 1);
}
}
}
bool flg;
int pw[MaxN];
inline int calc(int n)
{
if (flg)
return pw[n];
int res = 0;
for (int i = 0; i <= K && i < n - 1; ++i)
res = (res + 1LL * (i + 1) * f[n - i - 2]) % mod;
if (n - 1 <= K)
add(res, n);
return res;
}
int main()
{
freopen("loop.in", "r", stdin);
freopen("loop.out", "w", stdout);
std::cin >> n >> K;
flg = n == K;
sieve_init(n);
f[0] = pw[0] = 1;
for (int i = 1; i <= n; ++i)
{
add(f[i] = f[i - 1], f[i - 1]);
if (i >= K + 2)
dec(f[i], f[i - K - 2]);
else if (i == K + 1)
dec(f[i], 1);
pw[i] = pw[i - 1] * 2LL % mod;
}
int ans = 0;
for (int i = 1; i <= n; ++i)
if (n % i == 0)
add(ans, 1LL * phi[n / i] * calc(i) % mod);
std::cout << 1LL * ans * qpow(n, mod - 2) % mod << std::endl;
return 0;
}
6.1.3 「SDOI2013」项链
项链由 \(n\) 个珠子串成一个环。
每个珠子是一个正三棱柱,正三棱柱的三个侧面刻有数字,每个数字 \(x\) 需要满足 \(1 \leq x \leq a\),并且珠子上的三个数字的 \(\gcd=1\)。两个珠子相同,当且仅当三棱柱通过旋转或翻转能相同(对应面的数字相同)。
项链的相邻的两个珠子必须不同。两个项链如果能通过旋转变成一样的,那么认为两个项链相同。
求有多少种不同的项链,对 \(10^9+7\) 取模。共有 \(T\) 组数据。
\(n \leq 10^{14}\),\(a \leq 10^7\),\(T \leq 10\)
时空限制:\(3\texttt s/128\texttt{MB}\)
首先我们先计算珠子有多少种。考虑 Burnside 引理,珠子的旋转和翻转形成的置换群一共有 \(6\) 个置换。每个置换下的不动点数,就是将置换分解为 \(k\) 个轮换后,最大公约数为 \(1\) 的有序 \(k\) 元组的数量。
设 \(S_k\) 表示最大公约数为 \(1\) 的有序 \(k\) 元组的数量。那么不同的珠子数 \(m\) 就是
通过莫比乌斯反演,我们可以算出 \(S_k=\sum\limits_{i=1}^a\mu(i){\lfloor\frac ai\rfloor}^k\),具体推导略,用整除分块算即可。
接着再用 Burnside 引理算不同项链数,根据套路,答案就是
其中 \(f(d)\) 表示 \(d\) 个珠子的环的方案数(不考虑旋转同构)。
这样算需要面临的问题是 \(n\) 可能是 \(p=10^9+7\) 的倍数。但是因为 \(n\) 还不可能是 \(p^2\) 的倍数,所以这个时候我们将模数设成 \(p^2\)。有除法的地方只有最后这里,因为 \(n=kp\),所以 \(\sum_{d|n}f(d)\varphi(\frac{n}{d})\) 一定有 \(p\) 这个因子,在模 \(p^2\) 意义下一定还是有 \(p\) 这个因子。因此最后算出来只要将模数和答案同除以 \(p\),然后乘上 \(k\) 在模 \(p\) 意义下的逆元即可。
接下来我们只考虑求 \(f(n)\)。考虑插入第 \(n\) 个珠子,若原来的 \(1\) 和 \(n-1\) 颜色不同,那么再插入一个就是 \((m-2)f(n-1)\)。如果 \(1\) 和 \(n-1\) 相同,那么就是 \((m-1)f(n-2)\)。
所以 \(f(n)=(m-2)f(n-1)+(m-1)f(n-2)\)。为了使后面的递推正确,令 \(f(0)=m,f(1)=0\)。到这其实可以矩乘优化了,但是发现这是二阶常系数线性齐次递推式,其实可以推导出通项公式。
reference:https://blog.csdn.net/bzjr_Log_x/article/details/104225410
具体地,我们用特征根法,解出方程 \(\lambda^2=(m-2)\lambda+m-1\) 的两个特征根 \(\lambda_1=-1,\lambda_2=m-1\)。
那么 \(f(n)=A\lambda_1^n+B\lambda_2^n\),代入解得 \(f(n)=(-1)^n(m-1)+(m-1)^n\)。
时间复杂度 \(\mathcal O(T\sigma_0(n)\log n+T\sqrt n)\)。
#include <bits/stdc++.h>
template <class T>
inline void relax(T &x, const T &y)
{
if (x < y) x = y;
}
typedef long long s64;
typedef long double ld;
const s64 mod1 = 1e9 + 7;
const s64 mod2 = mod1 * mod1;
const s64 inv6_1 = (mod1 + 1) / 6;
const s64 inv6_2 = 833333345000000041LL;
s64 mod, inv6;
inline void add(s64 &x, const s64 &y)
{
x += y;
if (x >= mod)
x -= mod;
}
inline void dec(s64 &x, const s64 &y)
{
x -= y;
if (x < 0)
x += mod;
}
inline s64 qmul(s64 x, s64 y)
{
if (mod == mod1)
return 1LL * x * y % mod;
else
{
s64 res = x * y - (s64)((ld)x * y / mod + 1e-14) * mod;
return res < 0 ? res + mod : res;
}
}
inline s64 qpow(s64 x, s64 y)
{
s64 res = 1;
for (; y; y >>= 1, x = qmul(x, x))
if (y & 1)
res = qmul(res, x);
return res;
}
const int MaxN = 1e7 + 5;
int n_pri, pri[MaxN], miu[MaxN];
inline void sieve_init(int n)
{
miu[1] = 1;
static bool sie[MaxN];
for (int i = 2; i <= n; ++i)
{
if (!sie[i])
{
pri[++n_pri] = i;
miu[i] = -1;
}
for (int j = 1; j <= n_pri && i * pri[j] <= n; ++j)
{
int x = i * pri[j];
sie[x] = true;
if (i % pri[j] == 0)
{
miu[x] = 0;
break;
}
else
miu[x] = -miu[i];
}
}
for (int i = 1; i <= n; ++i)
miu[i] += miu[i - 1];
}
int a;
s64 n, ans, m, p[MaxN];
int cnt, c[MaxN];
inline s64 f(s64 n)
{
s64 res = qpow(m, n);
return (n & 1 ? dec(res, m) : add(res, m)), res;
}
inline void dfs(int k, s64 d = 1, s64 phi = 1)
{
if (k > cnt)
{
add(ans, qmul(f(d), phi % mod));
return;
}
s64 prod = 1;
for (int i = 1; i <= c[k]; ++i)
prod *= p[k];
dfs(k + 1, d * prod, phi);
s64 cur = 1;
for (int i = 1; i <= c[k]; ++i)
{
prod /= p[k], cur *= p[k] - (i == 1);
dfs(k + 1, d * prod, cur * phi);
}
}
inline void case_answer(s64 cx_n, int cx_a)
{
n = cx_n, a = cx_a;
if (n % mod1 == 0)
mod = mod2, inv6 = inv6_2;
else
mod = mod1, inv6 = inv6_1;
m = 2;
for (int i = 1, nxt; i <= a; i = nxt + 1)
{
int d = a / i;
nxt = a / (a / i);
s64 delta = (miu[nxt] - miu[i - 1] + mod) % mod;
s64 val = (3LL * d * d % mod + qmul(1LL * d * d % mod, d)) % mod;
add(m, qmul(val, delta));
}
m = qmul(inv6, m);
--m;
s64 x = n; cnt = 0;
for (int j = 1, i; i = pri[j], j <= n_pri && 1LL * i * i <= n && x > 1; ++j)
if (x % i == 0)
{
p[++cnt] = i, c[cnt] = 0;
while (x % i == 0)
++c[cnt], x /= i;
}
if (x > 1) ++cnt, c[cnt] = 1, p[cnt] = x;
ans = 0;
dfs(1);
if (mod == mod1)
std::cout << 1LL * ans * qpow(n % mod, mod - 2) % mod << '\n';
else
{
ans /= mod1, n /= mod1;
mod = mod1;
std::cout << 1LL * ans * qpow(n % mod, mod - 2) % mod << '\n';
}
}
int main()
{
int orzcx;
static s64 cx_n[11];
static int cx_a[11], max_a;
scanf("%d", &orzcx);
for (int i = 1; i <= orzcx; ++i)
scanf("%lld%d", &cx_n[i], &cx_a[i]), relax(max_a, std::max((int)sqrt(cx_n[i]), cx_a[i]));
sieve_init(max_a);
for (int i = 1; i <= orzcx; ++i)
case_answer(cx_n[i], cx_a[i]);
return 0;
}
6.1.4 「Celeste-B」Say Goodbye
给定 \(n\) 个珠子和 \(m\) 种颜色,第 \(i\) 种颜色的珠子恰有 \(a_i\) 种。保证 \(\sum a_i=n\)。
将这 \(n\) 个珠子串成一个环长 \(\geq 2\) 的无向基环树。基环树的两个子树不同当且仅当它们对应点的颜色不同或者这两棵子树不同构,两棵子树同构当且仅当,两个根的每个儿子的子树对应同构,和一般的树的计数不同,这里的儿子是有顺序的。
例如下面的几种部分串法是互不相同的
如果两个基环树能通过旋转基环得到同样的结果,那么这两种基环树本质上是相同的。求有多少本质不同的基环树。
\(m \leq n \leq 2\times 10^5\)
时空限制:\(1\texttt{s}/512\texttt{MB}\)
reference:https://www.cnblogs.com/PinkRabbit/p/11525881.html
首先考虑不染色的情况下的 \(n\) 个点的无标号有根有序树(儿子有序)的计数。考虑这棵树的括号序列,因为最外层必须只有一对括号,所以方案数就是 \(\mathrm{Cat}_{n-1}\),其中 \(\mathrm{Cat}\) 是卡特兰数。
那么环长为 \(k\) 的基环树相当于 \(k\) 棵大小之和为 \(n\) 的无标号有根有序树。我们枚举环长,然后用 Burnside 引理计算答案,为了后续推导方便,写成
其中 \(f_k(d)\) 表示环长为 \(k\) 的基环树,限制环上\(\bmod d\) 相同的位置的子树要完全一样的方案数(不考虑旋转同构)。那么 \(f_k(\frac k d)\) 就是要求\(\bmod \frac k d\) 相同的位置要完全一样,即每个等价类的元素个数为 \(d\) 个。
那么我们就需要将所有点平均分成 \(d\) 份,然后求用其中的一份拼成 \(\frac k d\) 棵大小之和为 \(\frac n d\) 的无标号有根有序树的方案数。无标号有根有序树的 OGF 就是卡特兰数的 OGF 乘上 \(x\),即 \(xC\),因为每一棵大小都不能为 \(0\),因此常数项为 \(0\)。那么 \(k\) 棵大小之和为 \(n\) 的树的方案数就是 \([x^n](xC)^k\)
那么答案就可以写成
其中 \(g(d)=\frac{(n/d)!}{\prod(a_i/d)!}\),表示可重集排列的方案数,即安排好树的形态后染色的方案数,计算一次需要 \(\mathcal O(m)\)。
接着有两条路可走,一条路是暴力生成函数
时间复杂度 \(\mathcal O(n\log n+\sigma_0(n)m)\)。
另一条路是智慧组合意义(我想不到):有一个很牛逼的性质是
证明:考虑 \([x^n]C^m\) 的组合意义,即所有合法括号序列划分成 \(m\) 段合法括号序列的方案数之和。
卡特兰数的其中一种含义是看成 \(n\) 个 \(+1\) 和 \(n\) 个 \(-1\) 的任意排列,使得任意前缀和都 \(\geq 0\)。
为了方便计算划分方案数,我们可以将合法划分看成在某些原本前缀和 \(=0\) 的位置后面加上一个 \(-1\),看成在这里划一段。故一种合法划分方案就能映射到 \(n\) 个 \(+1\) 和 \(n+m-1\) 个 \(-1\) 的任意排列,使得任意前缀和都 \(\geq -m+1\)。
那么我们考虑能否构造映射回来的方法,不难发现,第一段的末尾就是第一个前缀和为 \(-1\) 出现的位置、第二段的末尾就是第一个前缀和为 \(-2\) 出现的位置……这样我们也构造了使得任意前缀和都 \(\geq -m+1\) 的 \(n\) 个 \(+1\) 和 \(n+m-1\) 个 \(-1\) 的任意排列,到合法划分方案的映射。
这样我们就建立了两者的双射。因此方案数相同。使得任意前缀和都 \(\geq -m+1\) 的 \(n\) 个 \(+1\) 和 \(n+m-1\) 个 \(-1\) 的任意排列数即为 \(\binom{2n+m-1}{n}-\binom{2n+m-1}{n-1}\)。
那么直接根据这个式子计算即可
时间复杂度 \(\mathcal O(\sigma(n)+\sigma_0(n)m)\)。
#include <bits/stdc++.h>
template <class T>
inline void read(T &x)
{
static char ch;
while (!isdigit(ch = getchar()));
x = ch - '0';
while (isdigit(ch = getchar()))
x = x * 10 + ch - '0';
}
const int mod = 998244353;
inline void add(int &x, const int &y)
{
if (x += y, x >= mod)
x -= mod;
}
inline void dec(int &x, const int &y)
{
if (x -= y, x < 0)
x += mod;
}
inline int minus(int x, int y)
{
return x -= y, x < 0 ? x + mod : x;
}
const int MaxN = 2e5 + 5;
const int MaxM = MaxN * 3;
int phi[MaxN];
int fac[MaxM], fac_inv[MaxM], ind[MaxM];
inline void phi_init(int n)
{
for (int i = 1; i <= n; ++i)
{
phi[i] += i;
for (int j = i << 1; j <= n; j += i)
dec(phi[j], phi[i]);
}
}
inline void fac_init(int n)
{
fac[0] = fac_inv[0] = 1;
for (int i = 1; i <= n; ++i)
{
fac[i] = 1LL * fac[i - 1] * i % mod;
ind[i] = i == 1 ? 1 : 1LL * ind[mod % i] * (mod - mod / i) % mod;
fac_inv[i] = 1LL * fac_inv[i - 1] * ind[i] % mod;
}
}
inline int C(int n, int m)
{
if (n < 0 || m < 0 || n < m) return 0;
return 1LL * fac[n] * fac_inv[m] % mod * fac_inv[n - m] % mod;
}
inline int catalan_powm(int n, int m)
{
return minus(C(n * 2 + m - 1, n), C(n * 2 + m - 1, n - 1));
}
int n, m, cnt[MaxN];
inline int prod_C(int d)
{
int res = fac[n / d];
for (int i = 1; i <= m; ++i)
res = 1LL * res * fac_inv[cnt[i] / d] % mod;
return res;
}
int main()
{
read(n), read(m);
int D = n;
for (int i = 1; i <= m; ++i)
read(cnt[i]), D = std::__gcd(D, cnt[i]);
phi_init(n);
fac_init(n * 3);
int ans = 0;
for (int d = 1; d <= D; ++d)
if (D % d == 0)
{
int prod = 1LL * phi[d] * ind[d] % mod * prod_C(d) % mod;
int sum = 0;
for (int k = 1, lim = n / d; k <= lim; ++k)
sum = (sum + 1LL * ind[k] * catalan_powm(lim - k, k)) % mod;
ans = (ans + 1LL * prod * sum) % mod;
}
dec(ans, 1LL * catalan_powm(n - 1, 1) * prod_C(1) % mod);
std::cout << ans << std::endl;
return 0;
}
6.2 无标号图的计数
6.2.1 「SHOI2006」有色图
弱化版:「HNOI2009」图的同构记数(只需要将边的有无,看成边的染色就和这题完全一样了)
\(n\) 个点的完全图,点没有颜色,边有 \(m\) 种颜色,问本质不同的图的数量对质数 \(p>n\) 取模。
本质不同指的是在点的 \(n!\) 种不同置换下不同。
\(n \leq 53\)
时空限制:\(1\texttt s/128\texttt{MB}\)
注意到本题的染色是边到颜色的映射,而给出的置换是对点的置换,我们需要求出对应的边置换才可以用 Burnside 引理。
首先我们考虑有标号图上的点置换对边的作用。对于一个点置换 \(\left(\begin{array}{c}{1,2,\cdots,n} \\{p_1,p_2,\cdots,p_n}\end{array}\right)\),我们就令对应的边置换为 \(\left(\begin{array}{c}{(1,2),(1,3),\cdots,(i,j),\cdots,(n-1,n)} \\{(p_1,p_2),(p_1,p_3),\cdots,(p_i,p_j),\cdots(p_{n-1},p_n)}\end{array}\right)\),即我们建立了点置换到边置换的映射。
再考虑令这里的每个边置换对应到唯一的点置换,将点置换和边置换都分解成不相交的轮换,对于一个点置换中的轮换 \((a_1,a_2,\cdots,a_{k-1},a_k)\),在边置换中一定存在这么一个轮换 \(\big((a_1,a_2),(a_2,a_3),\cdots,(a_{k-1},a_k),(a_k,a_1)\big)\)。因此对于边置换中满足首尾相连条件的轮换,我们就可以对应到一个点轮换。
因为这里的边置换都是点置换映射到的,因此这里边置换的乘法我们就可以看成对应的点置换的乘法,因此就满足封闭性、结合律、单位元、逆元的性质。至此,我们证明了点置换和边置换可以建立双射,且这些边置换构成一个群,大小也是 \(n!\)。
我们将点置换分解成不相交的轮换,然后考虑对应的边置换能分解成多少轮换,那么就可以求出在当前置换下的不动染色方案数。
先考虑边的两个端点属于同一个轮换的情况。如下图,相同颜色的边(所有距离相同的点对之间的边)表示属于同一个边轮换,需要染同一种颜色。(有的边带箭头是为了看得清楚一点)。发现奇数和偶数的情况不太一样,综合一下两个端点都属于同一个大小为 \(k\) 的点轮换的边,构成了 \(\lfloor\frac k 2\rfloor\) 个不相交的边轮换。
然后考虑边的两个端点属于不同轮换的情况。考虑两个大小分别为 \(a_1,a_2\) 的点轮换,连接两个点置换的边一共有 \(a_1a_2\) 条。考虑对于其中一条边,这两个点置换同时作用在边的两个端点,我们发现恰好作用 \(\mathrm{lcm}(a_1,a_2)\) 次后两个端点都回到原位,即这 \(a_1a_2\) 条边分解成不相交的边轮换后,每个边轮换的大小为 \(\mathrm{lcm}(a_1,a_2)\),即边轮换的个数为 \(\frac{a_1a_2}{\mathrm{lcm}(a_1,a_2)}=\gcd(a_1,a_2)\)。
那么总的染色数就是 \(m\) 的边轮换个数次方。即我们假设每个点轮换的大小为 \(a_1,a_2,\cdots,a_k\),其中 \(\sum a_i=n\)。那么这个点置换对应的边置换下,不动的边染色方案数就是
我们开始的想法是枚举点置换,并以此计算对应边置换的轮换数,从而算出在这种边置换下的不动染色方案数。但是点置换的数量太多了,我们没有办法枚举所有的点置换。
但是实际上,如果两个置换分解出的不相交的轮换,大小划分相同,那么这两种置换本质上是没有区别的。那么我们可以就枚举 \(n\) 的所有划分方案(写成若干个正整数相加的形式),然后计算这种划分对应多少种置换,并计算这种划分的答案,那么我们就有办法计算了。
对于一种划分方案 \(a_1\leq a_2\leq \cdots \leq a_k\),\(\sum a_i =n\),其对应的置换数一共有
其中 \(c_i\) 表示这个划分中大小为 \(i\) 的轮换个数。
可以理解为,首先先划分每个点属于哪个轮换,方案数是可重集排列数 \(\frac{n!}{\prod a_i!}\)。接着每个轮换内部是一个圆排列,方案数即为 \(\prod (a_i-1)!\)。最后还要考虑轮换之间是没有区别的,需要除以相同大小的轮换数阶乘乘积 \(\prod c_i!\)。
那么因为 \(n!\) 与总的置换数抵消了,答案就是
时间复杂度为 \(\mathcal O\left(\sum\limits_{p\in \text{Partition}(n)}\mathrm{len}(p)^2\log n\right)\),实际比较小。
#include <bits/stdc++.h>
const int MaxN = 66;
int n, m, mod;
int fac[MaxN], fac_inv[MaxN], ind[MaxN];
int ans;
int cnt, a[MaxN];
inline void add(int &x, const int &y)
{
x += y;
if (x >= mod)
x -= mod;
}
inline int qpow(int x, int y)
{
int res = 1;
for (; y; y >>= 1, x = 1LL * x * x % mod)
if (y & 1)
res = 1LL * res * x % mod;
return res;
}
inline void fac_init(int n)
{
fac[0] = fac[1] = fac_inv[0] = fac_inv[1] = ind[1] = 1;
for (int i = 2; i <= n; ++i)
{
fac[i] = 1LL * fac[i - 1] * i % mod;
ind[i] = 1LL * ind[mod % i] * (mod - mod / i) % mod;
fac_inv[i] = 1LL * fac_inv[i - 1] * ind[i] % mod;
}
}
inline void dfs(int k, int rest, int lst = 1)
{
if (!rest)
{
cnt = k - 1;
int res = 0;
for (int i = 1; i <= cnt; ++i)
{
res += a[i] >> 1;
for (int j = i - 1; j >= 1; --j)
res += std::__gcd(a[i], a[j]);
}
int cur = qpow(m, res), lst = 0, c = 0;
for (int i = 1; i <= cnt; ++i)
{
if (lst == a[i])
++c;
else
{
cur = 1LL * cur * fac_inv[c] % mod;
lst = a[i], c = 1;
}
cur = 1LL * cur * ind[a[i]] % mod;
}
if (c > 1)
cur = 1LL * cur * fac_inv[c] % mod;
add(ans, cur);
}
for (int i = lst; i <= rest; ++i)
{
a[k] = i;
dfs(k + 1, rest - i, i);
}
}
int main()
{
std::cin >> n >> m >> mod;
fac_init(n);
dfs(1, n);
std::cout << ans << '\n';
return 0;
}
6.2.2 画画
求有多少个本质不同的无重边无自环的无向图,使得每个连通块都有欧拉回路。
两个图本质相同,当且仅当存在一个点到点的置换,使得对于原图和在置换作用下的新图,任意两点之间要么都没有连边,要么都有连边。
\(1 \leq n \leq 50\)
首先每个连通块都有欧拉回路,当且仅当每个点的度数均为偶数。边的有无仍然可以看成染色。
那么和上面那题类似,我们可以在点置换和边置换之间建立双射,并且用 Burnside 引理转化成在有标号图上算每个置换下的不动染色方案数。
现在的问题就是怎么计算合法的不动染色方案数,我们先考虑将边置换分解成不相交的边轮换,那么每个边轮换内部的边必须同时选择或者同时不选择,然后看看每个边轮换中的边选择或者不选择会有什么影响。
对于边的两个端点都在同一个大小为 \(k\) 的点轮换的边,根据前一题的推理,会有如下图的 \(\lfloor \frac k 2\rfloor\) 个边轮换。但是我们发现,这 \(\lfloor \frac k 2\rfloor\) 个边轮换产生的效果不一定相同。具体地:
当 \(k\) 是偶数时,有 \(\frac{k-1}{2}\) 个边轮换,它们选或不选都不会改变点度数的奇偶性,故可以任意染色,方案数是 \(2^{\frac{k-1}{2}}\)。
当 \(k\) 是奇数时,有 \(\frac{k}{2}-1\) 个边轮换选或不选都不会改变点度数的奇偶性,方案数是 \(2^{\frac{k}{2}-1}\)。但是 恰有一个边轮换,如果选了这个边轮换的边,那么这 \(k\) 个点的度数奇偶性均取反。
(是的,你没看错,和上面那题的图一模一样)
接着我们考虑边的两个端点不在同一个点轮换的边。对于两个大小分别为 \(a_1,a_2\) 的点轮换,根据上一题的推理,我们得到了 \(\frac{a_1a_2}{\mathrm{lcm}(a_1,a_2)}=\gcd(a_1,a_2)\) 个边轮换,每个边轮换有 \(\mathrm{lcm}(a_1,a_2)\) 条边。
那么在一个边轮换中,和 \(a_1\) 的每个点相连的边有 \(e_1=\frac{\mathrm{lcm}(a_1,a_2)}{a_1}=\frac{a_2}{\gcd(a_1,a_2)}\) 条,和 \(a_2\) 的每个点相连的边有 \(e_2=\frac{a_1}{\gcd(a_1,a_2)}\) 条。
根据 \(e_1,e_2\) 的奇偶性,我们就能知道每个边轮换选或不选产生的影响。
- 若 \(e_1,e_2\) 均为偶数,那么每个边轮换选或不选都不改变点的奇偶性,方案数是 \(2^{\gcd(a_1,a_2)}\)。
- 若 \(e_1,e_2\) 均为奇数,那么每个边轮换选择即会使两个点轮换中的每个点的度数奇偶性均取反。
- 若 \(e_1,e_2\) 中恰有一个奇数,每个边轮换选择即会使其中一个点轮换中的每个点的度数奇偶性均取反。
那么因为取反操作都是基于整个点轮换的,那么我们可以将每个点轮换缩成一个点。对于任意选取而不影响奇偶性的边轮换,我们直接乘上 \(2\) 的次幂的系数。
简化之后的问题就是,给定一张无向图,每个点有自然数点权,每条边有自然数边权——每个点 \(x\) 的点权 \(v_x\) 表示有 \(v_x\) 次机会将这个点的度数奇偶性取反,每条边 \(e\) 的边权 \(w_e\) 表示有 \(w_e\) 次机会将这条边的两个端点的度数奇偶性一起取反。
对于边权不为 \(0\) 的边形成的点连通块,我们可以分开考虑。对于某个点数为 \(s\) 的连通块,考虑这个连通块的某棵生成树,如果我们确认了点的选择情况和非树边的选择情况(选择情况指的是操作次数的奇偶性),那么可以证明,如果恰好选择了偶数个点改变奇偶性,树边的选择情况是唯一确定的;否则不存在合法情况。
首先考虑,如果改变了奇数个点的奇偶性,因为改变一次边总是改变两个点的奇偶性,所有点的奇偶性之和仍是奇数,显然不存在合法情况。接着如果我们确定了点和非树边的选择情况,那么树边的选择情况就能唯一确定(可以考虑从叶子推上来)。
因此对于一个大小为 \(s\) 的连通块,方案数就是
那么时间复杂度就是 \(\mathcal O\left(\sum\limits_{p\in \text{Partition}(n)}\mathrm{len}(p)^2\log n\right)\),可以通过本题。
#include <bits/stdc++.h>
const int MaxN = 66;
const int mod = 998244353;
inline void add(int &x, const int &y)
{
x += y;
if (x >= mod)
x -= mod;
}
inline int qpow(int x, int y)
{
int res = 1;
for (; y; y >>= 1, x = 1LL * x * x % mod)
if (y & 1)
res = 1LL * res * x % mod;
return res;
}
int n;
int fac[MaxN], fac_inv[MaxN], ind[MaxN];
int ans;
int cnt, a[MaxN];
inline void fac_init(int n)
{
fac[0] = fac[1] = fac_inv[0] = fac_inv[1] = ind[1] = 1;
for (int i = 2; i <= n; ++i)
{
fac[i] = 1LL * fac[i - 1] * i % mod;
ind[i] = 1LL * ind[mod % i] * (mod - mod / i) % mod;
fac_inv[i] = 1LL * fac_inv[i - 1] * ind[i] % mod;
}
}
inline int calc_same()
{
int cur = 1;
int lst = 0, c = 0;
for (int i = 1; i <= cnt; ++i)
{
if (lst == a[i])
++c;
else
{
cur = 1LL * cur * fac_inv[c] % mod;
lst = a[i], c = 1;
}
cur = 1LL * cur * ind[a[i]] % mod;
}
if (c > 1)
cur = 1LL * cur * fac_inv[c] % mod;
return cur;
}
int deg[MaxN], ufs[MaxN];
int sze[MaxN], sump[MaxN], sume[MaxN];
inline void ufs_init(int n)
{
for (int i = 1; i <= n; ++i)
{
ufs[i] = i;
sze[i] = 1;
sump[i] = sume[i] = 0;
}
}
inline int ufs_find(int x)
{
if (x == ufs[x])
return x;
return ufs[x] = ufs_find(ufs[x]);
}
inline void ufs_merge(int x, int y)
{
x = ufs_find(x);
y = ufs_find(y);
if (x == y) return;
ufs[y] = x;
sze[x] += sze[y];
sump[x] += sump[y];
sume[x] += sume[y];
}
inline int solve()
{
ufs_init(cnt);
int res = 0;
for (int i = 1; i <= cnt; ++i)
{
if (~a[i] & 1)
++sump[ufs_find(i)];
res += (a[i] - 1) >> 1;
for (int j = 1; j < i; ++j)
{
int d = std::__gcd(a[i], a[j]);
int n1 = a[j] / d, n2 = a[i] / d;
if ((n1 & 1) && (n2 & 1))
{
ufs_merge(i, j);
sume[ufs_find(i)] += d;
}
else if (n1 & 1)
sump[ufs_find(i)] += d;
else if (n2 & 1)
sump[ufs_find(j)] += d;
else
res += d;
}
}
for (int i = 1; i <= cnt; ++i)
if (ufs_find(i) == i)
res += std::max(sump[i] - 1, 0) + sume[i] - (sze[i] - 1);
return res;
}
inline void dfs(int k, int rest, int lst = 1)
{
if (!rest)
{
cnt = k - 1;
add(ans, 1LL * qpow(2, solve()) * calc_same() % mod);
}
for (int i = lst; i <= rest; ++i)
{
a[k] = i;
dfs(k + 1, rest - i, i);
}
}
int main()
{
std::cin >> n;
fac_init(n);
dfs(1, n);
std::cout << ans << '\n';
return 0;
}
6.3 其他问题
先咕咕咕
6.3.1 烷基计数和烷烃计数
6.3.2 无标号有根树计数和无标号无根树计数
注:该问题的重点应当是生成函数和多项式,Burnside 引理只在其中一种解决方法中起到较小的作用。