欢迎使用皮肤 Geek|

Tsawke

园龄:2年7个月粉丝:6关注:2

浅析群论

Group Theory - 浅析群论

更好的阅读体验戳此进入

Tips:不保证正确性,不保证没锅,部分定理可能只是口糊的证明。

称集合 G 和作用于集合 G 的二元运算 为一个群,记作 (G,),这里我们简记为 G,当且仅当其满足以下 4 个性质:

  1. 封闭性(closure)

a,bG,则 abG

  1. 结合律(associativity)

a,b,cG,则 (ab)c=a(bc)

  1. 单位元(identity element,亦称幺元)

eG 满足 aG,ae=ea=a,则 e 称为单位元,其有且仅有一个。

考虑证明,有两个不同的单位元 e1,e2,显然应有 $ e_1 \circ e_2 = e_1 = e_2 \texttt{QED} $。

然后根据这个还有个性质:若有 ax=ax=e

大概的证明:

ax=aa1ax=a1aex=ex=e

  1. 逆元(inverse element)

aG,总 a1G,使得 aa1=a1a=e,则 a1a 的逆元,有且仅有一个。

考虑证明,首先需要另一个性质,即:$ (a{-1}) = a a a $。

证明:

(aa1)(a1)1=e(a1)1=(a1)1

a(a1(a1)1)=ae=a

则有:

(a1)1=a

然后考虑证明逆元唯一,如果逆元不唯一,我们可以把刚才的性质换一种叙述,任意元素的任意逆元的任意逆元均为元素本身。首先我们可以把群中每个元素作为一个点,逆元的关系当作边,抽象成一个图,由刚才的性质显然应为无向边。如果一个点有多个逆元,那么其应有多条边,且连接的所有其它点度数应仅为 1,否则不满足性质,但是此时其连结的点所连结的点(也就是原来的点)的度数不为 1 了,显然矛盾。或者换句话说,这样的图中所有点度数都应为 1,这个很好理解,而度数均为 1 即逆元唯一,QED

e.g. 如整数集和整数间加法构成群 (Z,+),单位元为 0,逆元为其相反数。

在实际应用上可以认为树状数组的元素和运算符必须满足群,如 minmax 就因为没有逆元所以不能用树状数组维护。

其它群

半群

G 是一个半群,当且仅当其满足封闭性与结合律。

幺半群(亚群、含幺子群、独异点)

G 是一个幺半群,当且仅当其满足封闭性与结合律,且存在单位元。

交换半群

G 是一个交换半群,当且仅当其满足封闭性与结合律,且其中的运算 满足交换律。

交换幺半群(交换亚群)

G 是一个幺半群,当且仅当其满足封闭性与结合律,且存在单位元,且其中的运算 满足交换律。

双半群模型

首先明确一下,该模型一般适用于对于修改与查询的范围有限制的问题中,如:序列区间,树上简单路径,二维平面矩形,高位正交范围,半平面范围,圆范围。

这里的 2I0 表示 I0 的幂集,即 I0 的所有子集构成的集合。

具体来说,给定初始集合 I0,一个有限集合 II0,询问集合 Q2I0,s.t.I0Q,交换半群 (D,+),半群 (M,),且二元运算符 满足 DMD

满足结合律,且满足 + 的分配律,即:(a+b)c=ac+bc

存在初始权值 d0:ID。(个人理解的含义是指 I 中元素的权值在交换半群 (D,+) 中)

按序执行 m 个操作,操作为修改或查询,第 t 个操作定义为:

  1. 范围修改,给定 qtQ,ftM,定义若 xqt,则 dt(x)=dt1(x)ft,反之 dt(x)=dt1(x)
  2. 范围查询,给定 qtQ,定义 dt(x)=dt1(x),对所有 xI,答案则为 xqtdt(x)

所有操作与初始权值均离线给出,用 n=|I| 表示初始权重的个数,用 m 表示操作数。

对于满足上述大量限制的模型,我们称为双半群模型。

而为了能够通过线段树等数据结构维护,一般维护的信息都一定需满足半群性质,且大多为交换半群的性质。

而对于一道具体的题目,我们的目的就是去设计 D,M 而满足上述的双半群模型。

再具体的我也就不会了,总之这似乎是一个很高妙的东西,已知存在的一个可以用的题 LG-P8868 [NOIP2022] 比赛

定义群的阶为群中元素的数量,记作 |G|

子群

HG,且 (H,) 构成群,那么称 (H,)(G,) 的一个子群,简记为 HG

陪集

定义与性质

考虑存在 HG,且有 gG

我们称 ghhH(H,)(G,) 内关于 g 的左陪集,记作 gH

同理对于 hghH 为右陪集,记作 Hg

对于右陪集,满足以下 6 个性质(左陪集同理):

  1. gG,|H|=|Hg|

证明:对于 h1,h2H,如果 h1g=h2g,那么 $ h_1 = h_2 \texttt{QED} $。

  1. gG,gHg

证明:单位元唯一,子集也成群,则子集一定包含该单位元,则有 $ e \circ g \in \mathbb{H}g \Longleftrightarrow g \in \mathbb{H}g \texttt{QED} $。

  1. gHHg=H

证明:充分性由封闭性易证。必要性:考虑单位元 e,由题意显然有 $ e \circ g = g \in \mathbb{H} \texttt{QED} $。

  1. Ha=Hbab1H

证明:充分性:$ \mathbb{H}a = \mathbb{H}b \Longleftrightarrow \mathbb{H} a \circ b^{-1} = \mathbb{H} b \circ b^{-1} \Longleftrightarrow \mathbb{H} a \circ b^{-1} = \mathbb{H} 3 a \circ b^{-1} \in \mathbb{H} \texttt{QED} $。必要性反之亦然。

  1. HaHbHa=Hb

换句话说,一个子群的陪集的交集要么为空,要么相等。

证明:假设有 cHacHb,且 HaHb,则 h1,h2H,h1a=h2b=c,同 一个 h11b1 则有 ab1=h2h11,显然 h2h11H,则 ab1H,由性质4可得 $ \mathbb{H}a = \mathbb{H}b \texttt{QED} $。

  1. H 的所有右陪集的并为 G

证明:显然 eH,则对于每个关于 g 的右陪集都含有 eg=g,所有右陪集则包含了 G 中的每一个元素,即为 $ G \texttt{QED} $。

常见表述

若满足 HG,则 G/H 表示 gHgG,即 G 中所有 H 的左陪集。

若满足 HG,则 [G:H] 表示 GH 的不同陪集的数量。

拉格朗日定理

对于有限群 GH,若 HG,则有 |H||G|,也就是 |G|0mod|H|,即 H 的阶整除 G 的阶。

还有一种表述 |H|×[G:H]=|G|

证明很显然,考虑性质1,5,6易证。

置换

定义

一般用双行表示法表示一个置换,即如:

σ=(1234524153)

比较易于理解,即用第二个元素替换第一个元素,第四个替换第二个,以此类推。

特别地,不失一般性,若前者排列为 (1,2,3,,n),那么不妨直接简记后者排列为置换,即:

σ=(a1,a2,a3,,an)

所以不难发现,我们可以认为一个序列,或者更严谨的说,一个排列,既可以是一个序列,也可以是一个置换

同时可以证明,不论表示为双行亦或单行,长度为 n 的置换个数均为 n!,对于单行表示证明平凡,对于双行可以从定义的角度理解,对于本质相同的双行表示可以认为是同一种单行表示,故置换个数相同。

同时需要注意一点,对于抽象代数中,或者更具体地说,对于群论中,我们认为置换即为集合(通常为有限集)对自身的双射,所以置换应该认为是严格的排列,而组合数学中只要求无重复,但却是可以有阙漏的。但当然本文中的置换均指严格的双射。

运算

对于置换 σ 和序列 a 的运算,一般记作 σ×aσ(a),即 σ(a)=(aσ1,aσ2,,aσn)。一般称此过程为置换的合成。

当然更严谨地说,实际上应该是对于置换 σ 和置换 a 之间的运算,上文记作序列仅为更好理解。

具体来说,个人理解该合成运算是可以定义在置换和序列之间的,也就是对于如 a=(1,1,1,1,1),这是序列,但不是置换,但也可以与置换进行右乘(不是乘法)操作而生成新的序列,这里在群作用处会用到更多一些。

置换群

定义

称对于集合 N=1,2,,n 的若干个排列构成的集合 M,或者更严格地说,应该是这些排列代表的置换,以及置换之间的 “合成” 操作的二元运算符,即上文的 × 所构成的群 (M,×) 为置换群。

简而言之,置换群即为若干置换和置换之间的合成运算构成的群。

显然置换群存在单位元 $ e = (1, 2, \cdots, n) a \times e = e \times a = a \texttt{QED} $。

同时显然 × 运算满足结合律,证明可以通过将置换抽象成有向边理解,较为显然。

群作用

群对自身的作用

对于群 G,及 gG,用 g 左乘 G 的所有元素(包括 g),可以看成一种 G 到自身的映射:fg:GG,使得对于 gG,有 fg(x)=g×x

如此对于群中每个元素都能形成一个这样的映射,此时我们可以考虑将所有的映射放到一起,定义为一个二元的映射,即 f:G×GG。也就是说对于 g,xG,有 f(g,x)=fg(x)=g×x,对于这样的一个映射即称其为群作用

然后这个东西一般我们称其为左平移作用,同理也存在右平移作用,这里不再赘述。

群作用

更一般地,对于对于群 G 和集合 M,群中每个元素都能代表一个 MM 的映射,记映射 f:G×MM,若该映射满足以下两条公理:

结合律:对于 g1,g2G,mM,有 f(g1,f(g2,m))=f(g1×g2,m)

单位元恒等映射:对于 G 的单位元 e,有 f(e,m)=m

则称群 G 作用于集合 M 上。

不难想到该映射应为双射,考虑证明:对于 mM,gG,存在 f(g1,f(g,m))=m,而逆元唯一,若两个不同的 m 通过同一置换 g 指向了相同的 m,但 m 通过唯一的 g1 应仅能指向唯一的 $ m'' \texttt{QED} $。

但是注意,不同的 m 不可能通过相同的 g 得到相同的 m,但相同的 m 可能通过不同的 g 得到相同的 m。举个例子如 m=(1,1,1,1,1),对于任意的 g 都可以使得 mm

轨道-稳定子定理

轨道

对于作用在集合 X 上的群 $ \mathbb{G} X x x \mathbb{G} x \mathbb{G}(x) $。

稳定子

g(x)f(g,x),则稳定子为 Gx=ggG,g(x)=x

简而言之就是群中所有满足 g(x)=xg 构成的集合。

轨道-稳定子定理

抽象一点地表述,轨道-稳定子定理即为:

|Gx|×|G(x)|=|G|

即元素 x 稳定子大小和轨道的大小的乘积即为群的阶(或者说群的大小)。

证明

首先 GxG,且 eGx,证明显然。

满足结合律,封闭性,以及存在逆元易证,这里不再赘述。

我们可以先考虑一个固定的 x 以其 gG,考虑将 g 标号,不难想到一定存在如下映射关系:

xg1,g2,,gnx1

xgn+1,gn+2,,g2nx2

显然可能存在 n=1,这里记作这样仅为方便举例。当然也可以将上式记作 g1(x)=g2(x)==gn(x)=x1,。同时这里为什么每一段的长度都是相同的,或者说满足 xxig 个数相同,我们会在后文提到。

此处我们记 g 表示固定的任意一个稳定子,记 g 为固定的任意一个 G 中非稳定子的元素,记 g1 为固定的满足 xx1 的任意一个映射。

换句话说,满足 g(x)=x,g(x)x,g1(x)=x1

显然 g1(g(x))=g1(x)=x1,而对于 g1(g(x)),因 g(x)x,所以 g1(g(x))x1,这个通过我们在群作用处证明的对于固定的 g 可变的 $ m f(g, m) $ 是双射可显然证明。

此时我们将刚才的结论换种表述方式,即对于 $ g_1(x) = x_1 g_1(g(x)) = x_1 g(x) = x g_1 \circ g g_1 g_1(x) = x_1 $。

同理此结论推导至 g2,g3, 均成立。

现在我们再继续推导,仍令 g1 满足 g1(x)=x1,并固定 g1,此时再令 gG,发现如果我们做 GG 关于 g1 的陪集,其可记作:g1G=g1ggG=G,至于最后一步转换为 G,由陪集性质 3 可得 gG=G。也就是说 g1 对任意 g 运算之后结果构成的集合就是原群 G

现在我们再把目光放回证明的前半部分,只有稳定子能够使得 g1(x)=x1,那么我们考虑所有的 g1g,当且仅当 gGx,那么映射不变,而这样的话,不难发现在我们前面记录映射时用的 n,其实满足 n=|Gx|。当然这里可能会有点小问题,我们刚才已经固定了 g1,那么对于 g2 等难道不会对证明造成影响吗?显然不会。从我们之前的证明内容不难发现,我们这里找的稳定子 g 实际上是根据 xx1 找的,与 g1 只是表面上的关系,所以对于 g1,g2,gn,他们对应的 g 实际上是完全相同的。所以既然满足 n=|Gx|,而 x1,x2, 共有轨道数个,即 |G(x)| 个,每个长度又相等,自然乘起来就是整个群的阶了,即 $ \lvert \mathbb{G}^x \rvert \times \lvert \mathbb{G}(x) \rvert = \lvert \mathbb{G} \rvert \texttt{QED} $。

Tips:这里我们可以在回过头考虑为什么每一段 xxig 数量相同,从上一段文字则不难理解,能够使 g1(g(x))=x1g,共有 |Gx| 个,对于同一对应关系的不同映射之间的 g 也是等效的,那么这一段的 n 显然就是 |Gx|,所以说每一段长度一定相同且均为 |Gx|

证明2

除了上面的方法之外,仍存在一个思路:

根据拉格朗日定理可知 |Gx|×[G:Gx]=|G|

于是问题转化为需要证明 |G(x)|=[G:Gx]

也就是说稳定子的关于原群的不同陪集数量等于轨道的大小。

可以再次转化为证明每一个 g(x),或者说对于固定的 x 的每一个 g(x),都对应一个陪集。

然后这个地方的证明较为复杂,就不再赘述了。

Burnside定理

不动点

类似于稳定子,对于稳定子中 $ g(x) = x g x $ 为不动点。

对于集合 X,记 Xg 表示集合 Xg 的作用下的不动点的集合。

等价类

对于置换群 G,定义其作用于集合 X,若对于 $ x, y \in X \exists g \in \mathbb{G} 使 g(x) = y x, y $ 在同一个等价类中。

定理

同等价类的定义中,不同的等价类的数量,记作 |X/G|,为:

|X/G|=1|G|gG|Xg|

也就是说等价类的数量等于每一个 g 作用于 X 的不动点的数量的算术平均值。

证明

首先考虑把不动点转化为稳定子,较为显然:

gG|Xg|=xX|Gx|

然后用轨道-稳定子定理转化一下,即:

xX|Gx|=xX|G||G(x)|

然后把分子提出来,再将枚举 X 的元素转化为枚举 X 的每个等价类里的每个元素:

xX|G||G(x)|=|G|YX/GxY1|G(x)|

显然一个等价类的轨道的大小就是等价类的大小,所以此时 |G(x)|=|Y|

不难发现有:xY1|Y|=1

所以最终转化为:

|G|YX/G1

也就是等价类的个数了,所以最终式子化为:

gG|Xg|=|G||X/G|

然后把这个带入原定理的 RHS 即得证。

Tips

对于这个东西有一个例子,十分形象:

img

例题 #1 LG-P4980 【模板】Pólya 定理

题面

n 个点 n 条边的环用 n 种颜色给点染色,有多少种本质不同染色方案。答案对 1e9+7 取模。

Solution

原题的问题可以转化为:

在置换群 G=旋转0个,旋转1个,旋转2个,,旋转n-1个,作用于 nn 种染色方案的集合 M 得到的等价类的数量。

根据 Burnside定理 可知答案即为:

1|G|gG|Mg|

然后考虑一下这个枚举的过程。

显然对于旋转 0 的整个 M 均为不动点,所以答案的贡献为 nn

对于旋转 k 的,如果一个点是不动点,那么其应该是以 a 为循环节,且 ak,并且同时亦必须有 an,所以即为 agcd(n,k),那么也就可以认为只需要满足为任意的长度为 gcd(n,k) 的染色方案的循环的染色方案即可。

故最终答案可以转化为:

1nk=1nngcd(n,k)

然后不难发现这东西就是个标准莫反,考虑枚举 gcd

1ndnndk=1nd[gcd(k,nd)=1]

然后右边的显然是 φ(nd)

所以最终式子为:

1ndnndφ(nd)

然后大概分析一下这个的复杂度,枚举因数是 O(n) 的,快速幂是 O(logn) 的,求欧拉函数可以通过 O(n) 分解质因数之后直接枚举求得,最后复杂度似乎是 O(Tnlogn) 的,看起来完全不能通过,但是因为卡不满,这样就可以过这道题了。

然后似乎直接强行枚举 n 的因数,然后再直接强行求 φ,这样大概是 O(T(nlogn+n34)),还是看起来不能过,但是可以过。

当然,因为本题题目名就是 Polya定理 模板,那么我们可以再考虑一下 Polya 的推导方式。

Code

#define _USE_MATH_DEFINES
#include <bits/stdc++.h>

#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW(arr) void* Edge::operator new(size_t){static Edge* P = arr; return P++;}

using namespace std;

mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}

typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;

#define MOD (ll)(1e9 + 7)

template < typename T = int >
inline T read(void);

bitset < 110000 > notPrime;
basic_string < ll > Prime;

void Init(void){
    for(ll i = 2; i <= 101000; ++i){
        if(!notPrime[i])Prime += i;
        for(auto p : Prime){
            if(p * i > 101000)break;
            notPrime[p * i] = true;
            if(i % p == 0)break;
        }
    }
}
ll qpow(ll a, ll b){
    ll ret(1), mul(a);
    while(b){
        if(b & 1)ret = ret * mul % MOD;
        b >>= 1;
        mul = mul * mul % MOD;
    }return ret;
}
ll phi(ll x){
    ll ret(x);
    basic_string < ll > fact;
    for(auto p : Prime){
        if(p * p > x)break;
        if(x % p == 0)fact += p;
        while(x % p == 0)x /= p;
    }if(x != 1)fact += x;
    for(auto p : fact)ret /= p, ret *= (p - 1);
    return ret;
}

int main(){
    Init();
    int T = read();
    while(T--){
        ll N = read < ll >();
        ll ans(0);
        for(ll i = 1; i * i <= N; ++i){
            if(N % i != 0)continue;
            (ans += qpow(N, i) * phi(N / i) % MOD) %= MOD;
            if(i * i != N)(ans += qpow(N, N / i) * phi(i) % MOD) %= MOD;
        }(ans *= qpow(N, MOD - 2)) %= MOD;
        printf("%lld\n", ans);
    }
    fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
    return 0;
}

template < typename T >
inline T read(void){
    T ret(0);
    int flag(1);
    char c = getchar();
    while(c != '-' && !isdigit(c))c = getchar();
    if(c == '-')flag = -1, c = getchar();
    while(isdigit(c)){
        ret *= 10;
        ret += int(c - '0');
        c = getchar();
    }
    ret *= flag;
    return ret;
}

Polya定理

这里我们基于刚才的模板题来叙述 Polya定理。

这个东西我认为也可以看作是 Burnside定理 的推论。

考虑一个置换,用 (a1,a2,,an) 单行表示。然后我们在一般 Burnside 的时候求的是不动点的数量,于是我们考虑发现若对于这样一个置换,我们令每个 iai 连边,不难发现得到的就是若干个环,同时不难想到,我们若要使其为不动点,那么环上所有点的颜色都应相同。

令环的数量为 c(g) 为置换 g 能够形成的环数,于是 Burnside定理 的式子就可以转化为:

1|G|gGnc(g)

也就是每个环任意选择一个颜色。

然后推导是类似的,对于旋转 0 个与上文相同,对于旋转 k 个的 g,显然 c(g)=gcd(n,k)。然后式子就和刚才用 Burnside 所推导的完全相同了。

例题 #2 LG-P4727 [HNOI2009]图的同构计数

题面

一道 NP 问题,求互不同构的含 n 个点的图有多少种。

Solution

首先称两个图同构,当且仅当两个图可以通过对顶点的重标号使得两图完全相同。

然后这个东西的答案是一个数列,OEIS 上的编号是 A000088

然后我们再次回到 Burnside定理,集合 X 即为 n 个顶点可能构成的 2(n2)=2n(n1)2 个简单无向图。置换群 G 即为 [1,n] 的全排列。

然后我们的难点依然在于求不动点的数量。也就是说对于一个置换 g,有多少图在经过置换 g 之后与原图同构。

下面将会有一个与上一题,即 Polya定理 模板,或者说群论的标准套路。我们可以考虑先将图认为是完全图,然后用两种颜色对边进行染色,两种颜色表示该边存在或不存在。于是此时我们就会发现对于某一个置换 g,仍然存在一个等价类中,所有边必须均存在或不存在。

举个例子,对于一个标号为 1,2,3 的三角形完全图,如果有置换 g=(2,3,1),那么此时不难发现三条边同属一个等价类,因为你要么使三条边均存在,要么均不存在,否则按照这个置换的意义,旋转一下三角形,就会使得图不同构。此时的 g 对应的不动点即为 21=2 个。

所以说上面我们说的这一对,就是标准的 Polya定理,即 |Xg|=2c(g)

所以现在关键就在于我们如何求这个 c(g)

首先我们会发现,对于一个置换 g,其是可以被分成多个不相交的部分的,用双行表示法举个例子:

(123456231546)=(123231)+(4554)+(66)

考虑若将置换 g 拆成 k 个循环,长度分别为 b1,b2,,bk。然后不难发现有两种边:

首先考虑对于两个点均在同一段循环内的边

然后我们考虑对于一种置换中的一个长度为 b 的循环,有一个结论,这样的循环共有 b2 个等价类。

我们可以将对应的点数排成一个正 n 边形,比如 n=6 的时候,如果有这样一个置换 g=(123456 234561),存在这样的图和边:

img

不难发现对于长度相同的边都是等价的,其必须同时染或不染,否则置换后将会不同构。而不难想到,由于正 b 边形存在对称性,所以线段的长度共有 b2 种,所以等价类也就共有这些种。

于是其贡献的等价类数量为:i=1kbi2

然后考虑在不同循环中的边

举个例子:

g=(123231)+(4554)+(66)

这个东西的图大概是这样的:

img

如对于长度为 b1,b2 的两个循环,那么两者之间共有 b1×b2 条边,所以在这两者之间的边,每次经过 lcm(b1,b2) 次置换之后就会回到原位,则等价类的大小,或者说环长为 lcm(b1,b2),那么此时等价类个数,也就是环种类数,即为 b1×b2lcm(b1,b2),也就是 gcd(b1,b2)

所以这些边的贡献的等价类个数即为:

i=1kj=1i1gcd(bi,bj)

所以最终等价类的个数即为:

c(g)=i=1kbi2+i=1kj=1i1gcd(bi,bj)

答案即为:

1n!gG2c(g)

不过 Gn! 的,复杂度肯定不行,尝试优化;

考虑发现很多置换会被重复计算,如:

(123231)+(4554)+(66)(11)+(2332)+(456645)

这样的 bk 都是 1,2,3,那么 c(g) 的值是相同的。

也就是说对于相同的 n 的拆分贡献是相同的,所以可以考虑枚举 n 的拆分。

考虑对于一种拆分,计算有多少种对应的置换,首先总共有 n!,但是对于每一段长度为 b 的循环,都需要使其从普通排列转换为圆排列,也就是先去掉重复的 b!,然后再乘上圆排列的 (b1)!,并且对于相同长度的多个循环,假设有 c 个,那么这里又会重复 c! 个,那么在去重后应该为:

n!bi(ci!)

然后这里再枚举每个 n 的拆分,答案即为:

1n!n!bi(ci!)2c(g)

然后化简一下即为:

2c(g)bi(ci!)

c(g)=i=1kbi2+i=1kj=1i1gcd(bi,bj)

这样便可以通过了。然后尝试分析一下复杂度:

枚举 n 的拆分是 A000041,然后对于拆分长度为 k 的求解为 O(k2) 的,最终应为 A296010,然后这个东西完全不知道怎么分析,总之最后的复杂度大约在 O(10n),虽然我也不知道是怎么分析出来的。。。

Code

#define _USE_MATH_DEFINES
#include <bits/stdc++.h>

#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW(arr) void* Edge::operator new(size_t){static Edge* P = arr; return P++;}

using namespace std;

mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}

typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;

#define MOD (997)

template < typename T = int >
inline T read(void);

int N;
ll fact[110], inv[110], inv_d[110];
int cnt[110];
ll ans(0);
basic_string < int > cur;
unordered_set < int > exist;

ll qpow(ll a, ll b){
    ll ret(1), mul(a);
    while(b){
        if(b & 1)ret = ret * mul % MOD;
        b >>= 1;
        mul = mul * mul % MOD;
    }return ret;
}

void Init(void){
    for(int i = 1; i <= 100; ++i)inv_d[i] = qpow(i, MOD - 2);
    fact[0] = 1;
    for(int i = 1; i <= 100; ++i)fact[i] = fact[i - 1] * i % MOD;
    inv[100] = qpow(fact[100], MOD - 2);
    for(int i = 99; i >= 0; --i)inv[i] = inv[i + 1] * (i + 1) % MOD;
}

void dfs(int lft = N){
    if(!lft){
        ll C(0);
        for(auto i : cur)C += i >> 1;
        for(int i = 1; i <= (int)cur.size(); ++i)
            for(int j = 1; j <= i - 1; ++j)
                C += __gcd(cur.at(i - 1), cur.at(j - 1));
        ll ret = qpow(2, C);
        for(auto i : cur)(ret *= inv_d[i]) %= MOD;
        for(auto i : exist)(ret *= inv[cnt[i]]) %= MOD;
        (ans += ret) %= MOD;
        return;
    }
    for(int i = cur.empty() ? 1 : cur.back(); i <= lft; ++i){
        cur += i, ++cnt[i], exist.insert(i);
        dfs(lft - i);
        cur.pop_back();
        if(!--cnt[i])exist.erase(i);
    }
}

int main(){
    Init();
    N = read();
    dfs();
    printf("%lld\n", ans);
    fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
    return 0;
}

template < typename T >
inline T read(void){
    T ret(0);
    int flag(1);
    char c = getchar();
    while(c != '-' && !isdigit(c))c = getchar();
    if(c == '-')flag = -1, c = getchar();
    while(isdigit(c)){
        ret *= 10;
        ret += int(c - '0');
        c = getchar();
    }
    ret *= flag;
    return ret;
}

例题 #3 LG-P4128 [SHOI2006] 有色图

双倍经验,将 2c(g) 改为 mc(g) 即可。

例题 #4 UVA10601 Cubes

题面

给定 12 根木棍的颜色,颜色最多 6 种,求其能构成的本质不同正方体的数量。

Solution

依然尝试考虑 Burnside定理,即:

|X/G|=1|G|gGXg

集合 X 即为 12 根木棒构成的正方体,然后置换群 G 则要复杂一些,其中元素即为对于正方体的各种旋转等操作,同时不难发现,对于任意一种旋转,会存在多个循环,或者说多个环,而在同一个环中所有边的颜色应该是相同的,否则其必定不可能为不动点。考虑分类讨论:

结论:对于正方体的旋转共有 24 种。

对于恒等变换,共 1 种,环长为 1

对于沿对面中心定向旋转 90270,共 3×2=6 种,环长为 4

对于沿对面中心旋转 180,共 3 种,环长为 2

对于沿对棱中点旋转 180,有 6 对对棱,共 6 种,环分为两种,存在 2 个环长为 1 的环和 5 个环长为 2 的环。

对于沿相对顶点定向旋转 120240,相对顶点共有 4 对,则共 4×2=8 种,环长为 3

故置换的个数共 1+6+3+6+8=24 种。

如果直接枚举所有排列然后计算不动点再去重似乎有点卡,感觉复杂度似乎是 O(24×12!),好像过不了。

考虑将构建长方体转换为进行圆排列,不难发现大多数的置换的环长都是相同的,令环长为 x,则可以将问题转换为有 6 种颜色,每种颜色有 ci 个,然后 ci=12,用其进行圆排列。然后考虑一下环长再次转换,即颜色有 cix 个,共有 12x 个环,对环进行染色,这样的话就又变成普通排列了,于是问题转化为多重集排列,从所有排列中去重即可,最终答案:

12x!i=16cix!

对于不满足 xci 的直接忽略即可。

然后对于两种环的部分,枚举两个环长为 1 的环是什么颜色即可,然后去掉这两个后对剩下的按照相同方式计算即可。

最后复杂度总之是 O(能过)

Code

#define _USE_MATH_DEFINES
#include <bits/stdc++.h>

#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW(arr) void* Edge::operator new(size_t){static Edge* P = arr; return P++;}

using namespace std;

mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}

typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;

template < typename T = int >
inline T read(void);


int cnt[7];
ll fact[13];

void Init(void){
    fact[0] = 1;
    for(int i = 1; i <= 12; ++i)fact[i] = fact[i - 1] * i;
}
ll Cal(int len, int siz = 12){
    if(siz % len)return 0;
    ll ret = fact[siz / len];
    for(int i = 1; i <= 6; ++i){
        if(cnt[i] % len)return 0;
        else ret /= fact[cnt[i] / len];
    }return ret;
}

int main(){
    Init();
    int T = read();
    while(T--){
        memset(cnt, 0, sizeof cnt);
        for(int i = 1; i <= 12; ++i)cnt[read()]++;
        ll ans(0);
        ans += Cal(1) + 6 * Cal(4) + 3 * Cal(2) + 8 * Cal(3);
        for(int i = 1; i <= 6; ++i){
            if(!cnt[i])continue;
            --cnt[i];
            for(int j = 1; j <= 6; ++j){
                if(!cnt[j])continue;
                --cnt[j];
                ans += Cal(2, 10) * 6;
                ++cnt[j];
            }++cnt[i];
        }printf("%lld\n", ans / 24);
    }

    fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
    return 0;
}

template < typename T >
inline T read(void){
    T ret(0);
    int flag(1);
    char c = getchar();
    while(c != '-' && !isdigit(c))c = getchar();
    if(c == '-')flag = -1, c = getchar();
    while(isdigit(c)){
        ret *= 10;
        ret += int(c - '0');
        c = getchar();
    }
    ret *= flag;
    return ret;
}

例题 #5 LG-P2561 [AHOI2002]黑白瓷砖

题面

有点繁琐,挂两个图片吧。

Solution

显然置换群为:

{旋转120,旋转240,翻转(共三种),静止}

分别考虑,对于旋转,考虑 Polya定理,每个环长均为 3,且可能存在环长为 1 的一个中心点,所以贡献为:

2n(n+1)6

对于翻转,要求对称,环长为 12,不难发现贡献为:

2i=1ni2

对于静止,贡献为:

2n(n+1)2

套用一下 Polya定理 即可。

注意需要高精度。

Code

#define _USE_MATH_DEFINES
#include <bits/stdc++.h>

#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
#define ROPNEW_NODE void* Node::operator new(size_t){static Node* P = nd; return P++;}

using namespace std;

mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}

typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;

template < typename T = int >
inline T read(void);

int N;

class Bignum{
private:
public:
    basic_string < int > nums;
    friend Bignum operator + (Bignum a, Bignum b){
        reverse(a.nums.begin(), a.nums.end());
        reverse(b.nums.begin(), b.nums.end());
        while(a.nums.size() < b.nums.size())a.nums += 0;
        while(b.nums.size() < a.nums.size())b.nums += 0;
        Bignum ret; bool plus(false);
        for(int i = 0; i < (int)a.nums.size(); ++i){
            a.nums.at(i) += b.nums.at(i) + plus;
            plus = false;
            if(a.nums.at(i) >= 10)
                plus = true, a.nums.at(i) %= 10;
        }
        if(plus)a.nums += 1;
        reverse(a.nums.begin(), a.nums.end());
        return a;
    }
    friend Bignum operator * (Bignum a, Bignum b){
        reverse(a.nums.begin(), a.nums.end());
        reverse(b.nums.begin(), b.nums.end());
        Bignum ret;
        for(int i = 1; i <= (int)(a.nums.size() + b.nums.size()); ++i)ret.nums += 0;
        for(auto i = 0; i < (int)a.nums.size(); ++i)
            for(int j = 0; j < (int)b.nums.size(); ++j)
                ret.nums.at(i + j) += a.nums.at(i) * b.nums.at(j);
        for(int i = 0; i < (int)ret.nums.size() - 1; ++i)
            ret.nums.at(i + 1) += ret.nums.at(i) / 10, ret.nums.at(i) %= 10;
        if(ret.nums.back() >= 10)ret.nums += ret.nums.back() / 10, *prev(ret.nums.end(), 2) %= 10;
        while(ret.nums.size() > 1 && ret.nums.back() == 0)ret.nums.pop_back();
        reverse(ret.nums.begin(), ret.nums.end());
        return ret;
    }
    friend Bignum operator / (Bignum a, ll div){
        Bignum ret;
        ll cur(0); bool flag(false);
        for(auto i : a.nums){
            cur *= 10, cur += i;
            if(cur < div && !flag)continue;
            flag = true, ret.nums += cur / div, cur %= div;
        }return ret;
    }
    void Print(void){
        for(auto v : nums)printf("%d", v);
        printf("\n");
    }
};

Bignum qpow(Bignum a, ll b){
    Bignum ret, mul(a);
    ret.nums += 1;
    while(b){
        if(b & 1)ret = ret * mul;
        b >>= 1;
        mul = mul * mul;
    }return ret;
}

int main(){
    N = read();
    Bignum ans; ans.nums += 0;
    Bignum base; base.nums += 2;
    ans = ans + (qpow(base, (ll)ceil((ld)N * (N + 1) / 6.0)) * base);
    ll num(0);
    for(int i = 1; i <= N; ++i)num += (i + 1) >> 1;
    Bignum mul; mul.nums += 3;
    ans = ans + (qpow(base, num) * mul);
    ans = ans + qpow(base, (N * (N + 1)) >> 1);
    ans = ans / 6ll;
    ans.Print();
    fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
    return 0;
}

template < typename T >
inline T read(void){
    T ret(0);
    int flag(1);
    char c = getchar();
    while(c != '-' && !isdigit(c))c = getchar();
    if(c == '-')flag = -1, c = getchar();
    while(isdigit(c)){
        ret *= 10;
        ret += int(c - '0');
        c = getchar();
    }
    ret *= flag;
    return ret;
}

例题 #6 SP419 TRANSP - Transposing is Fun

题面

给定 a,b,存在 2a×2b 的任意矩阵,现在求其转置矩阵,但仅能用交换两个元素的操作,求最少的操作次数能够求得任意该大小矩阵的转置矩阵。特别地,我们将矩阵抽象成序列,具体地,按照第一行的元素、第二行的元素、第三行的元素……存储。

Solution

一道很好的群论题,尤其是其中转化的思想还是很高妙的。

不难发现,我们考虑按序为矩阵标号,从 0 开始,则问题转化为我们要进行一次类似于置换的操作,如对于 a=1,b=2

(0123456704152637)

这个东西朴素的交换次数是 2a+b 的,但是不难发现,对于一个循环的置换,我们是可以优化掉一次交换操作的,如 (1,2,4)(4,1,2) 就可以用 31=2 次解决,于是如果我们找出整体有多少个循环,令其为 x,则答案为 2a+bx,考虑计算 x

由题面中 2a 的信息我们或许可能想到将其从二进制上考虑,发现变化即为将其二进制循环向右位移 a 位,或向左平移 b 位,证明不难,考虑转置变化为:i×2b+jj×2b+i,意义上就是将二进制位分割后交换,与上述操作等效。

于是我们想到,若将二进制数抽象为长度为 a+b 的环,对其进行 0/1 染色,置换为静止和旋转 a,对于旋转 a,显然环数为 gcd(a,a+b)=gcd(a,b),看起来很正确,但是仔细想一下就会发现这个置换群不存在逆元,所以不是群,无法计算。

这时就存在一个很人类智慧的转化,我们可以容易想到每 a+bgcd(a,b) 个点在一个环内,或者说每 gcd(a,b) 个数之后就会遇到应与其在同一环内的点,那么我们就可以将这 gcd(a,b) 个点缩为一个大点这样它们之间的任意染色都不会使得数同构,那么问题转化为我们用 2gcd(a,b) 个颜色去染 a+bgcd(a,b) 个点,令 n=a+bgcd(a,b) 置换为旋转 0,1,2,,n1。现在其满足性质,直接套用 Polya定理,考虑经典套路,得到:

1ni=1n(2gcd(a,b))gcd(n,i)

用经典的莫反思想推导:

1ni=1n2gcd(a,b)gcd(n,i)=1ndni=1n2gcd(a,b)d[gcd(n,i)=d]=1ndni=1nd2gcd(a,b)d[gcd(nd,i)=1]=1ndn2gcd(a,b)dφ(nd)

可以通过精细实现达到 O(T(n+logn)) 的复杂度,但是意义不大,本题不卡,直接 O(T(n34+nlogn)) 强行处理即可。

Code

#define _USE_MATH_DEFINES
#include <bits/stdc++.h>

#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
#define ROPNEW_NODE void* Node::operator new(size_t){static Node* P = nd; return P++;}

using namespace std;

mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}

typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;

#define MOD (1000003ll)
#define LIMIT (510000)

template < typename T = int >
inline T read(void);

int A, B;
int N;
basic_string < int > Prime;
bitset < LIMIT + 100 > notPrime;

ll qpow(ll a, ll b){
    ll ret(1), mul(a);
    while(b){
        if(b & 1)ret = ret * mul % MOD;
        b >>= 1;
        mul = mul * mul % MOD;
    }return ret;
}
int Phi(int x){
    int ret(x);
    basic_string < int > fact;
    for(auto p : Prime){
        if(p * p > x)break;
        if(x % p == 0)fact += p;
        while(x % p == 0)x /= p;
    }if(x != 1)fact += x;
    for(auto f : fact)ret /= f, ret *= (f - 1);
    return ret;
}

int main(){
    for(int i = 2; i <= LIMIT; ++i){
        if(!notPrime[i])Prime += i;
        for(auto p : Prime){
            if(p * i > LIMIT)break;
            notPrime[p * i] = true;
            if(i % p == 0)break;
        }
    }
    int T = read();
    while(T--){
        A = read(), B = read();
        N = (A + B) / __gcd(A, B);
        ll ans(0);
        for(int i = 1; i * i <= N; ++i){
            if(N % i)continue;
            if(i * i != N)
                (ans += qpow(2, (ll)__gcd(A, B) * i) * Phi(N / i) % MOD) %= MOD,
                (ans += qpow(2, (ll)__gcd(A, B) * (N / i)) * Phi(i) % MOD);
            else (ans += qpow(2, (ll)__gcd(A, B) * i) * Phi(N / i) % MOD) %= MOD;
        }(ans *= qpow(N, MOD - 2)) %= MOD;
        printf("%lld\n", (qpow(2, A + B) - ans + MOD) % MOD);
    }
    fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
    return 0;
}

template < typename T >
inline T read(void){
    T ret(0);
    int flag(1);
    char c = getchar();
    while(c != '-' && !isdigit(c))c = getchar();
    if(c == '-')flag = -1, c = getchar();
    while(isdigit(c)){
        ret *= 10;
        ret += int(c - '0');
        c = getchar();
    }
    ret *= flag;
    return ret;
}

例题 #7 SP422 TRANSP2 - Transposing is Even More Fun

题面

SP419 TRANSP - Transposing is Fun 相同,仅略增大数据范围。

Solution

有点不太理解这道题的意义,完全就是更卡常一点,需要更加精细实现,本质没有任何变化。。

推导部分完全相同,最终式子:

1ndn2gcd(a,b)dφ(nd)

发现对于 2k 最多 a+bgcd(a,b)×gcd(a,b)=a+b,于是可以预处理 2k,然后我们可以通过分解质因数后 dfs 跑一下,同时处理 φ 即可每次 O(n) 处理该式,故最终复杂度 O((a+b)+T(n+logn)),可以通过。

Code

#define _USE_MATH_DEFINES
#include <bits/stdc++.h>

#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
#define ROPNEW_NODE void* Node::operator new(size_t){static Node* P = nd; return P++;}

using namespace std;

mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}

typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;

#define MOD (1000003ll)
#define LIMIT (1100000)

template < typename T = int >
inline T read(void);

int A, B;
int N, gcdv;
ll ans(0);
ll pow2[LIMIT + 100];
basic_string < int > Prime;
bitset < LIMIT + 100 > notPrime;
struct Num{int v; int cnt;};
basic_string < Num > fact;

ll qpow(ll a, ll b){
    ll ret(1), mul(a);
    while(b){
        if(b & 1)ret = ret * mul % MOD;
        b >>= 1;
        mul = mul * mul % MOD;
    }return ret;
}
void Init(int x){
    fact.clear();
    for(auto p : Prime){
        if(p * p > x)break;
        if(x % p == 0)fact += {p, 0};
        while(x % p == 0)fact.back().cnt++, x /= p;
    }if(x != 1)fact += {x, 1};
}
void dfs(int dep = 0, ll d = 1, ll base = 1, ll div = 1){
    if(dep == (int)fact.size())
        return (ans += pow2[gcdv * (N / d)] * (d / div * base) % MOD) %= MOD, void();
    dfs(dep + 1, d, base, div);
    base *= (fact.at(dep).v - 1), div *= fact.at(dep).v;
    for(int i = 1; i <= fact.at(dep).cnt; ++i)
        d *= fact.at(dep).v, dfs(dep + 1, d, base, div);
}

int main(){
    for(int i = 2; i <= LIMIT; ++i){
        if(!notPrime[i])Prime += i;
        for(auto p : Prime){
            if(p * i > LIMIT)break;
            notPrime[p * i] = true;
            if(i % p == 0)break;
        }
    }
    pow2[0] = 1;
    for(int i = 1; i <= LIMIT; ++i)pow2[i] = pow2[i - 1] * 2 % MOD;
    int T = read();
    while(T--){
        A = read(), B = read();
        N = (A + B) / __gcd(A, B), gcdv = __gcd(A, B);
        Init(N);
        ans = 0;
        dfs();
        (ans *= qpow(N, MOD - 2)) %= MOD;
        printf("%lld\n", (pow2[A + B] - ans + MOD) % MOD);
    }
    fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
    return 0;
}

template < typename T >
inline T read(void){
    T ret(0);
    int flag(1);
    char c = getchar();
    while(c != '-' && !isdigit(c))c = getchar();
    if(c == '-')flag = -1, c = getchar();
    while(isdigit(c)){
        ret *= 10;
        ret += int(c - '0');
        c = getchar();
    }
    ret *= flag;
    return ret;
}

例题 #8 LG-P4916 [MtOI2018]魔力环

题面

你需要对长度为 n 的环黑白染色,要求染 m 个黑色与 nm 个白色,并要求不能有超过 k 个黑色相邻,求本质不同方案数。

Solution

纯组合意义是不可能的,这辈子都不可能的

发现本质不同关键字,不难想到群论,对于本题可以使用 Burnside,考虑令 g(i) 表示置换为旋转 i 的不动点的数量,令 f(i) 表示存在 i 个环的方案数,这样答案不难想到即为:

1ni=1ng(i)

考虑经典转化,即 f(i)=g(gcd(n,i))

转化为:

1ni=1nf(gcd(n,i))=1ndnf(d)φ(nd)

考虑处理 f(d),令 ξ(n,m,k) 表示 n+m 个球染 m 个黑色限制为 k 的方案数,由将 m 个球可空地放入 n 个盒每个盒不能超过 k 的方案数得到,不难想到一个类似二项式反演的容斥:

ξ(n,m,k)=n+mni=0k(1)i(ni)(mi×(k+1)+n1n1)

即经典插板法。

则对于 f(d),应有:

f(nd)=ξ(nmd,md,k)

同时注意对于 dm,显然 f(d)=0

Code

#define _USE_MATH_DEFINES
#include <bits/stdc++.h>

#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
#define ROPNEW_NODE void* Node::operator new(size_t){static Node* P = nd; return P++;}

using namespace std;

mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}

typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;

#define MOD (998244353ll)
#define LIM (210000)

template < typename T = int >
inline T read(void);

int N, M, K;
ll phi[LIM + 100];
bitset < LIM + 100 > notPrime;
basic_string < int > Prime;
ll fact[LIM + 100], inv[LIM + 100];

int main(){
    auto qpow = [](ll a, ll b)->ll{
        ll ret(1), mul(a);
        while(b){
            if(b & 1)ret = ret * mul % MOD;
            b >>= 1;
            mul = mul * mul % MOD;
        }return ret;
    };
    auto Init = [qpow](void)->void{
        phi[1] = fact[0] = 1;
        for(int i = 2; i <= LIM; ++i){
            if(!notPrime[i])phi[i] = i - 1, Prime += i;
            for(auto p : Prime){
                if((ll)i * p > LIM)break;
                notPrime[i * p] = true;
                if(i % p == 0)phi[i * p] = p * phi[i] % MOD;
                else phi[i * p] = phi[p] * phi[i] % MOD;
                if(i % p == 0)break;
            }
        }
        for(int i = 1; i <= LIM; ++i)fact[i] = fact[i - 1] * i % MOD;
        inv[LIM] = qpow(fact[LIM], MOD - 2);
        for(int i = LIM - 1; i >= 0; --i)inv[i] = inv[i + 1] * (i + 1) % MOD;
    };Init();
    auto GetC = [](ll n, ll m)->ll{
        if(n < m || n < 0 || m < 0)return 0;
        return fact[n] * inv[m] % MOD * inv[n - m] % MOD;
    };
    auto Cal = [qpow, GetC](ll N, ll M, ll K)->ll{
        ll ret(0);
        for(int i = 0, flag = 1; i * (K + 1) <= M; ++i, flag *= -1)
            (ret += ((flag * GetC(N, i) * GetC(M - i * (K + 1) + N - 1, N - 1) % MOD) + MOD) % MOD) %= MOD;
        (ret *= (N + M) * qpow(N, MOD - 2) % MOD) %= MOD;
        return ret;
    };
    ll ans(0);
    N = read(), M = read(), K = read();
    if(N == M)printf("%d\n", K >= N ? 1 : 0), exit(0);
    for(int i = 1; i <= N; ++i)
        if(N % i == 0 && M % i == 0)
            (ans += Cal((N - M) / i, M / i, K) * phi[i] % MOD) %= MOD;
    (ans *= qpow(N, MOD - 2)) %= MOD;
    printf("%lld\n", ans);
    fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
    return 0;
}

template < typename T >
inline T read(void){
    T ret(0);
    int flag(1);
    char c = getchar();
    while(c != '-' && !isdigit(c))c = getchar();
    if(c == '-')flag = -1, c = getchar();
    while(isdigit(c)){
        ret *= 10;
        ret += int(c - '0');
        c = getchar();
    }
    ret *= flag;
    return ret;
}

后面就是烷烃计数烯烃计数等一大堆阴间东西,省选之后再来写~

UPD

update-2023_01_06 初稿

update-2023_01_09 fix大量笔误及不严谨的地方 补充了部分证明

update-2023_01_10 重写轨道-稳定子定理的证明

update-2023_02_04 调整部分排版

update-2023_02_06 添加例题 #5 #6 #7

update-2023_02_16 添加半群等内容 添加双半群模型

update-2023_03_06 添加例题 #8

本文作者:Tsawke

本文链接:https://www.cnblogs.com/tsawke/p/17032879.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Tsawke  阅读(227)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起