15333 字
77 分钟

信竞初等数论导论

2023-11-26
2024-02-18
浏览量 加载中...

引入#

如果说数论是数学体系中专门用来研究数字性质的一个分支,那么初等数论则是对整数的性质进行系统性的探讨与研究。千万不要因为其中的“初等”二字小瞧这初等数论尽管名称和学习难度上都没有高等数论那么有逼格,就像初等数学之于高数,数论的所有内容均筑基于此。其中欧几里得证明的算数基本定理(一切合数都可被分解为有限个质数的乘积)在质数筛、GCD(以及LCA)计算、无理数证明等问题上均有用武之地。可以说高等数论奠基于初等数论。它同时也是初学者接触数论的必经之路。

Part1.  前置知识#

Div1. 数论有关定理#

算术基本定理:每一个合数都可以被分解为有限个质数的乘积。即对于任意合数nn,都存在:

n=p1c1p2c2p3c3...pkckn=p_{1}^{c_1}p_{2}^{c_2}p_{3}^{c_3}...p_{k}^{c_k},其中pp为质数。

推论一:正整数nn的正因数集合为:

{n=p1b1p2b2p3b3...pibi1bici,1ik}\{n=p_{1}^{b_1}p_{2}^{b_2}p_{3}^{b_3}...p_{i}^{b_i} \mid 1 \leq b_i \leq c_i, 1 \leq i \leq k\}

推论二:正整数nn的正因数个数为:τ(n)=(c1+1)(c2+1)(c3+1)...(ck+1)=i=1k(ci+1)\tau (n)= (c_1+1) \cdot(c_2+1) \cdot (c_3+1) ... (c_k+1) =\prod_{i=1}^{k}\left( c_i+1\right) \\

推论三:正整数nn的所有正因数之和为

σ(n)=(p1+p12+...+p1c1+1)×(p2+p22+...+p2c2+1)×...×(pk+pk2+...+pkck+1)=i=1kpkck+1pk1\sigma(n)=(p_1+p_1^2+...+p_1^{c_1}+1)\times (p_2+p_2^2+...+p_2^{c_2}+1)\times...\times (p_k+p_k^2+...+p_k^{c_k}+1)=\prod_{i=1}^{k}\frac{p_k^{c_k+1}}{p_k-1}

质数分布定理:区间[1,N][1,N]中,当NN\to \infty时,质数个数π(x)nlnn\pi (x)\approx \frac{n}{\ln n}

费马小定理:pp是一个质数,则apamodpa^p\equiv a\bmod p\equiv为同余符号)。

欧拉定理(费马小定理扩展):ana\perp naann互质),则有aφ(n)1modna^{\varphi{(n)}} \equiv 1 \bmod n,其中φ(n)\varphi(n)为欧拉函数。

Div2. 同余#

同余,顾名思义,两个数分别除以一个正整数mm后得到相同的余数。即amodm=bmodma\bmod m=b\bmod m。但是它的定义给出了这样一句话:“对于正整数aabb,若(ab)m(a-b)\mid maba-b能被mm整除),则称aabb对模mm同余,记作ab(modm)a\equiv b\pmod m。当然以上两种说法是等价的。

同余具有以下三种基本性质:

  1. 反身性:对于任何正整数aaaa(modm)a\equiv a\pmod m
  2. 对称性:即对于ab  (modm)a\equiv b\;(\bmod m),有ba(modm)b\equiv a\pmod m
  3. 传递性:若ab(modm)a\equiv b\pmod m,且bc(modm)b\equiv c\pmod m,有ac(modm)a\equiv c\pmod m

当然,它可以延申到计算机的模运算(毕竟出现了mod\bmod)。模运算有三种基本运算:

  1. 加运算:(a+b)%m=(a%m+b%m)%m(a+b)\% m=(a\% m+b\% m)\% m
  2. 减运算:(ab)%m=(a%mb%m)%m(a-b)\%m=(a\%m-b\%m)\%m
  3. 乘运算:(ab)%m=((a%m)(b%m))%m(a\cdot b)\%m=((a\%m)*(b\%m))\%m

还有两个推论:

  1. 幂运算:ab%p=((a%p)b)%pa^b\%p=((a\%p)^b)\%p
  2. 求和运算:(x=1nx)%p=(x=1nx%p)%p(\sum\limits_{x=1}^{n}x)\%p=(\sum\limits_{x=1}^{n}x\%p)\%p

同余消去原则:

若同余号两端的项相等,且都与模nn互质,则可以同时消去

举例:acbcmodna\cdot c\equiv b\cdot c \bmod n,如果gcd(c,n)=1gcd(c,n)=1,则acbcabmodna\cdot c\equiv b\cdot c\Rightarrow a\equiv b \bmod n

Part2.  质数#

质数的判断:除了它自身PP以及11以外,不存在其他正整数NN使得NPN\mid P

Div1.  质数判断#

试除法: 这是三种方法中,唯一一种能做到100%正确的质数判断方法。对于给定数nn,遍历所有[2,n][2,\sqrt n]间的正整数mm,若出现mnm\mid n则证明它不是质数,因为质数只能被1以及它本身整除

bool isPrime(int n) {
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return false;
}
return true;
}

复杂度:O(n)\mathcal O(\sqrt n)

适用范围:普及、提高

试除法の大胜利!

费马素性检验: 是上述费马小定理的实际运用,它与常规算法思想有所不同:它主张在[2,n1][2,n-1]中随机选取一个数aa。若出现与费马小定理不符的情况,那么nn一定为合数;若每次均符合定理,称为 费马伪素数 ,因为它很大概率是一个质数。

bool isPrime(int n) {
if (n <= 2) return false;
int k = 10;
while (k--) {
srand(time(0));
int a = rand() % (n - 2) + 2;
if (__gcd(a, n) != 1) return false;
if (qpow(a, n - 1, n) != 1) return false;
}
return true;
}

复杂度:O(klogn)\mathcal O(k\log n),其中kk为随机数检验次数,logn\log n是因为使用了快速幂算法。

适用范围:提高T2及以下(慎用)

为什么用该方法判断的质数 大概率 是个质数呢?不妨测试一下561(3)、1105(5)、1729(7)(括号内为它的最小因子),你会发现函数的返回值均为true,即都为质数。可见这个算法不是100%正确的,这些“漏网之鱼”被称为“Carmichael数”。它们极其罕见,一亿范围内仅255个。也因如此,你可以通过打表特判的方式抠掉这些特例(你保证记得住就行)。2016年中国物流工人余建春给出了一个Carmichael数的判断准则,这个标准目前在国际上得到了广泛认同。

对于优化,你可以在函数起始点加入类似于if (n % 2 == 0 || n % 3 == 0) return false;的特判,进一步降低复杂度。

Miller-Rabin算法: 该算法同样无法保证结果100%准确,慎用!

MB算法实质上是对费马素性检验算法的效率和准确度优化。算法流程如下:

  1. n1n-1分解为2s+d2^s+d的形式,其中dd为奇数
  2. [2,n2][2,n-2]中选取整数aa,称为“基数”
  3. 计算admodna^d \bmod n的值,若结果为11n1n-1,则可能为质数,继续检验
  4. 若结果不等于11n1n-1,计算a2dmodna^{2d} \bmod na4dmodna^{4d} \bmod na6dmodna^{6d} \bmod n……a2s1dmodna^{2^{s-1}d} \bmod n的值,若结果等于n1n-1,则可能为质数,继续检验
  5. 若都不等于11,则nn一定是合数。称为强费马证据

当然,它同样有特例,称为强伪质数,如2047(23)、3277(29)、4033(39)等(括号内为它的最小因子)。

Div2.  质数筛#

常见的质数筛法有:试除法、埃氏筛、线性筛。

试除法:从质数定义出发,即存在一个正整数NN,对于任意[2,N][2,\sqrt N]间的正整数MM,总有NmodM0N\bmod M \neq 0成立。代码实现只需枚举[2,N][2, \sqrt N]间所有正整数,并让NN对其取余。若取模运算出现00则代表它不为质数,没有出现00则为质数。

bool isPrime(int n) {
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return false;
}
return true;
}

复杂度:O(N)\mathcal O( \sqrt N )

适用范围:普及T2及以下

这里所展示的试除法代码实际上经过一轮优化。若严格根据质数定义,第二行的循环上限应为n1n-1。考虑到如下性质:m[2,N]\forall m\in [2,\sqrt N],若mNm\mid N,则一定有NmN\frac{N}{m}\mid N。因此可以将循环上限压缩至N\sqrt N

埃氏筛:全称叫埃拉托斯特尼筛法,老哥生活在2200年前的古希腊,不借助望远镜就计算出了地球的周长(与真实值偏差仅0.96%)、同时他也是第一位根据经纬线绘制出世界地图的人、也是最先提出将地球根据南北回归线分为“五带”的大人物。他提出的筛法核心思想如下:

第一步:列出从2开始的一列连续数字;第二步:选出第一个质数(本例中为2),将该质数标记,将数列中它的的所有倍数划去;第三步:若数列中的末项小于它前一项的平方,则质数已全部筛出;否则返回第二步

void get(int n) {
for (int i = 2; i <= n; i++) {
if (!vis[i]) {
prime[cnt++] = i;
}
for (int j = 2; i * j <= n; j ++) vis[i * j] = true;
}
}

其中,prime数组存储质数,vis数组用于标记(即上文中“划去数字”),变量cnt则存储[2,N][2,N]中质数的个数。

复杂度:O(nlnn)\mathcal O(n\ln{n})

适用范围:普及T2及以下

但是继续观察算法发现:我们其实无需将所有ii的倍数删去,只需删去前一步得出的质数的所有倍数即可。 这与前文介绍的埃氏法核心相符。因此将jj循环迁移至条件判断中即可:

void get(int n) {
for (int i = 2; i <= n; i++) {
if (!vis[i]) {
prime[cnt++] = i;
for (int j = i + i; j <= n; j += i) vis[j] = true;
}
}
}

优化复杂度:O(nloglogn)\mathcal O(n\log{\log n})

适用范围:普及T3及以下

线性筛/欧拉筛:实质是埃氏筛的线性优化。因为在埃氏筛中,有些数字被重复筛了多次(例如30会被2、3、5筛到)。本着线性优化的原则,我们需要找到一个方法,使得每个合数仅被筛选一次。主要思想如下:

我们发现,线性筛和埃氏筛均使用了质数的nn倍为合数的结论。我们只需要保证每一个数仅被它自身的最小质因数筛出即可。即对于数字mmmpim\cdot p_i是一个合数,且mpim\cdot p_i只会被pip_i筛出。

void get(int n) {
for (int i = 2; i <= n; i++) {
if (!vis[i]) prime[cnt++] = j;
for (int j = 1; prime[j] <= n / i; j++) {
vis[prime[j] * i] = true;
if (i % prime[j] == 0) break;
}
}
}

复杂度:O(n)\mathcal O(n)

适用范围:普及、提高


例题:

  1. P5736 【深基7.例2】质数筛
  2. P5723 【深基4.例13】质数口袋

Part3.  因数#

因数定义: 对于一个数nn,若存在一个正整数mm使得mnm\mid n,则称mmnn的因数。

Div1.  因数分解法#

试除法:万能暴力解法。即遍历[2,n][2,\sqrt n]间的所有数mm,若可以整除nn,则mmnm\frac{n}{m}均为nn的因数。特殊情况:n\sqrt n为整数时,因数仅有n\sqrt n本身,因此需特判。

vector<int> get(int n) {
vector<int> ret;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
ret.push_back(i);
ret.push_back(n / i);
}
if (n % (i * i) == 0) ret.pop_back();
}
sort(ret.begin(), ret.end());
return ret;
}

复杂度:O(n)\mathcal O(\sqrt n)

适用范围:普及T1

Div2.最大公约数#

辗转相除法: 又是我们大名鼎鼎的欧几里得老先生提出的一套公约数算法,整个算极其简洁:核心只有一行,即:

两个数的最大公约数等于其中较小的数字和二者之间余数的最大公约数

可以写出:

int gcd(int a, int b) {
return b ? gcd(a, a % b) : a;
}

但是为什么gcd(a,b)=gcd(a,amodb)gcd(a,b)=gcd(a,a\bmod b)呢?我们可以通过以下方法证明:

假设如下关系:A=BC+DA=B\cdot C + D。其中被除数AA,除数BB,商CC,余数DD。则AmodB=AmodC=DA\bmod B=A\bmod C=D

首先证明充分性:令A=akA=a\cdot kB=bkB=b\cdot k,即二者有相同因子kk

代入初始除法算式得:D=ABCakbkCk(abC)D=A-B\cdot C\rightarrow a\cdot k-b\cdot k\cdot C\rightarrow k(a-b\cdot C)

接着由于加减乘法的封闭性,即一个整数进行加减乘运算得到的结果同样是一个整数。可以得出:D=kNNN+D=k\cdot N\mid N\in \mathbb{N_+}。即DDAmodBA\bmod B)与AA有共同因子。

接下来证必要性。令B=bqB=b\cdot qD=dqD=d\cdot q

Stein算法: 上一个方法的明显缺点在于,它处理大质数的效率并不好(但总体来说是很好的),因为它使用了取余运算,这会减慢一些速度。可以理解,生在2000多年前——一个没有电脑和OI的古希腊社会,这个算法已经足够兼顾常规效率和手推难度了。但是步入21世纪,加快的生活节奏毒瘤数据使得人们对更快算法的需求空前高涨。Stein算法便应运而生。

算法流程如下:

  1. 任意给定两个正整数,先判断它们是否都是偶数,若是,则用2约简,若不是,则执行第二步。
  2. 若两数是一奇一偶,则偶数除以2,直至两数都成为奇数。再以较大的数减较小的数,接着取所得的差与较小的数,若两数一奇一偶,仍然偶数除以2,直至两数都成为奇数。再次以大数减小数。不断重复这个操作,直到所得的减数和差相等为止。
  3. 两数相等时,第一步中约掉的若干个2与第二步中最终的等数的乘积就是所求的最大公约数。
int gcd(int a, int b) {
int p = 0, t;
if (!(1 & a) && !(1 & b)) {
a >>= 1;
b >>= 1;
p++;
}
while (!(1 & a)) a >>= 1;
while (!(1 & b)) b >>= 1;
if (a < b) {
t = a;
a = b;
b = t;
}
while (a = ((a - b) >> 1)) {
while (!(1 & a)) a >>= 1;
if (a < b) {
t = a;
a = b;
b = t;
}
}
return b << p;
}

这个算法的优点在于:它大大优化了大质数的运算。但可惜的是,它的代码量膨胀了8倍,因此不太建议赛时使用。毕竟C++都给你内置了__gcd()函数嘛,干嘛不偷个懒?

Div3. 最小公倍数#

我们可以简单概括成一句话:

两个数的最小公倍数等于这两个数的乘积与这两个数最大公约数的商

即:lca(a,b)=abgcd(a,b)lca(a,b)=\frac{a\cdot b}{gcd(a,b)}

凭啥呀?

我们假设两个数AABB有最大公约数xx,则A=axA=a\cdot x,且B=bxB=b\cdot x。并且aabb一定互质(若不互质,AABB的最大公约数就不会是xx,而是一个比xx大的值)。

由乘法交换律,可知:AB=BAAbx=BaxA\cdot B = B\cdot A\rightarrow A\cdot b \cdot x=B\cdot a\cdot x

消去xx得:Ab=BaA\cdot b=B\cdot a。因为aabb互质,所以AbA\cdot b或者BaB\cdot a即为两个数的最小公倍数。得证。


例题:

  1. P1075 [NOIP2012 普及组] 质因数分解
  2. P2424 约数和 (需要逆向思维)

Part4. 欧拉函数相关#

Div1. 欧拉函数推导#

问:论牧师欧拉有多么的高产

答:平均每年800页数学论文你说高不高产嘛

欧拉函数,记作φ(n)\varphi(n)。表示[1,n)[1,n)中与nn互质的数的个数,即m[1,n)\forall m\in [1,n),满足gcd(n,m)=1gcd(n,m)=1mm的总个数即为φ(n)\varphi(n)的值。举个例子,φ(3)=2\varphi(3)=2,因为在[1,3)[1,3)中,1122均与33互质。特殊地,φ(1)=1\varphi(1)=1

欧拉函数有如下计算公式:若nn可被表示为n=i=1kpiαin=\prod\limits_{i=1}^{k}p_i^{\alpha_i}(算术基本定理分解式)的形式,则φ(n)=n(11p1)(11p2)(11pk)=ni=1k(11pi)\varphi(n)=n*(1-\frac{1}{p_1})(1-\frac{1}{p_2})\dots(1-\frac{1}{p_k})=n*\prod\limits_{i=1}^{k}(1-\frac{1}{p_i})

推导思想即为用nn减去所有[1,n)[1,n)中所有与nn不互质的数。在计算机上实现,首先需要分解质因数。思路如下:首先抛出第一个质因数p1p_1,那么将[1,n][1,n]中所有的p1p_1的倍数删去,因而可以保证筛出的数一定是nn的质因子,否则他们将存在最大公约数p1p_1。那么能被p1p_1整除的数的个数(也就是nn以内p1p_1的倍数个数)为[np1][\frac{n}{p_1}]——其中的中括号代表整除。

因此我们离解出欧拉函数就进了一步了,我们的过渡式子就是TS1(n)=n[np1][np2][npk1][npk]TS_1(n)=n-[\frac{n}{p_1}]-[\frac{n}{p_2}]-\dots-[\frac{n}{p_{k-1}}]-[\frac{n}{p_k}]

好耶

别急着好耶,我们可以发现一个小小的推导谬误(可能并不是很容易发现)。当我们用p2p_2去筛数时,使用的算式仍然是[np2][\frac{n}{p_2}]。对于形如p1np2mp_1^np_2^m的数,会被重复筛去多次,导致多减,最终结果会小于φ(n)\varphi(n)。有些抽象,我们来看这张图:

容斥原理

易知[1,30][1,30]中可被22整除的数字共有30÷2=1530\div 2=15个,能被55整除的数字共有30÷5=630\div 5=6个。但是如果说能被2255整除的数字共有15+6=2115+6=21个,显然不合常理,因为101020203030都既能被22整除,也能被55整除,如果不加排除,他们将会被减去2次。因此需要补偿损失,正确的计算方法是(仅计算能被2255整除的数的总个数):

  • 能被22整除的:30÷2=1530\div 2=15
  • 能被55整除的:30÷5=630\div 5=6
  • 同时被2255整除的:30÷lca(2,5)=30÷10=330\div lca(2,5)=30 \div 10=3
  • 总个数:15+6(21)×3=1815+6-(2-1)\times 3=18个(就是被绿圈和橙圈捆住的的数的个数)

那么对于223355整除问题,中间的3030被重复加了3次,需减去两次平衡收支。此即容斥原理的简单思想表示。

回到欧拉函数推导上来:过渡公式TS1TS_1中的容斥问题可以解决一部分了。对于可同时被两个不同质数整除的数(例如6=2×36=2\times315=3×515=3\times5),我们加上它的总个数。 得到TS2=TS1+[np1p2]+[np1p3]++[np1pk]+[np2p3]+[np2p4]++[npk1pk]TS_2=TS_1+[\frac{n}{p_1p_2}]+[\frac{n}{p_1p_3}]+\dots+[\frac{n}{p_1p_k}]+[\frac{n}{p_2p_3}]+[\frac{n}{p_2p_4}]+\dots+[\frac{n}{p_{k-1}p_k}]

当然这又有一个小问题没完没了了是不是?对于p1  p2  p3p_1\;p_2\;p_3的公倍数,会被先减去3次,然后被上一步的操作加上3次,总体不加不减。还是回到上图:中间的3030会被每个颜色的圈先减去一次、共3次,上一步的补偿操作,可以看作又被橙绿圈(橙圈和绿圈的交集)、蓝绿圈、蓝橙圈一共加上了3次。减3次加3次相当于没动,为了让它被算上,我们需要加上它,对于φ(n)\varphi(n)则是全部减去(因为括号外有减号需要变号,不要忘记φ(n)\varphi(n)是由一系列不合规的数字个数相减得来的)。得到我们的过渡态3:TS3=TS2[np1p2p3][np1p2p4][np1p2p5][npk2pk1pk]TS_3=TS_2-[\frac{n}{p_1p_2p_3}]-[\frac{n}{p_1p_2p_4}]-[\frac{n}{p_1p_2p_5}]-\dots-[\frac{n}{p_{k-2}p_{k-1}p_{k}}]

又是如上的容斥判断,这里我们省去讨论。将最终的产物合并得到:φ(n)=n(11p1)(11p2)(11pk1)(11pk)\varphi(n)=n*(1-\frac{1}{p_1})(1-\frac{1}{p_2})\dots(1-\frac{1}{p_{k-1}})(1-\frac{1}{p_k})!(我不会合并,但是你可以把φ(n)\varphi(n)括号拆开看看是不是上述形式。总之,欧拉牛逼!)

QEDQED,好耶!终于可以好耶了……

Div2. 欧拉函数代码实现#

主要是如果压成一个Div会非常的长,因此这里新开一个Div2

我们明确了欧拉函数的推导,接下来就是整理思路写代码的时间了!我们也只需跟着原始思路走就可以了。再次回忆一下:首先我们需要筛出质因数,除去它的所有倍数,再用公式φ(n)=n(11p1)(11p2)(11pk1)(11pk)\varphi(n)=n*(1-\frac{1}{p_1})(1-\frac{1}{p_2})\dots(1-\frac{1}{p_{k-1}})(1-\frac{1}{p_k})代入pip_i就可以了。

long long eular(int n) {
long long res = n;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
res = res * (i - 1) / i;
while (n % i == 0) n /= i;
}
}
if (n > 1) res = res * (n - 1) / n;
return res;
}

时间复杂度:O(n)\mathcal O(\sqrt n)

适用范围:All Clear

Div3. 欧拉函数推论#

  1. pp为质数,则φ(p)=p1\varphi(p)=p-1

让我们回到欧拉函数的定义上去:φ(i)\varphi(i)[1,i][1,i]中与ii互质的数的个数(特殊地,φ(1)=1\varphi(1)=1)。那么对于pp这个质数,有多少数与它互质呢?

很显然,答案是p1p-1个!因为pp的质因子只有pp本身,若不止pp一个质因子,很显然它不是一个质数。因此φ(p)=p(11p)=p1\varphi(p)=p(1-\frac{1}{p})=p-1

  1. 假设pkp_k是一个质数,pkip_k\mid i(或imodpk=0i\bmod p_k=0),且φ(i)\varphi(i)的值已知,那么φ(pki)=pkφ(i)\varphi(p_k*i)=p_k\cdot\varphi(i)

凭啥呀?

因为pkp_k已经是一个质数,换句话说:在这个条件下pkp_kpkip_k*i的一个质因子。在计算φ(i)\varphi(i)时,pkp_k就已经作为一个质因子以(11pk)(1-\frac{1}{p_k})的形式乘进去了。此时φ(i)\varphi(i)可以写作

φ(i)=i(11p1)(11p2)(11pk)(11pn)(1)\varphi(i)=i(1-\frac{1}{p_1})(1-\frac{1}{p_2})\dots(1-\frac{1}{p_k})\dots (1-\frac{1}{p_n})\dots \dots(1)

那么函数值多乘了一个pkp_k

φ(pki)=pki(11p1)(11p2)(11pk)(11pn)(2)\varphi(p_k*i)=p_k\cdot i(1-\frac{1}{p_1})(1-\frac{1}{p_2})\dots(1-\frac{1}{p_k})\dots (1-\frac{1}{p_n})\dots\dots(2)

我们发现:(2)式中包含了(1)式,只是头上乘以了pkp_k。因而得到φ(pki)=φ(i)pk\varphi(p_k*i)=\varphi(i)\cdot p_k

  1. 假设pkp_k是一个质数,pkip_k\nmid i(或imodpk0i\bmod p_k\neq0),且φ(i)\varphi(i)的值已知,那么φ(pki)=pkφ(i)(11pk)=φ(i)(pk1)\varphi(p_k*i)=p_k\cdot\varphi(i)\cdot(1-\frac{1}{p_k})= \varphi(i)\cdot(p_k-1)

这东西长得和性质2很相似,唯一不同的是pkp_k不再是ii的一个质因子了。但是pkp_k变成了pkip_k*i的质因子。因此我们计算φ(pki)\varphi(p_k*i)的值时,不仅需要在头部乘上pkp_k,而且还需要将(11pk)(1-\frac{1}{p_k})乘进去:

φ(pki)=pki(11p1)(11pn)(11pk)\varphi(p_k*i)=p_k\cdot i\cdot (1-\frac{1}{p_1})\dots(1-\frac{1}{p_n})(1-\frac{1}{p_k})

因为pk(11pk)=pk1p_k\cdot (1-\frac{1}{p_k})=p_k-1,所以得到性质3,即φ(pki)=pkφ(i)(11pk)=φ(i)(pk1)\varphi(p_k*i)=p_k\cdot\varphi(i)\cdot(1-\frac{1}{p_k})= \varphi(i)\cdot(p_k-1)

这三个性质将作为重点性质出现在欧拉函数筛法中。

Div4. 欧拉函数线性筛#

我们已经接触了简单的欧拉函数计算方法,那么又该如何解决形如:“给定一个正整数i[x,y]i\in [x,y],求i=xyφ(i)\sum\limits_{i=x}^{y}\varphi(i)的值”的问题呢?

考虑继续使用上面的朴素算法,时间复杂度将会是nnn\sqrt n。明显无法满足需求,更何况,每个数与每个数之间的φ\varphi值之间有一种推导关系,使得我们无需每次重新计算φ\varphi值,而是用已经求出的φ(n)\varphi(n)来线性推出φ(i)\varphi(i)的值。

欧拉函数涉及到质因子的拆分,我们又需要在线性时间内求各种质数。自然而然想到了先前所学的线性筛:

int primes[N];
bool st[N];
int cnt = 0;
void sieve(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[++cnt] = i;
for (int j = 1; primes[j] * i <= n; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}

我们运用这段代码可以得出[1,n][1,n]范围内所有的质数,用st[N]数组可以筛出所有的合数。也就是说对于筛出的合数,我们能够得知组成它的质因子是什么,比如st[primes[j]*i]=true;这一行代码。接着套用上述三种性质,我们可以得出φ\varphi值。

此时我们就需要新建一个phi[N]数组来存储每个数的φ\varphi值,并且在代码中三个地方加入对于三种性质的公式:

typedef long long ll;
ll primes[N];
bool st[N];
int cnt = 0;
ll phi[N];
void phi_sieve(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) {
primes[++cnt] = i;
phi[i] = i - 1;
}
for (int j = 1; primes[j] * i <= n; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) {
phi[primes[j] * i] = phi[i] * primes[j];
break;
}
phi[primes[j] * i] = phi[i] * (primes[j] - 1);
}
}
}

没错我开了long long防止爆int

时间复杂度:O(n)\mathcal O(n)

适用范围:普及&提高

对于开头提出的求和问题,遍历phi[1]phi[n]的所有值求和即可。

Div5. 欧拉定理#

若正整数aann互质,则有aφ(n)1(modn)a^{\varphi(n)}\equiv1\pmod n

对于它的证明,百度百科中如此写到:

mm的缩系a1,a2,,aφ(m)a_1,a_2,\dots,a_{\varphi(m)},故aa1,aa2,,aaφ(m)aa_1,aa_2,\dots,aa_{\varphi(m)}也为mm的缩系。有i=1φ(m)aii=1φ(m)aaiaφ(m)i=1φ(m)ai(modm)\prod\limits_{i=1}^{\varphi(m)}a_i\equiv\prod\limits_{i=1}^{\varphi(m)}aa_i\equiv a_{\varphi(m)}\prod\limits_{i=1}^{\varphi(m)}a_i\pmod m

通俗来讲就是这样:

  1. [1,n][1,n]中取所有与nn互质的数a1,a2,aφ(n)a_1,a_2\dots,a_{\varphi(n)},很容易知道这样的aa共有φ(n)\varphi(n)个(根据欧拉函数定义得来)。它们都与nn互质。
  2. 给这列数同时乘上aa,得到aa1,aa2,,aaφ(n)aa_1,aa_2,\dots,aa_{\varphi(n)}。它们也都和nn互质,并且各不相同。
  3. 提出括号里乘了φ(n)\varphi(n)次的aa,得到以下关系式:aφ(n)[a1a2aφ(n)]a1a2aφ(n)(modn)a^{\varphi(n)}[a_1a_2\dots a_{\varphi(n)}]\equiv a_1a_2\dots a_{\varphi(n)}\pmod n
  4. 那么根据同余号两端的消去原则(左右两端两个项相同且与模nn互质),可以消去a1a2aφ(n)a_1a_2\dots a_{\varphi(n)}。得到aφ(n)1(modn)a^{\varphi(n)}\equiv1\pmod n,欧拉定理得证。
  5. 特殊地,如果pp是一个质数,有aφ(p)1(modp)ap11(modp)a^{\varphi(p)}\equiv1\pmod p\Rightarrow a^{p-1}\equiv1\pmod p。这被称作费马小定理(先前的费马素性检验就是基于这个原理编写的)。

真不知道明明可以写得通俗点为什么非得省那点空间写看起来那么高深莫测的专业术语,真的是只写给自己看的。

Div6. 降幂算法#

尤其对于绿题以上的题目,题面中可能出现“答案可能很大,请对大质数pp取余”的字样。这意味着题目可能涉及到大规模的幂运算,需要我们用简便的方法计算幂。对于一般的题目,我们使用快速幂。

快速幂:快速幂思想如下:

ab={ab2×ab2,bmod2=0a×ab12×ab12,bmod2=1a^b= \begin{cases}a^{ \frac{b}{2}}\times a^{ \frac{b}{2}},& b\bmod 2=0\\a\times a^{ \frac{b-1}{2}}\times a^{ \frac{b-1}{2}},&b\bmod2=1 \end{cases}

我们将指数bb分解为若干2n2^n的和(二进制表示),例如:a11=a20a21a23a^{11}=a^{2^0}\cdot a^{2^1}\cdot a^{2^3},因为(11)10=(1011)2(11)_{10}=(1011)_2。因而不必将aa连续乘11次,效率大幅提升。

typedef long long ll;
int qpow(int a, int k, int p) {
int res = 1;
while (k) {
if (k & 1) res = (ll) res * a % p;
a = a * a % p;
k >>= 1;
}
return res;
}

时间复杂度:O(logn)\mathcal O(\log n)

适用范围:基本All Clear

欧拉降幂:上面方法一个缺点在于无法处理过大的指数,在处理类似于a2na^{2^n}的计算时将会疯狂掉san。接下来介绍一种使用上面讲到的欧拉定理来解决大指数幂运算的方法。

欧拉降幂核心公式:akmodp=akmodφ(p)+φ(p)modpa^k\bmod p=a^{k\bmod\varphi(p)+\varphi(p)}\bmod p(又称 扩展欧拉定理

也就是说:我们只需要算出q=kmodφ(p)+φ(p)q=k\bmod\varphi(p)+\varphi(p)的值,再用快速幂算法,将qq作为新指数带入计算即可。当然,这里的kk可能会爆long long,因此可以选择使用字符串进行高精度计算。

typedef long long ll;
ll primes[N];
bool st[N];
int cnt = 0;
int qpow(int a, int k, int p) {
int res = 1;
while (k) {
if (k & 1) res = (ll) res * a % p;
a = a * a % p;
k >>= 1;
}
return res;
}
ll eular(int n) {
ll res = 0;
for (int i = 2; i <= n; i++) {
if (!st[i]) {
primes[++cnt] = i;
res = res * (i - 1) / i;
while (n % i == 0) n /= i;
}
for (int j = 1; i * primes[j] <= n; j++) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
if (n > 1) res = res * (n - 1) / n;
return res;
}
int edp(int a, string k, int p) {
ll phi = eular(p);
int drop = 0;
for (int i = 0; i < k.length(); i++) {
drop *= 10;
drop += k[i] % phi;
}
drop += phi;
return qpow(a, drop, p);
}

其中eular(int n)函数用于计算欧拉函数的值、edp(int a, string k, int p)用于计算降幂后的指数、qpow(int a, int k, int p)是快速幂算法。

时间复杂度:O(logn)\mathcal O(\log n)

适用范围:所有

扩展欧拉定理可谓是欧拉定理的一般形式,它的定义如下:对于任意正整数aakkpp,满足:

ak{ak(mod  p),gcd(a,p)1,k<φ(p)akmodφ(p)+φ(p)(mod  p),gcd(a,p)1,kφ(p)akmodφ(p)(mod  p),gcd(a,p)=1a^k\equiv\begin{cases}a^k&(\bmod\;p ),gcd(a,p)\neq1,k<\varphi(p)\\a^{k\bmod\varphi(p)+\varphi(p)}&(\bmod\;p),gcd(a,p)\neq1,k\geq\varphi(p)\\a^{k\bmod\varphi(p)}&(\bmod\;p),gcd(a,p)=1\end{cases}

其中第二个式子就是欧拉降幂的核心公式。

扩展欧拉定理的证明见这里因为太复杂了我不会证


例题:

  1. P2158 [SDOI2008] 仪仗队 (欧拉函数板子)
  2. P1447 [NOI2010] 能量采集(上一个问题的变式)
  3. P1226 [模板] 快速幂
  4. P5091 [模板] 扩展欧拉定理 (欧拉降幂)
  5. P4139 上帝与集合的正确用法 (欧拉降幂+递归)

Part5. 同余方程的解法#

这里会涉及到一元线性同余方程,一元线性同余方程组和高次同余方程的算法解法。

Div.1 裴蜀定理#

很多人会把他读成裴除(chú)(比如我的某位好友),这个名词正确的读法是裴蜀(shǔ)。或者可以直接改称作“贝祖定理”,它的提出者艾蒂安·裴蜀估计怎么也没想到后人居然连他的名字都读不对(想想如果这种事情发生到你身上会怎么样)。你也可以读他名字的法语发音BeˊzoutB\acute ezout(显得你很优雅且有文化)。

切入正题,裴蜀定理表述为:对于任意正整数aabb,总有整数xxyy,使得ax+by=(a,b)ax+by=(a,b),其中(a,b)(a,b)等价于gcd(a,b)gcd(a,b),是数论中最大公约数的表述方式。

首先可以知道at+bn=d      (n,tZ      n,t0)at+bn=d\;\;\;(n,t\in\mathbb Z\;\;\;n,t\neq0)(a,b)d(a,b)\mid d,因为aabb都具有约数(a,b)(a,b),让他们分别乘上另两个数ttnn并不会改变(a,b)(a,b)这一约数。所以假设d=k(a,b),kZd=k\cdot(a,b),k\in\mathbb Z,有atk+bnk=(a,b)a\frac{t}{k}+b\frac{n}{k}=(a,b)。在这里,x=tk    y=nkx=\frac{t}{k}\;\;y=\frac{n}{k}

Div2. 扩展欧几里得(EXGCD)#

加了“扩展”二字是不是感觉逼格上来了?

扩展欧几里得算法用于求出线性同余方程的解。线性同余方程,即形如axb(modp)ax\equiv b\pmod p的方程,我们需要求出xx的值。

回忆一下欧几里得算法的核心思路:(a,b)={(a,amodb)amodb0aamodb=0(a,b)=\begin{cases}(a,a\bmod b)&a\bmod b\neq0\\a&a\bmod b=0\end{cases}

再看看刚刚讲到的裴蜀定理,发现(amodb)x+by=k(a,amodb)=k(a,b)=d(a\bmod b)x+by=k\cdot(a,a\bmod b)=k\cdot(a,b)=d。根据余数的定义,有:(aabb)x+by=d(a-\lfloor\frac{a}{b}\rfloor\cdot b)x+by=d

那么我们的任务就是求出这里的xxyy值,因此拆开括号,整理出aabb的系数:ax+b(yabx)=dax+b(y-\lfloor\frac{a}{b}\rfloor x)=d。观察裴蜀定理的形式:ax+by=(a,b)ax+by=(a,b),我们得出的式子中,yy变成了yabxy-\lfloor\frac{a}{b}\rfloor x。因此每次递归时需要将yy的值减去abx\lfloor\frac{a}{b}\rfloor x

既然我们设计的是一个递归算法,我们就必须明确它的递归出口。根据欧几里得算法,当amodb=0a\bmod b=0时,(a,b)=a(a,b)=a。我们把aaamodba\bmod b代入发现:ax+(amodb)y=(a,b)=aax+(a\bmod b)y=(a,b)=a,得到ax+0y=aax+0y=a,此时yy可取任意整数值,x=1x=1。这里我所取的解是{x=1y=0\begin{cases}x=1\\y=0\end{cases}

最后,因为这本质上还是一个欧几里得算法,所以返回(a,b)(a,b)是有必要的(事实上exgcd算法返回的(a,b)(a,b)将作为推导式中的dd参与运算)。我们可以写出如下函数。

int exgcd(int a, int b, int &x, int &y) {
if (!b) {
x = 1;
y = 0;
return a;
}
int d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}

时间复杂度:O(logn)\mathcal O(\log n)

但是,题目中一般不会给出裴蜀定理那样的形式,而是形如axb(modm)ax\equiv b\pmod m的形式,让你求出xx的值,并且上述方法仅求出了一元线性同余方程的一组特解,如果题目中让你求出最小正整数解呢?接下来就是解决上述问题的方法:

1. 同余—等式互转(自己起的名字):

在上面的介绍中,我们遇到了一个问题:如何将axb(modm)ax\equiv b\pmod m这样的同余式变为ax+by=cax+by=c这样的二元一次不定方程的裴蜀定理形式呢?

考虑到同余方程的定义(或者你可以把以下关系死记住),得到axmodm=bax\bmod m=b。接着由余数定义,得到ax=by+m,yZax=by+m,y\in\mathbb Z,移项得到:axby=max-by=m。提出负号,令b=bb^\prime=-b,则ax+by=max+b^\prime y=m。它有解的充要条件是m(a,b)m\mid(a, b)。经过如上变换后就变成了裴蜀定理的形式,可以直接用exgcd求解xxyy

2. 最值解问题

二元一次不定方程通解的证明

Div3. 中国剩余定理(CRT)#

又称孙子定理(但我认为还是中国剩余定理听起来更有实力一些),最早见于《孙子算经》中“物不知数”问题,首次提出了有关一元线性同余方程的问题与解法。

对于一元线性同余方程:S{xa1(modp1)xa2(modp2)xak(modpk)S\rightarrow\begin{cases}x\equiv a_1\pmod{p_1}\\x\equiv a_2\pmod{p_2}\\\vdots\\x\equiv a_k\pmod{p_k}\end{cases},可以构造以下方法求出通解。

首先,令M=p1p2p3pk=i=1kpiM=p_1\cdot p_2\cdot p_3\dots p_k=\prod\limits_{i=1}^{k}p_i

然后,令Mi=Mpi  (1ik)M_i=\frac{M}{p_i}\;(1\leq i\leq k),即除了pip_i外所有pp的乘积。

接着,令tit_iMiM_i在模pip_i意义下的逆元,即tiMi1(modpi)t_i\cdot M_i\equiv1\pmod{p_i}

所以,SS的通解为:x=a1t1M1+a2t2M2++aktkMk+kM=kM+i=1kaipiMix=a_1t_1M_1+a_2t_2M_2+\dots+a_kt_kM_k+kM=kM+\sum\limits_{i=1}^{k}a_ip_iM_i

Div4. Baby Step Giant Step算法(BSGS)#

这个算法用于解决一元高次同余方程问题,模意义下的对数也可以求。又称“北上广深算法”(想出这种名字的人真是人才)

高次同余方程长成这个样子:

axb(modm)a^x\equiv b\pmod m

发现xx跑到了指数上边真是变态呢。这种问题显然没公式解,于是苦恼的人们只得选择一条略显暴力的求解道路,即搜索。严格来说,BSGS所使用的是双搜索,其中的一个变量的搜索步长会长于另一个变量的搜索步长,因而得名“大步小步算法”。或者叫北上广深/拔山盖世算法!

朴素BSGSaamm互质):不妨令x=AtBx=At-B,原式为aAtBb(modm)a^{At-B}\equiv b\pmod m。根据消去原则,两边同乘aBa^BaAtbaB(modm)a^{At}\equiv ba^B\pmod m

接下来我们对同余号右侧的部分求值,再任命一个固定的tt值,使得左侧模mm的值等于右侧模mm的值。为了快速比对左右侧的值,我们选择将右侧预先计算出来的值存入一个哈希表中,让baBmodmBba^B\bmod m\rightarrow B(键为baBba^B,对应值为BB)。接着就是选择tt值,计算aAtmodma^{At}\bmod m并比对了。

关于哈希表冲突,我们希望找到x,x=AtBx,x=At-B的最小值,因而BB需要尽可能大。每次冲突即代表一个更大的BB值被发现了。因此无需处理冲突问题。

对于tt的选择。可以发现BBφ(m)modt\varphi(m)\bmod t个可能的取值,AAφ(m)/t\lfloor\varphi(m)/t\rfloor个。ttφ(m)\lceil\sqrt{\varphi(m)}\rceil时最佳。因此代码就可以写出来了。

ll bsgs(ll a, ll b, ll m) {
unordered_map<ll, ll> hash;
ll bs = 1;
int t = sqrt(m) + 1;
for (int B = 1; B <= t; B++) {
bs *= a;
bs %= m;
hash[b * bs % m] = B;
}
ll gs = bs;
for (int A = 1; A <= t; A++) {
auto iter = hash.find(gs);
if (iter != hash.end()) return A * t - it->second;
gs *= bs;
gs %= m;
}
return -1;
}

时间复杂度:O(m)\mathcal O(\sqrt m)

扩展BSGSaamm不互质):


例题:

  1. P1082 [NOIP2012 提高组] 同余方程 (exgcd)
  2. P5656 [模板] 二元一次不定方程 (exgcd)
  3. P1495 [模板] 中国剩余定理(CRT)/ 曹冲养猪
  4. P1516 青蛙的约会 (CRT+exgcd)
  5. P3846 [TJOI2007] 可爱的质数/ [模板] BSGS
  6. P2485 [SDOI2011] 计算器 (欧拉降幂+乘法逆元+BSGS)
  7. P3306 [SDOI2013] 随机数生成器 (等比数列推导+BSGS)
  8. P4195 [模板] 扩展 BSGS/exBSGS

Part6. 乘法逆元#

乘法逆元定义如下(注意和矩阵求逆不是一个东西):

ax1(mod  b)a\cdot x\equiv1(\bmod\;b),且aabb互质,则xxaa在模bb条件下的乘法逆元,记作a1a^{-1}

简单来说乘法逆元xx就是模bb意义下的aa的倒数。

费马小定理求逆元:大部分题目会给出一个质数模数,因而互质是可以保证的。此时我们的乘法逆元就是使式子ax1(mod  p)a\cdot x\equiv1(\bmod\;p)成立的xx值,考虑到模数pp为质数,可以带回开头所说的费马小定理中。

得到ap11(mod  p)a^{p-1}\equiv1(\bmod\;p),由于aapp互质,消去得:aap21(mod  p)a\cdot a^{p-2}\equiv1(\bmod\;p),所以乘法逆元为ap2mod  pa^{p-2}\bmod\;p

int inv(int a, int p) {
return qpow(a, p - 2, p);
}

扩展欧几里得求逆元:这是万能的方法,对任意模数均成立。它不像上面费马小定理那样限制模数必须是质数,因而只要时间充裕,都建议使用这种求逆元的方式。

因为ax1(modb)ax\equiv 1\pmod b,运用同余-等式互转可以得到ax1=byax+by=1(modb),y=yax-1=by\rightarrow ax+by^\prime=1\pmod b,y^\prime=-y。符合exgcd的形式。

int exgcd(int a, int b, int &x, int &y) {
if (!b) {
x = 1;
y = 0;
return a;
}
int d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
int inv(int a, int m) {
int x, y;
exgcd(a, m, x, y);
return (x + m) % m;
}

递推求逆元


例题:

  1. P3811 [模板] 模意义下的乘法逆元 (递推求逆元)

来张弔图

Part7. 矩阵相关#

矩阵,是一个按照长方排列的实数或复数集合。它最早用来表示方程组中的系数和常数,简单理解就是它将nn元一次方程组中的系数,按照未知数的顺序依次挑出它们的系数组合为矩阵的某一行。nn元一次方程的矩阵有nn列,而行数则取决于方程组中方程的个数。

Div1. 初等行变换#

考虑这个方程组:

{2x+3y=11x2z=62x+2y4z=23\begin{cases}2x+3y=-1&\dots1\\x-2z=6&\dots2\\x+2y-4z=-2&\dots3\end{cases}

按照如上所述,将它转换为系数矩阵(只有x,y,zx,y,z的系数)就是:

(230120124)\begin{pmatrix}2&3&0\\1&-2&0\\1&2&-4\end{pmatrix}

你也可以写成增广矩阵(与系数矩阵相比多了一列常数,即等号右边的常数,这里用竖线隔开)的形式:

(230112061242)\left(\begin{array}{ccc|c}2&3&0&-1\\1&-2&0&6\\1&2&-4&-2\end{array}\right)

不难看出第一列代表了xx的系数,第二列和第三列是yyzz的系数。那么如果需要求解这个矩阵(得到方程组的解),我们应该通过初等行变换将它变成方便我们求解的模式。初等行变换内容如下(最好用方程组消元的思想简化理解):

  1. 交换某两行
  2. 把矩阵的某一行同乘以一个非零的数
  3. 把某行的若干倍加和到另一行

假设我们有一个nn元线性方程组,如何设计算法使计算机能够快速求出它的解呢。我们需要引入三角矩阵的概念:

顾名思义,系数排列看起来像一个三角形的矩阵,叫做三角矩阵。分为上三角矩阵和下三角矩阵。前者的非零系数均分布在对角线的右上方、后者都在左下方,例如矩阵:U(52217024910076400028)U\rightarrow\begin{pmatrix}5&2&2&1&7\\0&-2&4&9&-1\\0&0&7&6&4\\0&0&0&2&8\end{pmatrix}就是一个上三角矩阵(这里是增广矩阵)。通常用字母UU表示,求解线性方程组时经常化为这种形式方便求解:本例中当最后一个未知数(见最后一行)已知时,可以通过向上代入求解每一行中待求的未知数值。

那么如何将一个一般矩阵转换为上三角矩阵呢?答案是前面介绍过的初等行变换!步骤如下:

  1. 枚举每一列cc,选出无序组中第cc列系数绝对值最大的一行pp,并移到无序组的最上边。
  2. pp行通过自乘,将第cc列的系数变成11,并标记pp为有序。
  3. 通过加减有序组中某一行的非零倍,将之后所有行的第cc列系数化为00

文字还是太抽象,我们来举个例子:

令矩阵A(211141151110)A\rightarrow\left(\begin{array}{ccc|c}2&-1&1&1\\4&1&-1&5\\1&1&1&0\end{array}\right)(有序组用绿色表示)

枚举第一列,c=1c=1。开始时,所有行均无序。选出绝对值最大的那一项,本例中为第二行,进行移动,原矩阵变为:

(411521111110)\left(\begin{array}{ccc|c}4&1&-1&5\\2&-1&1&1\\1&1&1&0\end{array}\right)

第二步,自乘并标记有序,因此第一行除以44,原矩阵就变成了:

(10.250.251.2521111110)\left(\begin{array}{ccc|c}\textcolor{green}{1}&\textcolor{green}{0.25}&\textcolor{green}{-0.25}&\textcolor{green}{1.25}\\2&-1&1&1\\1&1&1&0\end{array}\right)

第三步,将无序组的第cc列消成00。本例中,我们让第二行减去二倍第一行;第三行直接减去第一行,得到:

(10.250.251.2501.51.51.500.751.251.25)\left(\begin{array}{ccc|c}\textcolor{green}{1}&\textcolor{green}{0.25}&\textcolor{green}{-0.25}&\textcolor{green}{1.25}\\0&-1.5&1.5&-1.5\\0&0.75&1.25&-1.25\end{array}\right)

枚举第二列,此时c=2c=2。第一步,选出第二列系数绝对值最大的那一行,移到无序组最上端。本例中无需移动,自乘23-\frac{2}{3},标记有序,原矩阵为:

(10.250.251.25011100.751.251.25)\left(\begin{array}{ccc|c}\textcolor{green}{1}&\textcolor{green}{0.25}&\textcolor{green}{-0.25}&\textcolor{green}{1.25}\\\textcolor{green}{0}&\textcolor{green}{1}&\textcolor{green}{-1}&\textcolor{green}{1}\\0&0.75&1.25&-1.25\end{array}\right)

最终的最终,第三行减0.750.75倍第二行,得到我们心心念念的上三角矩阵:

U(10.250.251.2501110022)U\rightarrow\left(\begin{array}{ccc|c}1&0.25&-0.25&1.25\\0&1&-1&1\\0&0&2&-2\end{array}\right)

我们假设从左到右,分别为xxyyzz的系数,竖线右侧为常数。矩阵可以改写成方程组的形式:

{x+0.25y0.25z=1.25yz=12z=2\begin{cases}x+0.25y-0.25z=1.25\\y-z=1\\2z=-2\end{cases}

根据最后一行,显然z=1z=-1。将zz代入2式,解得y=0y=0,以此类推,由下向上代入解出的值即可,本例的唯一解是:{x=1y=0z=1\begin{cases}x=1\\y=0\\z=-1\end{cases}

然而心细的你估计发现了疏漏之处:“求一元二次方程时都要先检验根是否存在(Δ\Delta判别式法)再来作答,你这里怎么没有讨论根的分布情况呢?”

事实上,矩阵的解的分布确实不止一种情况,这里是矩阵有唯一解的情况。类比高中立体几何求平面法向量的情景,我们通常都要令某个坐标为11或者是其他方便于计算的值,这里就是矩阵有无数组解的经典例子。要想系统分析矩阵方程解的数量情况,我们需要引入的概念。

Div2. 秩#

在上一节中我们通过初等行变换求出了矩阵的解,然而并不是所有矩阵都能轻而易举求出唯一解,因为它可能无解、也有可能无唯一解(默认最高次数为一)。类比一元二次方程中的Δ\Delta判别式法,矩阵是否也有判断根存在性的方法?

答案是:有滴!在矩阵运算中,我们使用来描述矩阵的一些关于解的个数的关系。秩被定义为:将矩阵通过初等行变换后形成的梯形矩阵中非零行的个数。试看如下例子:

定义一个3×23\times2的矩阵:(231693072)\begin{pmatrix}2&3&1\\6&9&3\\0&7&2\end{pmatrix}

经过初等行变换后出现了这样的情况:

(410000072)\begin{pmatrix}4&-1&0\\0&0&0\\0&7&2\end{pmatrix}(第二行减去乘3的第一行,第一行乘2减去第三行)

第二行变成了纯00的一行,一、三行说什么都无法消成一个未知数的形式。如果写成方程组就是:

{4xy=a17y+2z=a2\begin{cases}4x-y=a_1\\7y+2z=a_2\end{cases}

它有无数组解,原因是:矩阵的秩与矩阵增广矩阵的秩相等且小于了它的阶。简单来说就是你用两个方程去求三个未知数的值(初一内容),当然是有无数多组解。

规定对于矩阵AA,它的秩用R(A)R(A)表示(r(A)r(A)rk(A)rk(A)rank(A)rank(A)均可)。因此令方程组的nn阶增广矩阵秩为R(A)R(A),系数矩阵的秩为R(B)R(B)。矩阵有无数组解的条件就是R(B)<nR(B)<n(严格来说:有无数组解的充要条件是R(A)=R(B)<nR(A)=R(B)<n

看第二个例子:

定义增广矩阵:A(283132271)A\rightarrow\left(\begin{array}{cc|c}2&8&3\\1&3&-2\\2&7&1\end{array}\right);它的系数矩阵:B(281327)B\rightarrow\begin{pmatrix}2&8\\1&3\\2&7\end{pmatrix}

增广矩阵变换后:(0122025003)\left(\begin{array}{cc|c}0&1&2\\2&0&-25\\0&0&3\end{array}\right);系数矩阵:(012000)\begin{pmatrix}0&1\\2&0\\0&0\end{pmatrix}

根据定义,得到R(A)=3R(A)=3R(B)=2R(B)=2,此时R(A)>R(B)R(A)>R(B)。方程组无解。因而矩阵无解的充要条件是R(A)>R(B)R(A)>R(B)。简单理解起来就是方程组中的两个方程起了冲突,矩阵AA被省去的其中一步变换是:(0122025015)\left(\begin{array}{cc|c}0&1&2\\2&0&-25\\0&1&5\end{array}\right),第一行和第三行相当于要你求解如下的方程组:{y=2y=5\begin{cases}y=2\\y=5\end{cases}。显然矛盾,因此矩阵无解。

加上第一节里面的结论,我们总结出了矩阵解分布的三种情况(方程组的增广矩阵为AA、系数矩阵为BB,阶为nn):

  1. R(A)=R(B)=nR(A)=R(B)=n时,矩阵有唯一解
  2. R(A)=R(B)<nR(A)=R(B)<n时,矩阵有无数解
  3. R(A)>R(B)R(A)>R(B)时,矩阵无解

因此就有了一套组合算法:

#include <bits/stdc++.h>
#define N 110
#define NO_SOLUTION -1
#define INFINITE 0
#define SOLVE_OK 1
using namespace std;
typedef long long ll;
double mat[N][N];
int n;
double eps = 1e-6;
int solve() {
int rank = 0;
for (int c = 0, r = 0; c < n; c++) {
int t = r;
for (int i = r; i < n; i++) {
if (fabs(mat[i][c]) > fabs(mat[t][c])) t = i;
}
if (fabs(mat[t][c]) < eps) continue;
for (int i = c; i <= n; i++) swap(mat[r][i], mat[t][i]);
for (int i = n; i >= c; i--) mat[r][i] /= mat[r][c];
for (int i = r + 1; i < n; i++) {
if (fabs(mat[i][c]) > eps) {
for (int j = n; j >= c; j--) {
mat[i][j] -= (mat[r][j] * mat[i][c]);
}
}
}
r++;
rank = r;
}
if (rank < n) {
for (int i = rank; i < n; i++) {
for (int j = 0; j < n + 1; j++) {
if (fabs(mat[i][j]) > eps) return NO_SOLUTION;
}
}
return INFINITE;
}
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
mat[i][n] -= mat[i][j] * mat[j][n];
}
}
return SOLVE_OK;
}
int main() {
cin>>n;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n + 1; j++)
cin>>mat[i][j];
}
int res = solve();
if (res != SOLVE_OK) cout<<res<<endl;
else for (int i = 0; i < n; i++) {
if (fabs(mat[i][n]) < eps) mat[i][n] = fabs(mat[i][n]);
printf("x%d=%.2lf\n", i + 1, mat[i][n]);
}
return 0;
}

时间复杂度:O(n3)\mathcal O(n^3)

以后上大学解高次线性方程就可以用这段程序秒了。

Div3. 矩阵基本运算#

1. 加法

(238142)+(604335)=(2+63+08+41+34+32+5)=(8312477)\begin{pmatrix}2&3&8\\1&4&2\end{pmatrix}+\begin{pmatrix}6&0&4\\3&3&5\end{pmatrix}=\begin{pmatrix}2+6&3+0&8+4\\1+3&4+3&2+5\end{pmatrix}=\begin{pmatrix}8&3&12\\4&7&7\end{pmatrix}

注意类比nn元一次方程组的加减消元,两个矩阵相加意味着同一位置的元素相加。需要注意:只有同型的矩阵才有加法运算(同型即行数列数相等)。

可以知道,四则运算的加法交换律和结合律仍然适用于矩阵加法。

2. 减法

(238142)(604335)=(263084134325)=(434213)\begin{pmatrix}2&3&8\\1&4&2\end{pmatrix}-\begin{pmatrix}6&0&4\\3&3&5\end{pmatrix}=\begin{pmatrix}2-6&3-0&8-4\\1-3&4-3&2-5\end{pmatrix}=\begin{pmatrix}-4&3&4\\-2&1&-3\end{pmatrix}

加法的逆运算,让矩阵同一位置的元素相减即可。也是仅限于同型矩阵之间才可做减法。

3. 数乘

4(142615360)=(1×44×42×46×41×45×43×46×40×4)=(41682442012240)4\cdot\begin{pmatrix}1&4&2\\6&1&5\\3&6&0\end{pmatrix}=\begin{pmatrix}1\times4&4\times4&2\times4\\6\times4&1\times4&5\times4\\3\times4&6\times4&0\times4\end{pmatrix}=\begin{pmatrix}4&16&8\\24&4&20\\12&24&0\end{pmatrix}

即矩阵中每个元素都跟数字相乘。符合乘法交换律和结合律

矩阵的加法、减法和数乘合称为矩阵的线性运算

4. 转置

矩阵AA的转置矩阵用ATA^T表示。

(142615)T=(164125)\begin{pmatrix}1&4&2\\6&1&5\end{pmatrix}^T=\begin{pmatrix}1&6\\4&1\\2&5\end{pmatrix}

直观来讲就是将原矩阵旋转一下(行和列互换)。满足如下性质:

(AT)T=A(A^T)^T=A (转置一次后再转置一次还是原来的矩阵)

(λA)T=λAT(\lambda A)^T=\lambda A^T (常数转置后就是它本身)

(AB)T=ATBT(AB)^T=A^TB^T (上一条是它的特殊形式,类比两数乘积的幂)

5. 共轭

矩阵AA的共轭矩阵用A\overline A表示。

A=(2+i854i2i)A=\begin{pmatrix}2+i&8\\5-4i&2i\end{pmatrix}

A=(2i85+4i2i)\overline A=\begin{pmatrix}2-i&8\\5+4i&-2i\end{pmatrix}

类比共轭复数的定义:实部不变、虚部取相反数。矩阵共轭变换就是将矩阵中的所有复数变为其共轭形式。

6. 共轭转置

矩阵AA的共轭转置矩阵记作AA^*AT\overline A^TAT\overline{A^T}AHA^H

A=(2+i854i2i1+i22i)A=\begin{pmatrix}2+i&8\\5-4i&2i\\1+i&2-2i\end{pmatrix}

AH=(2i5+4i1i82i2+2i)A^H=\begin{pmatrix}2-i&5+4i&1-i\\8&-2i&2+2i\end{pmatrix}

字面意思,先取共轭,再转置。它具备转置矩阵的三条性质。

Div4. 矩阵乘法#

只有一个矩阵的行数和另一个矩阵的列数相等时才可进行乘法运算

例如一个n×pn\times p矩阵A(142308)A\rightarrow\begin{pmatrix}1&4&2\\3&0&8\end{pmatrix}和一个p×mp\times m矩阵B(264138)B\rightarrow\begin{pmatrix}2&6\\4&1\\3&8\end{pmatrix}。记它们的乘积C=ABC=AB。则CC中的某个元素ci,j=ai,1b1,j+ai,2b2,j++ai,pbp,j=k=1nai,kbk,jc_{i,j}=a_{i,1}b_{1,j}+a_{i,2}b_{2,j}+\dots+a_{i,p}b_{p,j}=\sum\limits_{k=1}^{n}a_{i,k}b_{k,j}。并且CC是一个n×mn\times m的矩阵。

因此AB=(1×2+4×4+2×31×6+4×1+2×83×2+0×4+8×33×6+0×1+8×8)=(24263082)AB=\begin{pmatrix}1\times2+4\times4+2\times3&1\times6+4\times1+2\times8\\3\times2+0\times4+8\times3&3\times6+0\times1+8\times8\end{pmatrix}=\begin{pmatrix}24&26\\30&82\end{pmatrix}

它满足结合律、分配律,但是大多数情况下不满足交换律。交换律不成立可以看到下面这个例子:

首先根据定义,CC矩阵的行列数取决于做乘法的两个矩阵AABB的行列数,比如8×38\times3矩阵和3×43\times4矩阵相乘,得到一个8×48\times4矩阵,但是将它颠倒顺序,让一个3×43\times4矩阵与8×38\times3矩阵相乘,结果将是一个4×84\times8矩阵,和前者行列数相反。

对于结果是正方形矩阵的,可以自己随便设置两个矩阵进行计算。但是部分矩阵仍然可以进行交换律运算:矩阵乘一个单位矩阵/数量矩阵[AE=EAAE=EA/A(kE)=(kE)AA(kE)=(kE)A]、矩阵乘它的伴随矩阵(AA=AAAA^*=A^*A)。

Div5. 其他常用类型的矩阵#

1. 零矩阵:顾名思义,由00组成的矩阵称作零矩阵。零矩阵不可逆,且任何符合条件的矩阵与一个零矩阵的积均为零矩阵。

2. 单位矩阵:形如(100010001)\begin{pmatrix}1&0&0\\0&1&0\\0&0&1\end{pmatrix}的矩阵被称作单位矩阵,通常用字母EEII表示。单位矩阵指仅对角线系数为11、且其他系数为00的矩阵。nn阶矩阵与它的逆矩阵相乘得到的结果就是一个nn阶单位矩阵,即AA1=EAA^{-1}=E

3. 数量矩阵:形如(k000k000k),kR\begin{pmatrix}k&0&0\\0&k&0\\0&0&k\end{pmatrix},k\in\mathbb R的矩阵叫数量矩阵,可以看作实数kk与单位矩阵EE进行数乘运算后的结果,通常表示成kE,kRkE,k\in\mathbb R。矩阵与一个数量矩阵的乘积满足乘法交换律。

4. 逆矩阵:如果存在一个矩阵BB和单位矩阵EE,使得AB=E=BAAB=E=BA,则称矩阵AA可逆,BBAA的逆矩阵,也可记作A1A^{-1}。单位矩阵的逆矩阵是它本身;零矩阵不可逆。nn阶矩阵可逆的充要条件是R(A)=nR(A)=n

5. 对称矩阵:转置矩阵与自身相等的矩阵叫做对称矩阵,特征是所有元素关于对角线对称,例如:(035308580)\begin{pmatrix}0&3&5\\3&0&8\\5&8&0\end{pmatrix}。对称矩阵必为方形矩阵,反之不一定成立,对于一个方形矩阵AAA+ATA+A^T必定是对称矩阵。

Div6. 矩阵的几何表示#

平面直角坐标系上,一个向量a=(1,2)\vec a=(1,2)可以被表示成[12]\begin{bmatrix}1\\2\end{bmatrix}的形式,即[xy]\begin{bmatrix}x\\y\end{bmatrix}

计算机中,用两个不共线向量i\vec ij\vec j能够表示整个平面直角坐标系。运用一点高中数学的空间几何知识,这里的i\vec ij\vec j被称作基底(当然,如果需要描述三维空间坐标系,则需要三个不共线的基底向量)。于是我们使用矩阵[xixjyiyj]\begin{bmatrix}\textcolor{red}{x_i}&\textcolor{green}{x_j}\\\textcolor{red}{y_i}&\textcolor{green}{y_j}\end{bmatrix}来描述这个平面直角坐标系就是非常简洁明了且优雅的了。

假设我们常规想法中的平面直角坐标系是[1001]\begin{bmatrix}\textcolor{red}{1}&\textcolor{green}{0}\\\textcolor{red}{0}&\textcolor{green}{1}\end{bmatrix},经过一轮线性变换后得到的新坐标系是:[2312]\begin{bmatrix}\textcolor{red}{2}&\textcolor{green}{3}\\\textcolor{red}{1}&\textcolor{green}{-2}\end{bmatrix}。用一张图看一下变换后的坐标系:

如果在最开始的坐标系中有一个向量a[57]\vec a\rightarrow\begin{bmatrix}5\\7\end{bmatrix},我们如何在新的坐标系中表示它呢?再根据我们高中数学所学,只需要算出5i+7j5\vec i+7\vec j的值即可。因为i\vec ixx轴的基底,相当于xx上的一个单位,我们求新向量时只需求出在新的参考系中的新xx值和yy值,因而直接用xx方向的系数乘以一个单位即可,在这里就是5[21]+7[32]5\cdot\begin{bmatrix}\textcolor{red}{2}\\\textcolor{red}{1}\end{bmatrix}+7\cdot\begin{bmatrix}\textcolor{green}{3}\\\textcolor{green}{-2}\end{bmatrix},得到a[319]\vec a\rightarrow\begin{bmatrix}31\\-9\end{bmatrix}

抽象之后变成:

[abcd][xy]=x[ac]+y[ad]=[ax+bycx+dy]\begin{bmatrix}\textcolor{red}{a}&\textcolor{green}{b}\\\textcolor{red}{c}&\textcolor{green}{d}\end{bmatrix}\begin{bmatrix}x\\y\end{bmatrix}=x\begin{bmatrix}\textcolor{red}{a}\\\textcolor{red}{c}\end{bmatrix}+y\begin{bmatrix}\textcolor{green}{a}\\\textcolor{green}{d}\end{bmatrix}=\begin{bmatrix}\textcolor{red}{a}x+\textcolor{green}{b}y\\\textcolor{red}{c}x+\textcolor{green}{d}y\end{bmatrix}

TODOTODO


例题:

  1. P3389 [模板] 高斯消元法 (上三角矩阵的转换)
  2. P2455 [SDOI2006] 线性方程组 (前一道题的升级版)

Part8. 组合计数#

StarterDiv1. 阶乘概述#

阶乘,数学中用!!表示,n!,nNn!,n\in\mathbb N表示n×(n1)×(n2)×2×1n\times(n-1)\times(n-2)\dots\times2\times1的值,即i=0n1(ni)\prod\limits_{i=0}^{n-1}(n-i)

特殊地,0!=10!=1

StarterDiv2. 常用排列总结#

1. 排列数:数学中用AnmA_{n}^{m}表示(ArrangementArrangement,老教材记作PnmP_{n}^{m}PermulationPermulation)。表示从mm个数中选择nn个进行排列,公式为:Anm=n(n1)(n2)(nm+1)=n!(nm)!A_{n}^{m}=n(n-1)(n-2)\dots(n-m+1)=\frac{n!}{(n-m)!}

为啥呢?,有弔图为证↓

2. 组合数:假设有mm个物品,从中任选出nn个排成一组,叫做组合;所有可能的选法总数叫做组合数。用CnmC_{n}^{m}表示,计算公式为:Cnm=AnmAmm=n!m!(nm)!C_{n}^{m}=\frac{A_{n}^{m}}{A_{m}^{m}}=\frac{n!}{m!(n-m)!}。简记为:乌鸦坐飞机

弔图×2↓

GZ表示就凭这几张图他能速通整个组合数的内容

StarterDiv3. 二项式定理#

学过初中的大家都知道:(x+y)2=x2+2xy+y2(x+y)^2=x^2+2xy+y^2,这是完全平方和公式。高中的一些牛逼娃还知道完全立方和公式,也就是:(x+y)3=(x+y)(x2+xy+y2)=x3+3y2x+3xy2+y3(x+y)^3=(x+y)(x^2+xy+y^2)=x^3+3y^2x+3xy^2+y^3。这些式子其实都是可以由二项式定理套出来的。

二项式定理定义式如下:

(x+y)n=(n0)xny0+(n1)xn1y1+(n2)xn2y2++(nn1)x1yn1+(nn)x0yn=k=0n(nk)xnkyk(x+y)^n=\begin{pmatrix}n\\0\end{pmatrix}x^ny^0+\begin{pmatrix}n\\1\end{pmatrix}x^{n-1}y^1+\begin{pmatrix}n\\2\end{pmatrix}x^{n-2}y^2+\dots+\begin{pmatrix}n\\n-1\end{pmatrix}x^1y^{n-1}+\begin{pmatrix}n\\n\end{pmatrix}x^0y^n=\sum\limits_{k=0}^{n}\begin{pmatrix}n\\k\end{pmatrix}x^{n-k}y^k

这里出现的(nk)=n!k!(nk)!\begin{pmatrix}n\\k\end{pmatrix}=\frac{n!}{k!(n-k)!}。是不是突然发现它和组合数公式的共同之处喽?但是这一章并不会用它,只是作补充知识的说……

有这三条就够了,接下来进入组合计数的内容。

Div1. 高考娃狂喜——组合数计算#

一个小栗子:

宇宙榜一大学阿福大学的榜一博士后导师黑虎阿福给你出了一道难题:

给你两个正整数aabbaba\geq b),让你求出CbaC_{b}^{a}的值。

“这还不简单?”

阿福“好的,我这里将aa设为2085090420850904bb设为17720931772093,请你求解。”

“WTF?”

于是你决定用程序来代替人脑,阿福教授也做出了一定让步,让你求出CbamodpC_{b}^{a}\bmod p的值。但是不幸的是,人类的计算机科学水平自从2024之后就被来自几光年外的八体星人文明发出的“侄子一号(NEPHEW 1)”探测僚机锁定了,因此你需要设计一个高效的计算方式,而不是妄想着用2077年的赛博机器运行暴力计算,来解决这个问题。

一旦你的运行时间超过一秒,阿福教授就会使用战技“乌鸦坐飞机”对你造成大量阶乘伤害。已经学习了阶乘的你想必已了解了它的威力,所以还是老老实实推导公式吧!

递推版:

组合数递推公式:Cab=Ca1b+Ca1b1C_{a}^{b}=C_{a-1}^{b}+C_{a-1}^{b-1}

分析思路类似于动态规划问题:我们要从aa个物品中挑选bb个出来,求组合数。

上图中,若包含这个红色物体,那么我们只需再从剩下的a1a-1个物体里挑选,因为红色物体自身占据了bb个位置中的其中一个,因此留给其他物体的总名额就只有b1b-1个,因此该情况下组合数:Ca1b1C_{a-1}^{b-1};同样地,若不包含红色物体,从剩下的a1a-1个物体中选出bb个,因为在该情况下红色的物体不计入组合,因此剩余名额还是bb个,组合数就是Ca1bC_{a-1}^{b}。最后,因为从aa个物体里选,只有包含红色和不包含红色两种情况(就好像你的双亲,不是你的母亲就是你的父亲),因此可以做到不重不漏。所以总组合数就是Ca1b1+Ca1bC_{a-1}^{b-1}+C_{a-1}^{b}

#include <bits/stdc++.h>
#define N 2010
using namespace std;
int c[N][N];
void Csieve(int p) {
for (int i = 0; i < N; i++) {
for (int j = 0; j <= i; j++) {
if (!j) c[i][j] = 1;
else c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % p;
}
}
}
int main() {
int a, b, p;
cin>>a>>b>>p;
Csieve(p);
cout<<c[a][b]<<endl;
return 0;
}

时间复杂度:O(N2)\mathcal O(N^2)

适用于a,b105a,b\leq10^5的大部分情况。

预处理版:

但是众所周知,递归有两大痛点:对于主观思维来说,是边界问题;对于客观条件来说,是内存。递归过程中CPU里储存了大量的未运行或者待返回的函数实例,当aabb的值增大时,尽管它能在时间方面表现出色,但是内存就不那么理想了,反而会显得臃肿至极。当题目中给出a,b105a,b\geq10^5时,建议用这种方法。

#include <bits/stdc++.h>
#define N 100010
using namespace std;
typedef long long ll;
int fact[N], infact[N];
int qpow(int a, int b, int p) {
int res = 1;
while (b) {
if (b & 1) res = (ll) res * a % p;
a = (ll) a * a % p;
b >>= 1;
}
return res;
}
int inv(int a, int p) {
return qpow(a, p - 2, p);
}
int C(int a, int b, int p) {
return ((fact[a] % p) * (infact[b] % p)) % p * infact[a - b] % p;
}
int main() {
int a, b, p;
cin>>a>>b>>p;
fact[0] = infact[0] = 1;
for (int i = 1; i <= N; i++) {
fact[i] = (ll) fact[i - 1] * i % p;
infact[i] = (ll) infact[i - 1] * inv(i, p) % p;
}
cout<<C(a, b, p)<<endl;
return 0;
}

时间复杂度:O(NlogN)\mathcal O(N\log N)

适用范围,a,b105a,b\geq10^5a,ba,b均在int范围内的大部分情况。

Lucas定理优化版:

LucasLucas定理如下:CabCamodpbmodpCa÷pb÷p(modp)C_{a}^{b}\equiv C_{a\bmod p}^{b\bmod p}\cdot C_{a\div p}^{b\div p}\pmod ppp为质数)。证明在此(建议直接背结论)。

typedef long long ll;
int p;
int qpow(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = (ll) res * a % p;
a = (ll) a * a % p;
b >>= 1;
}
return res;
}
int inv(int a) {
return qpow(a, p - 2);
}
int C(int a, int b) {
int res = 1;
for (int i = 1, j = a; i <= b; i++, j--) {
res = (ll) res * j % p;
res = (ll) res * inv(i) % p;
}
return res;
}
ll lucas(int a, int b) {
if (a < p && b < p) return C(a, b);
return (ll) C(a % p, b % p) * lucas(a / p, b / p) % p;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
int n, m;
cin>>t;
while (t--) {
cin>>n>>m>>p;
cout<<lucas(n + m, n)<<endl;
}
return 0;
}

时间复杂度:O(N)\mathcal O(N)

其本质是套用LucasLucas定理计算CabC_{a}^{b},因为是模pp意义下的除法,因而我们使用逆元来操作除法。

适用范围,a,ba,blong long范围内的大部分情况。

高精度版(选修):

什么?你厌倦了组合数后面挂着的模pp?不妨试试高精度版的组合数计算吧!它适用于作业上的题目求解!(虽然前面几种也可以,毕竟手算的题数据很小取不取模都一样)是不是心动了呢?

常规思路来说,我们的组合数公式经过一轮分式化简可以得到:Cab=a×(a1)×(a2)××(ab+1)b×(b1)×(b2)××2×1C_{a}^{b}=\frac{a\times(a-1)\times(a-2)\times\dots\times(a-b+1)}{b\times(b-1)\times(b-2)\times\dots\times2\times1}。因此我们可以实现高精度的乘除法来计算这个炒鸡长的算式,但是这样不仅效率低下,手写和调试的难度也会增加。我们急切地想知道如何简化成一种高精度算法。

我们看到了Part1里面讲的算术基本定理,将组合数转化为p1c1p2c2p3c3...pkckp_{1}^{c_1}p_{2}^{c_2}p_{3}^{c_3}...p_{k}^{c_k}的质数乘积分解式,最后我们只需要解决质数头顶的指数即可。我们使用以下这个公式:

α(n!)=npnp2npk,pkn,n,kZ\alpha(n!)=\lfloor\frac{n}{p}\rfloor\cdot\lfloor\frac{n}{p^2}\rfloor\cdot\dots\cdot\lfloor\frac{n}{p^k}\rfloor,p^k\leq n, n,k\in\mathbb Z

用它可以计算出n!n!pip_i的个数。

#include <bits/stdc++.h>
#define N 10010
using namespace std;
typedef long long ll;
vector<int> num;
ll primes[N], sum[N];
bool st[N];
int cnt = 0;
void prime_sieve(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[++cnt] = i;
for (int j = 1; i * primes[j] <= n; j++) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
}
int get(int a, int p) {
int res = 0;
while (a) {
res += a / p;
a /= p;
}
return res;
}
vector<int> mul(vector<int> a, int b) {
vector<int> res;
int t = 0;
for (int i = 0; i < a.size(); i++) {
t += a[i] * b;
res.push_back(t % 10);
t /= 10;
}
while (t) {
res.push_back(t % 10);
t /= 10;
}
return res;
}
int main() {
int a, b;
cin>>a>>b;
prime_sieve(a);
for (int i = 1; i <= cnt; i++) sum[i] = get(a, primes[i]) - get(b, primes[i]) - get(a - b, primes[i]);
vector<int> res;
res.push_back(1);
for (int i = 1; i <= cnt; i++) {
for (int j = 1; j <= sum[i]; j++) {
res = mul(res, primes[i]);
}
}
for (int i = res.size() - 1; i >= 0; i--) cout<<res[i];
cout<<endl;
return 0;
}

时间复杂度:O(NN)\mathcal O(N\sqrt N)

适用范围,a,ba,bint范围内。

有了这段代码,我们就可以完成开头阿福教授的原问题了(不模不限数据)!

Div2. 世界上最OI的IDE——Catalan数#

当你翻开Catalan数的介绍文章,并大学特学了一番,感觉自己完全掌握了这神奇的数列,正当你兴致勃勃地打开题库搜索到一道Catalan数的题目正准备大展身手时,你会发现,面对这神奇的题干,不同于往常秒模板题的你,你甚至完全看不出来它和Catalan数有任何的关系,而且很有可能,你其实连Catalan数究竟是什么东西都不知道!

苏子愀然,正襟危坐而问客曰:“何为其然也?” 其实还真不能让那些博客背上黑锅,这种现象与Catalan数本身的应用有很大的关系。

Catalan数,或者习惯叫卡特兰数、明安图数,是组合数学中常用的特殊数列。数列如下:“1,1,2,5,14,42,132,429,1430,4862,1,1,2,5,14,42,132,429,1430,4862,\dots”,它是一个无穷数列,数与数之间看起来似乎也没什么太大联系……其实它和斐波那契数列有类似之处,它们不具有特定的数学意义(只是斐波那契的递推方法简单得多罢了),只是一个十分普遍的数学规律。所以学习时应该挂靠于例子本身而不是一味依赖于定义所写,那我们就开始吧:

用最经典的例子写出来就是:

给你一个u×vu\times v的网格,你将从原点(0,0)(0,0)开始移动。对于每次移动,你只能向上/向右一格(yy坐标/xx坐标加一),但是需要保证你总向右走的次数不少于向上走的次数,问从原点到A(n,n)A(n,n)有多少种不同的合法路径?

假设你某时刻走到了点M(s,t)M(s,t),根据题目要求,意味着需要保证sts\geq t。我们拟合一条经过点MM的正比例函数,不难看出它的斜率k1k\leq1。对于这个u×vu\times v的网格,所有的点都在整数刻度上。我们接着画出直线y=x+1y=x+1的图像,然后尽可能画几条不合法的路径出来比对一下,你会发现:不合法的路径与直线y=x+1y=x+1至少有一个交点,合法路径一定与y=x+1y=x+1没有交点。用一张图来直观体会一下:

终点A(5,5)A(5,5),其中红线为不合法路径,蓝线为合法路径。不难发现,不合法的路径与绿线(y=x+1y=x+1)都有至少一个交点,因为它们在某次移动后的端点与原点拟合而成的正比例函数的斜率k>1k>1,因此不是合法路径。

那么如何来计算合法和不合法路径的条数呢?直接求出合法路径不好求,规律不好找,因此我们计算出总路径数量,减去不合法数量即是合法路径数量。

可以看到,无论选择什么样的路径,在不左移、不下移的前提下,到达A(n,n)A(n,n),你都只能移动2n2n次(小学内容,把横线和竖线平移到一块数格子),其中右移nn次、上移nn次。转化一下,就是在2n2n次移动中选出nn次进行右移操作,总数就是C2nnC_{2n}^n

因为所有路径,包括合法的和不合法的路径都最终抵达了A(5,5)A(5,5),难以将内鬼剔除出来。我们选择将不合法路径关于判定线y=x+1y=x+1对称过去,它们的新终点将是A(4,6)A^\prime(4,6),也就是A(n1,n+1)A^\prime(n-1,n+1)。根据上面的推导方法,这里就是在2n2n轮移动中挑出n1n-1次右移操作,于是不合法路径的数量就是:C2nn1C_{2n}^{n-1},合法路径数量是:f=C2nnC2nn1f=C_{2n}^{n}-C_{2n}^{n-1}

(至于为什么用右移次数而不是上移次数,是因为上移受到限制,这意味着你可以一直右移到x=5x=5而无需担心条件限制;但是你就不能先一直上移到y=5y=5,因为这不符合题目要求)

扩展:如果题干中指明向右走的次数不少于向上走的次数±t\pm t,则只需将判定线上下平移为y=x+1±ty=x+1\pm t即可。

那这些又和宇宙第一IDE有什么关系呢

应用场景一:括号匹配

将向右走转化为左括号“(”,向上走转化为右括号“)”。对于每一次输入,检查一下左括号输入次数是否永不小于右括号输入次数。若是,当输入最后一个右括号,使左右括号数量相同时,即为匹配成功;若不是,且左括号个数大于右括号个数,则表明括号等待补全;若不是,且左括号个数小于右括号个数,即立即宣布失配。

应用场景二:合法进出栈序列计数问题

假设一个初始为空的栈,有2n2n次操作,nn次进栈,nn次出栈,请问合法进出栈序列总数(空栈不出)是多少?

答案就是Catalan数,自行套公式计算。

应用场景三:圆的不相交弦计数问题

假设一个圆周上分布着偶数个点,对这些点两两连线,使相连的线不相交的所有方案数。其中一个合法解如下图:

聪明如你,答案还是Catalan数!那么如何转化为已知问题求解呢?

我们将出发点标记为左括号“(”,从出发点引出去的线与其他线/点的所有交点标记为右括号“)”。当所有点两两连接完毕时,根据场景一的模型,一旦左右括号失配即代表不合法,否则合法。因此这个问题也就变成了:给定2n2n个左括号和右括号,求出使左右括号匹配的排列个数。在这里,如果问题无解,将会是这样:


例题:

  1. P3807 [模板] 卢卡斯定理/Lucas 定理
  2. P5014 水の三角(修改版) (Catalan数公式变形推导)

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
信竞初等数论导论
https://justpureh2o.cn/articles/40325/
作者
JustPureH2O
发布于
2023-11-26
许可协议
CC BY-NC-SA 4.0
最后更新于 2024-02-18,距今已过 736 天

部分内容可能已过时

评论区

Profile Image of the Author
JustPureH2O
穷方圆平直之情,尽规矩准绳之用
公告
JustPureH2O 的博客现已正式迁移至 Astro!原 Hexo 网站将移至 https://hexo.justpureh2o.cn/
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
100
分类
12
标签
55
总字数
372,306
运行时长
0
最后活动
0 天前

目录