【复习笔记】数学知识(1)

开大坑了,没准退役都补不完。
以及本文全是 LATEX 请耐心加载。

线性代数

矩阵

其实好多内容自己也搞不懂,不过是写给自己看的,所以就简略搞一下。

矩阵的引入来自于线性方程组。与向量类似,矩阵体现了一种对数据“打包处理”的思想。

矩阵乘法

矩阵相乘只有在第一个矩阵的列数和第二个矩阵的行数相同时才有意义。

AP×M 的矩阵,BM×Q 的矩阵,设矩阵 CAB 的乘积(CP×Q 的矩阵),则矩阵 C 的第 i 行第 j 列的元素有:

Ci,j=k=1MAi,k×Bk,j

文字描述一下,就是结果 C 矩阵的第 i 行第 j 列的数,是由矩阵 AiM 个数与矩阵 BjM 个数分别相乘再相加得到的,左行右列

需要注意的是,矩阵乘法满足结合律不满足一般的交换律

Code

struct Matrix{
int hig/*行数*/, wid/*列数*/;
int num[SIZE][SIZE];
Matrix(){
hig = wid = 0;
}
Matrix operator * (const Matrix &b) const{
Matrix c;
c.hig = hig, c.wid = b.wid;
for(register int i = 1; i <= c.hig; i++)
for(register int j = 1; j <= c.wid; j++)
for(register int k = 1; k <= wid; k++)
c.num[i][j] += num[i][k] * b.num[k][j];
return c;
}
};

其实还有卡常的写法。以及规模比较小的矩阵直接手动展开就行了。

struct Matrix{
int hig/*行数*/, wid/*列数*/;
int num[SIZE][SIZE];
Matrix(){
hig = wid = 0;
}
Matrix operator * (const Matrix &b) const{
Matrix c;
c.hig = hig, c.wid = b.wid;
for(register int i = 1; i <= c.hig; i++){
for(register int k = 1; k <= wid; k++){
int res = num[i][k];
for(register int j = 1; j <= c.wid; j++)
c.num[i][j] += res * b.num[k][j];
}
}
return c;
}
};

矩阵快速幂

重载了运算符之后,和普通快速幂的写法一样。
同样需要注意的是,答案矩阵要先初始化成单位矩阵。

Code

void init(){
base.hig = base.wid = n;
for(register int i = 1; i <= n; i++)
base.num[i][i] = 1;
}
Matrix Fast_Pow(Matrix a, int w){
Matrix ans = base;
while(w){
if(w & 1) ans = ans * a;
a = a * a;
w >>= 1;
}
return ans;
}

应用

由于线性递推式可以表示成矩阵乘法的形式,常用矩阵快速幂来求线性递推数列的某一项,以及优化DP和图的连通性及方案数等问题。

P2579 [ZJOI2005]沼泽鳄鱼

这道题是一道典型的通过矩阵乘来维护图的连通性及方案数的题。对于一个邻接矩阵 G,如果是 G1,就是原矩阵,Gi,j1 就可以表示一步从 ij 的方案数。
发现 Gi,j2=k=1nGi,k×Gk,j,发现和 Floyd 类似,所以它满足传递闭包的性质,即枚举一个中间点 kij 走两步的方案数等于走一步从 ik 再走一步从 kj 的方案数之和,所以依次类推:

Gi,jt 表示在邻接矩阵 G 上从 it 步到 j 的方案数。

回到这道题,考虑怎么对每个食人鱼进行处理,可以发现 234 的最小公倍数是 12,所以我们可以以 12 个时刻为一组进行处理,直接开 12 个邻接矩阵就行。再定义一个矩阵 Ans,先处理出走 12 步之后的矩阵,即这 12 个邻接矩阵之积。之后以 12 为单位,计算 Ansk/12,剩下的时间单独进行乘法操作就行了。

P4159 [SCOI2009] 迷路

如上边所说,Gi,jt 代表从 it 步到 j 的方案数,然而它的前提是边权为 1。回到这道题,由于有边权,所以我们无法直接用上边的方法转移。

考虑把原图中每一个节点看成一个点集,由于题目中的边权不会超过 9,所以每个点最多拆成 9 个点。一条边 (u,v),权值为 w,以“点集”u 中的第 w 个点指向“点集”v 中的第 1 个点,权值为 1
同时,“点集”u 中的第 i 个点都向第 i+1 个点连一条权值为 1 的单向边

高斯消元

高斯消元是求解线性方程组的经典算法,它在当代数学中有着重要的地位和价值,是线性代数课程教学的重要组成部分。

高斯消元法

消元法是将方程组中的一方程的未知数用含有另一未知数的代数式表示,并将其带入到另一方程中,消去一未知数,得到一解;或将方程组中的一方程倍乘某个常数加到另外一方程中去,也可达到消去一未知数的目的。

高斯消元法一般有五个步骤:

  1. 增广矩阵行初等形变换为行最简形;
  2. 还原线性方程组;
  3. 求解第一个变量;
  4. 补充自由未知量;
  5. 列表示方程组通解。

比如以下方程组:

{2x1+5x3+6x4=9x3+x4=42x3+2x4=8

把它转化成增广矩阵:(增广矩阵是指方程系数矩阵 A 与常数列 b 的并生成的新矩阵,即 (A|b))。

(205600110022|948)

化为行阶梯形。

r32r2(205600110000|940)

化为最简形。

r12(102.5300110000|4.540)

r1r2×2.5(1000.500110000|14.540)

之后还原线性方程组,就是在行最简形的基础上,将增广矩阵重新写为线性方程组的形式。

{x1+0.5x4=14.5x3+x4=4

然后求解第一个变量,在还原的线性方程组中,将方程组中每个方程的第一个变量用其它量表示出来。

{x1=0.5x4+14.5x3=x44

再之后补充自由未知量,因为上方程组中,变量 x2x4 不受方程组的约束,是自由未知量,可以取任意值。同时,自由未知量不受约束,只能自己等于自己。

{x1=0.5x4+14.5x2=x2x3=x44x4=x4

最后,就要去列表示方程组的通解。

(x1x2x3x4)=(0100)x2+(0.5011)x4+(14.5040)=(0100)C1+(0.5011)C2+(14.5040)

其中 C1C2 为任意常数。

MD这段 LATEX 打了二十多行,恶心死我了。

理解了这些,再去考虑代码怎么写。

在写代码时,可以将上述的五步概括成对增广矩阵的三类操作:

  1. 用一个非零的数乘某一行;
  2. 把其中一行的若干倍加到另一行上;
  3. 交换两行的位置。

其实这三类操作就是矩阵的“初等行列变换”。

再举例有方程组(MD又要打苦难的 LATEX)如下:

{x1+2x2x3=62x1+x23x3=9x1x2+2x3=7(121213112|697)

对其进行若干次初等行变换后得到“阶梯形矩阵”。(真要详细打了二百行该出去了
其系数矩阵部分称为“上三角矩阵”。它表达的信息是:

(121011001|613){x1+2x2x3=6x2+x3=1x3=3

因此,我们已经知道了最后一个未知量的值,从下往上依次回带方程组,即可得到每一个未知量的值。实际上,上方的上三角矩阵仍可化简。

(121011001|613)(120010001|323)(100010001|123)

最后得到的矩阵叫做“简化阶梯形矩阵”,它的系数矩阵部分称为“对角矩阵”。该矩阵已经直接给出了方程组的解。

简化的说,高斯消元的思想就是对于每一个未知量 xi,找到一个 xi 的系数不为零,但 x1xi1 系数都为零的方程,然后用初等行列变换把其他方程的 xi 的系数都全部消成零。

需要注意的是,消元不一定每次都是能得到所有解的理想状况。在消元完成后,若存在系数全为零、常数不为零的行,方程组无解;若系数不全为零的行恰有 n 个,则说明主元有 n 个,方程组有解;若系数不全为零的行有 k<n 个,说明主元有 k 个,自由元有 nk 个,方程组有无穷多解。

Code

int Gauss(){
int len = 1; //枚举行数
for(register int i = 1; i <= n; i++){
int r = len; //寻找 x[i] 的系数不为 0 的第一个方程
for(register int j = i + 1; j <= n; j++)
if(fabs(a[j][i]) > fabs(a[r][i])) r = j;
if(r != len)
for(register int j = 1; j <= n + 1; j++)
swap(a[r][j], a[len][j]);
if(fabs(a[len][i]) < EPS) continue; //当前未知数是自由元,或者系数为零,常数项不为零,没有消的必要
for(register int j = len + 1; j <= n; j++){ //消去其他方程中的 x[i]
double f = a[j][i] / a[len][i];
for(register int k = i; k <= n + 1; k++) //常数项也得消
a[j][k] -= a[len][k] * f;
}
++len; //该下一行了
}
if(len <= n){ //判无解或者多解的情况
for(register int i = len; i <= n; i++)
if(fabs(a[i][n + 1]) > EPS && fabs(a[i][i]) < EPS)
return puts("-1"), -1; //无解
return puts("0"), 0; //多解
}
for(register int i = n; i >= 1; i--){ //从下往上回带
for(register int j = i + 1; j <= n; j++)
a[i][n + 1] -= a[i][j] * a[j][n + 1];
a[i][n + 1] /= a[i][i]; //最后的常数项就是未知数的值
}
return 1; //有唯一解
}

解异或方程组

联赛应该不考。

image

应用

P3389 【模板】高斯消元法

板子题,直接粘上边代码就能A

P2455 [SDOI2006]线性方程组

板子题,直接粘上边代码就能A

P4035 [JSOI2008]球形空间产生器

一个球体上所有点到球心的距离相同,只需要找到一个点 (x1,x2,...,xn) 使得:

j=0n(ai,jxj)2=C

其中 C 为常数,i[1,n+1],球面上第 i 个点的坐标为 (ai,1,ai,2,...,ai,n)。但这是 n+1n 元二次方程,不是线性方程组,但可通过相邻的两个方程作差,把它变成 nn 元一次方程,同时消去常数 C。可得:

j=1n(ai,j2ai+1,j22xj(ai,jai+1,j))=0i[1,n]

然后把常数项移到右边,未知数移到左边,有:

j=1n2(ai,jai+1,j)xj=j=1n(ai,j2ai+1,j2)i[1,n]

可写出增广矩阵:

(2(a1,1a2,1)2(a1,2a2,2)2(a1,na2,n)2(a2,1a3,1)2(a2,2a3,2)2(a2,na3,n)2(an,1an+1,1)2(an,2an+1,2)2(an,nan+1,n)|j=1n(a1,j2a2,j2)j=1n(a2,j2a3,j2)j=1n(an,j2an+1,j2))

然后高斯消元求解就行了。

P2973 [USACO10HOL]Driving Out the Piggies G

没有限制次数,没有办法递推求概率,所以我们可以设在节点 i 爆炸的概率为 xi,节点 i 的度为 degi。考虑炸弹会从哪里来:

  • 1 号节点:炸弹可能有 pq 的概率直接爆炸,或者会由相邻的点转移过来,有:

x1=pq+(i,j)Exj×(1pq)×1degj

  • 在其他节点,炸弹只可能从相邻的节点转移过来,有:

xi=(i,j)Exj×(1pq)×1degj

进行移项,有:

x1+(1,j)Exj×(1pq)×1degj=pq

xi+(i,j)Exj×(1pq)×1degj=0

即可写出增广矩阵,进行高斯消元。

P3232 [HNOI2013]游走

和上题一样,只不过我们要先推出每个点经过的期望次数,再通过点的期望次数来推边被经过的期望次数。
有一个显然的贪心,为使得总分的期望值最小,期望次数大的边要给予它一个小的编号。

先去考虑怎么推点的期望的经过次数,设节点 i 经过的期望次数为 xi,需要注意的是任意一个点都不能通过 n 号节点来转移,因为走到 n 号节点就结束了:

  • 最开始在 1 号节点,之后再到达 1 号节点就需要通过别的点了,所以有:

x1=1+(1,j)E,jnxj×1degj

  • 其他的节点只能通过别的节点来转移,所以是:

xi=(i,j)E,jnxj×1degj

然后我们去考虑怎么把经过点的期望转化为经过边的期望,设 gi 为第 i 条边期望经过次数。经过一个点,那一定会到另一个与之相连的点(n 号节点除外),也就是每个节点都会通过一条边出去。至于为什么不能考虑每个节点都会从一条边进去,因为最开始在 1 号节点,初始的位置不会经过边,不好处理,所以有:

gi=xudegu+xvdegvEi=(u,v),un,vn

求出边的期望之后贪心给期望值大的边赋小编号就行了。

数论基础

素数

这没得写了,小学生都会,写俩筛法得了。

埃式筛(埃拉托斯特尼筛法)

思想很简单,把 n 的所有的素数的倍数都剔除就行了。时间复杂度接近线性,为 O(nloglogn)

Code

void Eratosthenes(){
sum = 0;
vis[0] = vis[1] = true; //true 不是素数,false 是素数
for(register int i = 2; i <= n; i++){
if(!vis[i]){
prime[++sum] = i;
for(register int j = 2; i * j <= n; j++)
vis[i * j] = true;
}
}
}

线性筛(欧拉筛法)

埃式筛的时间复杂度不是严格的线性,原因是一个合数不一定只被筛一次,比如 15 会被 3 筛一次,又会被 5 筛一次。

线性筛通过从小到大积累质因子的方法标记每个合数,时间复杂度是 O(n)

void Euler(){
sum = 0;
for(register int i = 2; i <= n; i++){
if(!v[i]){ //没有最小质因子,表明它是素数
v[i] = i;
prime[++sum] = i;
}
for(register int j = 1; j <= sum; j++){//给当前数乘上一个质因子
if(prime[j] > v[i] || i * prime[j] > n) break;
//i 有比 prime[j] 更小的质因子或者超出范围,退出
v[i * prime[j]] = prime[j];
}
}
}

算数基本定理(唯一分解定理)

内容:

任何一个大于 1 的正整数都能唯一分解为有限个质数的乘积,写作:

n=p1c1p2c2...pmcm

其中 ci 都是正整数,pi 都是质数,且满足 p1<p2<...<pm

试除法:

扫描 1n 的每个数 d,若 d 能整除 n,则从 n 中除掉所有的因子 d,同时累计除去 d 的个数。

因为一个合数的因子一定在扫描到这个合数之前就被剔除掉了,所以上述过程能整除 n 的数一定是质数。时间复杂度 O(n)

Code:

void Divide(int n){
int sum = 0, sq = sqrt(n);
for(register int i = 2; i <= sq; i++){
if(n % i == 0){ //能整除 n,且是质数
p[++sum] = i, c[sum] = 0;
while(n % i == 0) n /= i, ++c[sum]; //把 i 全部除去,并记录个数
}
}
if(n > 1) p[++sum] = n, c[sum] = 1; //没有除净,表明 n 是质数
}

应用

素数部分就是数论基础中的基础,没人会单考个素数,大部分会和后边的数论知识结合。

约数

定义

若整数 n 除以 d 的余数为 0,即 d 能整除 n,称 dn 的约数,nd 的倍数,记为 d|n

算数基本定理的推论

由算数基本定理,正整数 n 可以表示成 p1c1p2c2pmcm,其中 ci 都是正整数,pi 都是质数,且满足 p1<p2<<pm,则 n 的正约数集可写作:

{p1b1p2b2pmbm},0bici

n 的正约数个数为:

(c1+1)×(c2+1)××(cm+1)=i=1m(ci+1)

n 的所有的正约数的和为:

(1+p1+p12+p13++p1c1)××(1+pm+pm1+pm2++pmcm)=i=1m(j=1ci( pi)j)

最大公约数

定理:

a,bNgcd(a,b)×lcm(a,b)=a×b

辗转相减法

a,bN,gcd(a,b)=gcd(b,ab)=gcd(a,ab)

a,bN,gcd(2a,2b)=gcd(a,b)

同时,可以拓展到:

a,bN,gcd(a+b,b)=gcd(b,a)

a,bN,gcd(xa,xb)=xgcd(a,b)

欧几里得算法(辗转相除法)

a,bN,gcd(a,b)=gcd(b,amodb)

欧几里得算法是最常用的求最大公约数的算法,时间复杂度为 O(log(a+b))。不过在进行高精度运算时,取模较难实现,可以考虑用辗转相减法代替欧几里得算法。

Code

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

MD这篇博写到这开始恶心了。

欧拉函数

对于 a,bN,若 gcd(a,b)=1,则称 a,b 互质。

1n 中与 n 互质的数的个数称为欧拉函数,记为 φ(n)
在算数基本定理中,n=p1c1p2c2pmcm,则有:

φ(n)=n×p11p1×p21p2×pm1pm=n×p|n(11p)

考虑这个式子怎么来的,设 pn 的质因子,1np 的倍数有 p2pnp×p,共有 np 个。同理,设 q 也为 n 的质因子,则 1nq 的倍数也有 nq 个。如果把这 np+nq 个数全部减去,那么 p×q 的倍数就被减去了两次,需要加回来一次。因此,1n 中不与 n 含有共同因子 p 或者 q 的数的个数为:

nnpnq+npq=n×(11p1q+1pq)=n(11p)(11q)

根据欧拉函数的计算式,我们只要分解质因数,就能求出欧拉函数,时间复杂度 O(n)

Code

int Phi(int n){
int ans = n;
int sq = sqrt(n);
for(register int i = 2; i <= sq; i++){
if(n % i == 0){
ans = ans * (i - 1) / i;
while(n % i == 0) n /= i;
}
}
if(n > 1) ans = ans * (n - 1) / n;
return ans;
}

欧拉函数的其他性质

  1. n>11n 中与 n 互质的数的和为 n×φ(n)÷2
  2. ab 互质,则 φ(ab)=φ(a)φ(b)
  3. p 为质数,若 pnp2n,则有 φ(n)=φ(n÷p)×p
  4. p 为质数,若 pnp2n,则 φ(n)=φ(n÷p)×(p1)
  5. dnφ(d)=n

其实这块就延展出了奇性函数了,但我不会以及联赛不考。

根据这些性质,我们就可以用筛法快速求出每个数的欧拉函数。利用埃式筛可以按照欧拉函数的计算式,在 O(nlogn) 的时间复杂度内求出 2n 的欧拉函数。

Code

void Eratosthenes(int n){
for(register int i = 2; i <= n; i++)
phi[i] = i;
for(register int i = 2; i <= n; i++)
if(phi[i] == i)
for(register int j = i; j <= n; j+= i)
phi[j] = phi[j] * (i - 1) / i;
}

由上边的性质 34

  • p 为质数,若 p|np2|n,则有 φ(n)=φ(n÷p)×p
  • p 为质数,若 p|np2n,则 φ(n)=φ(n÷p)×(p1)

在线性筛中,每个合数 n 只会被它的最小质因子筛一次,可以在此时执行上边两条判断,从 φ(n÷p) 递推到 φ(n)

Code

void Euler(int n){
sum = 0; //质数的数量
phi[1] =1;
for(register int i = 2; i <= n; i++){
if(!v[i]){ //没有最小质因子,自己就是质数
v[i] = i;
phi[i] = i - 1; //质数和除了自己以外的数都互质
prime[++sum] = i;
}
for(register int j = 1; j <= sum; j++){
if(prime[j] > v[i] || i * prime[j] > n) break;
v[i * prime[j]] = prime[j];
if(i % prime[j] == 0) phi[i * prime[j]] = phi[i] * prime[j];
//prime[j] * prime[j] | i * prime[j],对应性质3
else phi[i * prime[j]] = phi[i] * (prime[j] - 1);
//反之,对应性质4
}
}
}

应用

P2158 [SDOI2008] 仪仗队

设左下角的坐标为 (0,0),除了 (1,0)(1,1)(0,1) 三个位置外,一个位置 (x,y) 能被观察到,当且仅当 1x,yn,xy,gcd(x,y)=1 时能被观察到。同时可以得到,能被观察到的位置是关于过 (0,0)(n,n) 的直线对称的,所以只需要考虑半的情况,即满足 1x<yn。也就是对于每个 2yn,需要统计有多少个 1x<y 使得 gcd(x,y)=1,也就是 φ(y)

最后的答案就是 3+2×i=2nφ(i),直接筛法跑一遍就行了。

P2398 GCD SUM

由上方的性质 5dnφ(d)=n 可以把式子转化成:

i=1nj=1ndgcd(i,j)φ(d)

等价于:

i=1nj=1ndi,djφ(d)

也就是求:

i=1nj=1nd=1nφ(d)[didj]

我们换一下枚举顺序,得:

d=1nφ(d)i=1nj=1n[di][dj]

拿出 i=1nj=1n[di][dj],就是求在 1n 中有 d 能整除的数的个数的平方,转化一下就是 nd2

所以最终的式子就是:

d=1nφ(d)nd2

CF1717E Madoka and The Best University

某个神必推这道机房大佬一眼切的题推了半下午,西八。

三个未知量不好枚举,考虑去转化式子:

lcm(c,gcd(a,b))=c×gcd(a,b)gcd(c,gcd(a,b))=c×gcd(a,b)gcd(a,b,c)

由于 c 比较特殊,考虑将 c 替换成 nab,原式变成:

(nab)×gcd(a,b)gcd(a,b,nab)

然后去化下边的 gcd(a,b,nab)

gcd(a,b,nab)=gcd(gcd(a,b),nab)

由辗转相减法,gcd(a,b)=gcd(a+b,b),可化为:

gcd(a+b,b,nab)=gcd(b,gcd(a+b,n(a+b)))

其中 gcd(a+b,n(a+b))=gcd(n,a+b),再带入展开:

gcd(a+b,b,n)=gcd(gcd(a+b,b),n)=gcd(a,b,n)

所以原式化为了:

(nab)×gcd(a,b)gcd(a,b,n)

s=a+bd=gcd(a,b),需要求的就是

(ns)×dgcd(d,n)

因为 ds,所以我们从 1n 枚举 d,枚举 d 的倍数作为 s,这时我们只需要知道满足 a+b=s,gcd(a,b)=da,b 的方案数即可。

a=ad,b=bd,s=sd,因为 gcd(a,b)=d,所以 gcd(a,b)=1。所以,方案数为

a=1s[gcd(a,b)=1]=a=1s[gcd(a,sa)=1]=a=1s[gcd(a,s)=1]=φ(s)=φ(sd)

所以,当枚举每一个 ds 时,它们的贡献是:

(ns)×dgcd(d,n)×φ(sd)

直接线性筛求欧拉函数就行了。

P2568 GCD

最朴素的想法,筛出每个素数,对于每个素数枚举 x,y 判断 gcd(x,y) 等不等于这个素数,可写出:

pprimei=1nj=1n[gcd(i,j)=p]=pprimei=1nj=1n[gcd(ip,jp)=1]

可以发现有用的 ij 全部 1i,jnp,所以枚举到 np 就行了,即:

pprimei=1npj=1np[gcd(i,j)=1]

gcd(x,y)=1 时,gcd(y,x) 也肯定等于 1,所以只需要从 1i 枚举 j ,最后再乘 2 即可。同时,这样会算两次 gcd(1,1),减去 1 即可,写成式子为:

pprime(i=1np2×j=1i[gcd(i,j)=1])1

等价于:

pprime(i=1np2×φ(i))1

然后我们线性筛出欧拉函数后维护一个前缀和优化掉枚举即可。

P1390 公约数的和

和上边那道题一样,只不过反了一下。

i=1nj=i+1ngcd(i,j)=d=1ndi=1nj=i+1n[gcd(i,j)=d]=d=1ndi=1ndj=i+1nd[gcd(i,j)=1]

考虑到 gcd(x,y)=gcd(y,x),所以 gcd(i,j)=1 具有“对称性”,也就是:

i=1ndj=i+1nd[gcd(i,j)=1]=(i=1ndj=1i[gcd(i,j)=1])1

1 是因为从 1i 枚举 j 时,i=j=1 时会产生一次贡献,但从 i+1n 枚举 j 时,则不会产生这个贡献,故减去。最终的式子是:

d=1nd((i=1ndφ(i))1)

同样,线性筛筛出欧拉函数之后前缀和优化即可。

posted @   TSTYFST  阅读(97)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示