同步于 博客园 和 洛谷博客。
由于各网站之间 LATEX 及 Markdown 渲染方式的差异,故不保证除博客园外的排版完好,对于排版有明显不正确的地方请以博客园为主。
质数 约数 同余 组合
见初等数论及组合数学入门复习概览。
矩阵乘法
前置知识:
- 矩阵:在 OI 中,一个 n×m 的矩阵可视为一个 n×m 的二维数组,其数学本质为一个向量,对于一个 n×m 的矩阵 A,表示为:
A=a1,1a2,1⋮an,1a1,2a2,2⋮an,2⋯⋯⋱⋯a1,ma2,m⋮an,m
矩阵加法 矩阵减法:设 A,B,C 均为 n×m 的矩阵,则有
C=A±B⇔∀i∈[1,n],∀j∈[1,m],Ci,j=Ai,j±Bi,j
矩阵数乘:设 A,B 均为 n×m 的矩阵,k∈R,则有
B=k⋅A⇔∀i∈[1,n]∀j∈[1,m]Bi,j=k×Ai,j
矩阵乘法:由向量的数量积演变而来,若 A 为 n×m 的矩阵且 B 为 m×p 的矩阵,则对于 C=A×B,C 为 n×p 的矩阵,且:
∀i∈[1,n],∀j∈[1,p],Ci,j=k=1∑mAi,k×Bk,j

一个模拟矩阵乘法的网站。
注意:若对于 n×m 的矩阵 A 和 p×q 的矩阵 B 有 m=p,则 A 和 B 无法进行矩阵乘法。
单位矩阵:一个 n×m 的矩阵 I,且 ∀i∈[1,min(n,m)],Ii,i=1,形如:
In=10⋮001⋮0⋯⋯⋱⋯00⋮1
对于任意一个 n×m 的矩阵 A,均存在 A×I=A(在 A 的大小范围内相等)。
运算性质
-
结合律:(A×B)×C=A×(B×C)。
-
不存在交换律。
矩阵乘法应用
加速线性递推
满足优化所需条件:
-
在每个单位时间内每个阶段中只发生一次变化。(即递推)
-
变化的形式是一个递推式。
-
递推式在每个阶段都可以作用于不同的数据上,但本身保持不变。
-
未知的阶段数很大,但递推式长度很小。
例题:Fibonacci 数列(斐波那契数列)
Fn={1 (n⩽2)Fn−1+Fn−2 (n⩾3)
求 Fnmod109+7,其中 1⩽n<263。
题解:设转移矩阵为 base,通项矩阵为:
[FiFi−1]
对于每一项,有如下式:
Fi=Fi−1×1+Fi−2×1
Fi−1=Fi−1×1+Fi−2×0
由系数得:
base=[1110]
则有:
[FiFi−1]=base×[Fi−1Fi−2]=[1110]×[Fi−1Fi−2]=[Fi−1×1+Fi−2×1Fi−1×1+Fi−2×0]=[FiFi−1]
故对于第 n 项 Fibonacci 数列,只需求 basen−1(在 base0=I 时可用此)或 [11]×basen−2,最终矩阵 Ans1,1 即为答案。
时间复杂度 O(23logn)
扩展 Fibonacci:
对于形似
Fi={p1,p2,…,1⩽i⩽mFi−1+Fi−m,i⩾m+1
的递推式,都可以通过矩阵乘法加速线性递推在 O(m3logn) 的时间复杂度内求出 Fn 的值。
广义矩阵乘法
若 A 为 n×m 的矩阵,B 为 m×p 的矩阵,则对于 C=A×B,C 为 n×p 的矩阵,有:
∀i∈[1,n],∀j∈[1,p],Ci,j=k=1⨁m(Ai,k⊗Bk,j)
且下列运算律成立:
交换律:a⊗b=b⊗a
结合律:(a⊗b)⊗c=a⊗(b⊗c)
分配律:a⊗(b⊕c)=(a⊗b)⊕(a⊗c)
则称其为广义矩阵乘法。
其中 ⊕ 常为 min 或 max,⊗ 为 +。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| struct Matrix{ int n,m; int a[N][N]; inline Matrix(int x,int y){memset(a,0,sizeof(a));n=x,m=y;} inline void setI(){ int k=min(n,m); memset(a,0,sizeof(a)); F(i,1,k) a[i][i]=1; return ; } inline void print(){ F(i,1,n){ F(j,1,m) printf("%lld ",a[i][j]); puts(""); } return ; } inline Matrix operator+(const Matrix& x) const{ Matrix ret(n,m); F(i,1,n) F(j,1,m) ret.a[i][j]=a[i][j]+x.a[i][j],ret.a[i][j]%=p; return ret; } inline Matrix operator-(const Matrix& x) const{ Matrix ret(n,m); F(i,1,n) F(j,1,m) ret.a[i][j]=a[i][j]-x.a[i][j]; return ret; } inline Matrix operator&(const int& k) const{ Matrix ret(n,m); F(i,1,n) F(j,1,m) ret.a[i][j]=a[i][j]*k,ret.a[i][j]%=p; return ret; } inline Matrix operator*(const Matrix& x) const { Matrix ret(n,x.m); F(i,1,n) F(j,1,x.m) F(k,1,m) ret.a[i][j]+=a[i][k]*x.a[k][j],ret.a[i][j]%=p; return ret; } inline Matrix operator^(const int &x) const { int b=x; Matrix a=*this,I(a.n,a.m); I.setI(); while(b){ if (b&1) I=I*a; a=a*a,b>>=1ll; } return I; } };
|
0/1 分数规划
定义:类似于给定 2 个含 n 个整数的序列 a,b,求一组解 xi (∀i∈[1,n],xi∈[0,1])(即选或不选)使得下式最大化,给出其最大值:
∑i=1nbi×xi∑i=1nai×xi
求解
二分
二分答案 mid:
∑i=1nbi×xi∑i=1nai×xi>mid
i=1∑nai×xi−mid×i=1∑nbi×xi>0
i=1∑nxi×(ai−mid×bi)>0
二分左式最大值即可,时间复杂度 O(nlog2n)。
Dinkelbach 算法
取一个值 x 并将其多次迭代使其逼近于答案。
此方法不常用,时间复杂度玄学。
高斯消元
定义:通过初等行变换把增广矩阵变为简化阶梯形矩阵的线性方程组求解算法。
增广矩阵:在线性代数中系数矩阵右边添上常数列所得到的矩阵。
线性方程组转化为增广矩阵:对于一个 n 元一次方程组:
⎩⎨⎧a1,1x1+a1,2x2+⋯+a1,nxn=p1a2,1x1+a2,2x2+⋯+a2,nxn=p2⋮an,1x1+an,2x2+⋯+an,nxn=pn
可转化为一个 n 行 n+1 列的增广矩阵:
a1,1a2,1⋮an,1a1,2a2,2⋮an,2……⋮…a1,na2,n⋮an,np1p2⋮pn
初等行变换:平时解方程组的方法,在增广矩阵上可概括为以下三种操作:
-
用一个非零的数乘某一行。
-
把其中一行的若干倍加到另一行上。
-
交换两行的位置。
阶梯形矩阵:形如
a1,10⋮0a1,2a2,2⋮0……⋮…a1,na2,n⋮an,np1p2⋮pn
的矩阵,其系数矩阵部分被称为上三角矩阵。
简化阶梯矩阵:形如
a1,10⋮00a2,2⋮0……⋮…00⋮an,np1p2⋮pn
的矩阵,由阶梯形矩阵简化而来,该矩阵直接给出了方程的解。
自由元:若 ∃i∈[1,n],xi 可以取任意值,即方程组有无数组解,则称 xi 为自由元。
主元:不为自由元的元即为主元。
代码核心逻辑
-
对于每一个 xi,找出最大的 ak,i,(k∈[1,n]) 且 ak,i=0 所属的 k,将该行移动到第 i 行(若找不出 k 则说明方程组无解或存在自由元)。
-
使当前行的首项变为 1,当前行其它项除原首项系数(即将当前行约分至首项系数为 1)。
-
将当前行下面所有行的当前列都消成 0,同时更新正在处理的行后面的系数。
-
若找不出 k,处理完剩余行后遍历所有行,若 ∃i∈[1,n],ai,n+1=0,则说明方程组无解(未消元的行系数必为 0),反之则存在自由元,方程有无数组解。
-
反之,则将阶梯形矩阵转化为简化阶梯矩阵,ai,n+1 即为 xi 的对应解。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| const double eps=1e-6; int n; double a[N][N]; inline int Gauss(){ int i=1; for (int j=1;j<=n;++j){ int maxh=i; F(x,i,n) if (abs(a[x][j])>abs(a[maxh][j])) maxh=x; if (abs(a[maxh][j])<eps) continue; F(x,j,n+1) swap(a[maxh][x],a[i][x]); FJ(x,n+1,j) a[i][x]/=a[i][j]; F(x,i+1,n) if (abs(a[x][j])>eps) FJ(y,n+1,j) a[x][y]-=a[i][y]*a[x][j]; ++i; } if (i<=n){ F(x,i,n) if (abs(a[x][n+1])>eps) return -1; return -2; } FJ(x,n,1) F(y,x+1,n) a[x][n+1]-=a[x][y]*a[y][n+1]; return 0; } signed main(){ n=read(); F(i,1,n) F(j,1,n+1) a[i][j]=read(); int ret=Gauss(); if (ret==-1) puts("No Solution"),exit(0); else if (ret==-2) puts("Multiple Solutions"),exit(0); F(i,1,n) printf("x_%lld=%.2f\n",i,abs(a[i][n+1])<eps?0:a[i][n+1]); }
|
高斯-约旦消元
相比高斯消元的优势:
-
更高的精度。
-
代码更简洁。
缺点:
- 判无数组解的时候比较麻烦。
代码核心逻辑
-
找出一个此前未被找过的未知数作为主元。
-
将当前主元移到当前行。
-
使当前行的首项变为 1,后面的项约分。
-
因为这样处理后 ai,i 即为该主元的系数,所以若 ∃i∈[1,n],ai,i=0,则说明要么无解或有无数组解。
-
如果无解或有无数组解,则遍历每个未知数,若存在常数项不为 0,则说明方程组无解,若不存在则说明方程组有无数组解。
-
若有唯一解,则 ai,iai,n+1 即为 xi 的对应解。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| inline int GaussJordan(){ int i=0; F(j,1,n){ ++i; int maxh=i; F(x,1,n) if(x>j||abs(a[x][x])<eps) if (abs(a[x][j])>abs(a[maxh][j])) maxh=x; F(x,1,n+1) swap(a[i][x],a[maxh][x]); if (abs(a[i][i])<eps) continue; F(x,1,n){ if (x!=i){ double cur=a[x][j]/a[i][j]; F(y,j+1,n+1) a[x][y]-=a[i][y]*cur; } } } bool flg=0; F(i,1,n) if (abs(a[i][i])<eps){ if (abs(a[i][n+1]/a[i][i])>eps) return -1; flg=1; } return flg?-2:0; } signed main(){ n=read(); F(i,1,n) F(j,1,n+1) a[i][j]=read(); int ret=GaussJordan(); if (ret==-1) puts("-1"),exit(0); else if (ret==-2) puts("0"),exit(0); F(i,1,n) printf("x_%lld=%.2f\n",i,abs(a[i][n+1]/a[i][i])<eps?0:a[i][n+1]/a[i][i]); return 0; }
|
求解异或方程组
对于一个 n 元一次异或方程组:
⎩⎨⎧a1,1x1xora1,2x2xor…xora1,nxn=p1a2,1x1xora2,2x2xor…xora2,nxn=p2⋮an,1x1xoran,2x2xor…xoran,nxn=pn
依旧构造增广矩阵,因为异或本质就为不进位加法,所以仅需要在原高斯消元代码的基础上稍加修改即可,详见代码。
需要注意的是因为自由元只能取 0 或 1,故出现自由元时解的数量并不是无数组,而是 2自由元个数。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| inline int XorGauss(){ int i=1,ret=1; F(j,1,n){ int maxh=i; F(x,i+1,n) if (a[x][j]) maxh=x; if (!a[maxh][j]) continue; F(x,j,n+1) swap(a[maxh][x],a[i][x]); F(x,i+1,n) FJ(y,n+1,j) a[x][y]^=a[x][j]&a[i][y]; ++i; } if (i<=n){ F(x,i,n){ if (a[x][n+1]) return -1; ret<<=1ll; } } return ret; }
|
线性空间
前置知识:
-
向量加法:a+b,其中 a 和 b 均为向量。
-
标量乘法(数乘):ka,其中 a 为向量,k 为标量。
表出:若向量 b 能够由向量 a1,a2,…,ak 经过向量加法和标量乘法得出,则称 b 能被向量 a1,a2,…,ak 表出。
线性空间:若干个向量 a1,a2,…,ak 能表出的所有向量所构成的集合。
生成子集:对于一个线性空间来说,表出其所有向量的若干个向量 a1,a2,…,ak 为此线性空间的生成子集。
线性相关:任意选出线性空间中的若干个向量,若其中存在一个向量能够被其它向量表出,则称这些向量线性相关,反之则称这些向量线性无关。
基底:对于一个线性空间来说,线性无关的生成子集被称为线性空间的基底,简称为基。
维数:对于一个线性空间来说,基所包含的向量个数被称为线性空间的维数。(一个线性空间所有基包含的向量个数都相等)
与矩阵的联系
对于一个 n×m 的矩阵,其每一行都可看作一个长度为 m 的向量,称其为行向量,n 个行向量所能表出的所有向量构成一个线性空间,其维数被称为矩阵的行秩。
类似地,有列向量和列秩,由于行秩一定等于列秩,故合称为秩。
将该矩阵变为系数列均为 0 的增广矩阵进行高斯消元,得到简化阶梯矩阵。该简化阶梯矩阵所有非零行向量都线性无关,其构成的生成子集为该线性空间的一个基,这个基的个数为该矩阵的秩。
异或空间
表出:若整数 b 能够由整数 a1,a2,…,ak 经过异或运算得出,则称 b 能被 a1,a2,…,ak 表出。
异或空间:若干个整数 a1,a2,…,ak 能表出的所有数字所构成的集合。
其它的定义和对于线性空间的定义类似,其中异或空间的基又被定义为异或空间的极大线性无关子集。
对于 n 个整数 ai 且 ai∈[0,2k−1],则可以构造一个 n 行 m 列的 01 矩阵,对于第 i 行,从右到左依次为 (ai)2 从低位到高位,不够的在左边补 0。
比如,对于 n=5,k=4,a=[5,12,2,7,9],得到的矩阵为:
01001110100011010011
不够的在右边补 0,然后通过异或高斯消元求出它的简化阶梯矩阵:
10000010000010011000
即该异或空间的基为 [9,5,2],其数学含义为:
∀i∈[1,n],∃S⊆基,S=∅,xorx∈S=ai
数据结构求解线性基
以异或线性基为例,用 Ai(i∈[1,⌈log2(max{ai})⌉]) 保存基底。
对于插入线性基操作 Insert(x),有:
-
设 x 最高位 1 在第 i 位。
-
若 Ai=0,则使 Ai=x。
-
反之,则使 x=xxorAi,然后继续 Insert(x),直到 x=0。
Ai 的非 0 项即为基底。
代码
1 2 3 4 5 6 7 8 9
| inline void Insert(int x){ FJ(i,51,0){ if (x>>i){ if (bs[i]) x^=bs[i]; else {bs[i]=x;return;} } } return ; }
|
或
1 2 3 4 5 6
| inline void Insert(int x){ if (!x) return ; int pos=__lg(x); if (bs[pos]) Insert(x^bs[pos]); else {bs[pos]=x;return;} }
|