理论部分
- 奇/偶排列
逆序对为奇数/偶数的排列
- 矩阵单位元E:
(i, i)为1,其余为0.单位元乘其他矩阵,都为其他矩阵本身。
- 线性无关(线性独立):
可以这么理解:
把一个n维向量理解成n元方程,比如(2, 1, 3)可以理解成2x + 1y + 3z。
两个向量线性无关就是把一个方程通过加法或乘法无法得到另一个方程。
n个向量线性无关就是把n个方程中的任意n-1个进行加法或乘法,无法得到剩下的那个向量。
n元一次方程组(共n组)有唯一解,只有当那n个方程线性无关(暂且这么说)
- 秩:
又分为行秩和列秩。
秩 = 最大线性无关组数量。
一个矩阵A的列秩是A的线性独立的纵列的极大数,通常表示为r(A)
- by ryc
- 行列式的性质:
-
转置后行列式不变
-
交换两行后行列式为相反数
-
一行乘某个系数再加到另一行,行列式不变。
-
行列式中如果有两行元素成比例,则此行列式等于零
-
行列式的某一行中所有的元素都乘同一数k,行列式乘k.
- 行列式的(不常见)应用
-
无向图生成树个数(度数矩阵-邻接矩阵 后删最后一行一列的行列式)
-
n个n维向量构成的平行2n面体的“超体积”(类似叉积)
9.10 Update
向量 & 线性组合 & 张成的空间 & 基
向量可以理解为箭头或列矩阵(如 (egin{bmatrix} 2 \ -5\ 8 end{bmatrix}))。
线性组合:向量加减
张成的空间:两向量通过组合能得到的所有向量的集合
集:该空间的线性无关向量集。
线性变换(矩阵)
实际上矩阵和线性变换是一个东西。
线性变换类似函数,输入一个向量,输出一个向量。
也可以理解为将一个空间(坐标系)映射到另一个空间中,这是从整个空间的角度来理解的。例如旋转,翻折等。但是必须是线性的(同时具有 (f(x+y)=f(x)+f(y),f(ax)=af(x)) 的性质)。
为了简化问题,我们通常用基向量的变换来表示线性变换。而基向量的变换可以用若干列向量拼合成的矩阵来表示:(egin{bmatrix} x_1 & x_2\ y_1 & y_2 end{bmatrix})。根据矩阵乘法定义,我们用一个向量乘这个变换矩阵得到的是新坐标系中这个向量用原坐标的表示(或者可以理解为原平面中的这个向量经过某种变换得到的新的向量):
注意是从右向左乘。
一个典型的例子是曼哈顿距离转切比雪夫距离。我们希望将整个平面顺时针旋转45°并且扩大 (sqrt 2) 倍。基向量 ((0,1),(1,0)) 将变化到: ((1,1),(1,-1)),于是转移矩阵为 (egin{bmatrix} 1 & 1\ 1 & -1 end{bmatrix}),一个向量 (egin{bmatrix}x\yend{bmatrix}) 的转化过程可以表示为:
线性变换是可以复合的,即存在一种变换,等价于先做 A 变换,再做 B 变换。变换矩阵也可以复合:
行列式
行列式可以表示一个线性变换对空间的体积的拉伸倍数。如 (detigg(egin{bmatrix}1 3\0 2end{bmatrix}igg)=2) 表示 (egin{bmatrix}1 3\0 2end{bmatrix}) 这种线性变换会把原平面内面积为 (S) 的图形的面积变成 (2S)。
如果一个线性变换的行列式为 (0),那么该线性变换之后空间会降维,例如可能会把一个平面上的所有点(向量)拍到一根直线上;如果一个线性变换的行列式为负数,说明这种线性变换会造成翻转现象。
显然,线性变换的复合的行列式为各线性变换行列式的乘积:(det(M_1M_2)=det(M_1)det(M_2))
线性方程组与矩阵
线性方程组可以用矩阵的形式来表示。严格地说,应该可以表示成 (Ax=v) 的形式。((x,v) 是向量,(A) 是系数矩阵)
秩 & 列空间 & 零空间 & 非方阵
秩表示线性变换后空间的维度。
列空间表示一个向量通过数乘能到达的所有向量的集合。
零空间为在线性变换后映射到原点的向量集合。
非方阵形如 (egin{bmatrix}a d \b e\c fend{bmatrix}) 或者 (egin{bmatrix}a c e\b d fend{bmatrix}),表示一种将某维度的空间映射到另一维度的空间。注意即使从二维映射到了三维,也只能是三维中的一个平面。
点积及其对偶性
(二维)点积可以视为将一个二维向量映射到一根直线上的线性变换,因此可以写成 (egin{bmatrix}a bend{bmatrix}) 的形式;而我们发现这跟直线恰好是 (egin{bmatrix} a\b end{bmatrix}),于是有一种解释为点积=投影 × 模长,而另一种解释为 (egin{bmatrix} a \ b end{bmatrix} cdot egin{bmatrix} x \ y end{bmatrix} = egin{bmatrix} ax \ by end{bmatrix})。
对偶性我也说不清,建议看博客或视频。
叉积及其对偶性
不太会。见博客或视频。
二维叉积是一个平行四边形的有向面积,三维叉积是一个向量???
好像还和行列式有关系。
基变换
线性变换中所有涉及。线性变换可以看作基向量的变换。
如果要知道新平面中进行某种变换后某向量的结果,可以先将新平面转化为我们平常说的直角坐标系(用矩阵求逆),然后在我们熟悉的坐标系中进行变换,然后再转换回去。可以表示为:
特征向量与特征值
在一些线性变换中,一些向量离开了它所张成的平面(直线),一些向量仍在它所张成的平面中。我们成这些没有离开它所张成的平面的向量为特征向量,它的拉伸/压缩程度为特征值。
有些变换存在一个或多个特征向量,有些则不存在(如旋转)
利用特征向量可以加速矩阵快速幂,因为我们发现如果把特征向量当作基向量的话,自乘只是让它乘方。
具体计算的话是这样的:
据此计算 (lambda),从而计算 (v)
以下是 OI 中常用的线性代数算法。
高斯消元
每次用一行中的一个元,消去同一列中的所有元,最终消成对角矩阵。
但是可能会有无数解或无解的情况。比如,找不到第 (j) 列非 0,且其前面都被消去的一行,或者出现 (0x_1 + 0x_2 + ... + 0x_n = 2.71828) 的情况。
我们发现:当出现 (0 = d) 的情况时,说明方程组无解;否则,如果消元过程中出现 找不到第 (j) 列非 0,且其前面都被消去的一行 的情况时,说明方程组有无数组解;否则,说明方程组有唯一解。(前提:n个元,n组方程)
同时,碰到 “找不到第 (j) 列非 0,且其前面都被消去的一行” 的情况 的次数还与矩阵的秩有关。矩阵的秩 = 最终有多少行的元的系数不全为0.
高斯消元有时也可以解决一些二进制问题,如:P2447 [SDOI2010]外星千足虫
模板提交处:P2455 [SDOI2006]线性方程组; P3389 【模板】高斯消元法
实数模板:
for (register int i = 1; i <= n; ++i) {//消去第i列的元
int mx = i;//用第mx行来消元
for (register int j = i + 1; j <= n; ++j)//前i行任务已经完成,剩下的为可用行
if (F(a[j][i]) > F(a[mx][i])) mx = j;//防止选第i个元系数为0的方程来消元
if (F(a[mx][i]) <= eps) failed();//第i个元系数全为0,方程有无数解
for (register int j = 1; j <= n; ++j) {//去消掉第j行的第i元系数
if (j == mx) continue;
double k = a[j][i] / a[mx][i];
for (register int p = 1; p <= n + 1; ++p)
a[j][p] -= a[mx][p] * k;
}
for (register int p = 1; p <= n + 1; ++p)//完成任务,放到前面
swap(a[mx][p], a[i][p]);
}
//x[i] = a[i][n + 1] / a[i][i];
实数含判断解的情况 模板:
inline void che() {
for (register int i = n; i; --i) {
if (F(h[i][n + 1]) <= eps) continue;
bool flag = false;
for (register int j = 1; j <= n; ++j) {
if (F(h[i][j]) > eps) {
flag = true; break;
}
}
if (!flag) {
puts("-1");
exit(0);
}
}
if (fg) {
puts("0");
exit(0);
}
}
inline gx() {
for (register int j = 1; j <= n; ++j) {
int id = 0;
for (register int i = j - fg; i <= n; ++i) {
if (F(h[i][j]) <= eps) continue;
id = i; break;
}
if (!id) {
fg++; continue;
}
for (register int i = 1; i <= n; ++i) {
if (i == id) continue;
double k = h[i][j] / h[id][j];
for (register int lie = j; lie <= n + 1; ++lie) {
h[i][lie] -= k * h[id][lie];
}
}
swap(h[j - fg], h[id]);
}
}
取模模板:
//m = n + 1
for (register int i = 1; i <= n; ++i) {
for (register int j = i; j <= n; ++j)
if (a[j][i]) { swap(a[j], a[i]); break; }
if (!a[i][i]) failed();
ll inv = get_inv(a[i][i]);
for (register int p = i; p <= m; ++p) a[i][p] = a[i][p] * inv % P;
//把改行乘inv,将a[i][i]变为1
for (register int j = 1; j <= n; ++j) {
if (j == i) continue;
ll k = a[j][i];
for (register int p = i; p <= m; ++p)
a[j][p] = ((a[j][p] - a[i][p] * k) % P + P) % P;
}
}
//左边n*n矩阵直接消成单位矩阵
//x[i] = a[i][n + 1];
矩阵求逆
ryc学长课件:
求逆矩阵
对于一个矩阵An×n,先构造一个增广矩阵W=[A∣E] ,其中E 是一个n×n的单位矩阵,这样W就成了一个n×2n的矩阵。
对W进行行变换,使之变成[E∣B] 的形式,这样就可以确定A^(−1)=B。
证明如下:因为是仅对增广矩阵W做行变换,因此,其做变换的过程可看为对W 左乘一个可逆矩阵P,使之满足PW=P[A∣E]=[PA∣P]=[E∣B] ,因此,有P=B且PA=E,显然,就有BA=E,则矩阵B就为矩阵A的逆。
挑重点:
对于一个矩阵A (n×n),先构造一个增广矩阵W=[A∣E] ,其中E 是一个n×n的单位矩阵,这样W就成了一个n×2n的矩阵。
对W进行行变换,使之变成[E∣B] 的形式,这样就可以确定A^(−1)=B。
复合矩阵
把矩阵当作元素的矩阵,即矩阵套矩阵。
其中 (0) 为全0的矩阵, (1) 为对角线(左上到右下)为1,其它为0的对角矩阵。
支持矩阵快速幂等。
注意复合矩阵的矩阵乘法要先给初始的那个 (Matrix) 初始化,即赋值n, m,不然小矩阵的乘法会出错。
求行列式&基尔霍夫矩阵树定理
详见:矩阵树定理(+行列式)
实用版:lhm_
- 求行列式
首先要知道行列式的性质:
-
转置后行列式不变
-
交换两行后行列式为相反数
-
一行乘某个系数再加到另一行,行列式不变。
...(基本就用到这么多了)
因此我们需要一种类似高斯消元的算法。
模意义下求行列式时,可以用辗转相除法:用下面的行消去上面的行,然后把上面的行和下面的行互换,并且ans×=-1,直到下面的行的某系数为0为止。由于是求行列式,我们最终消成上三角矩阵即可,即每次只用i行去和下面的行消即可。
由于是辗转相除法,就不用再找该列最大数了,因为如果用的是个0的话,会被交换到下面去。
- 基尔霍夫矩阵树定理
无向图生成树(把所有点全部用上)的方案数 = |度数矩阵 - 邻接矩阵|(行列式)
实际上,如果生成树带边权,一棵生成树的权值为边权积,那么所有的生成树的权值和其实还是 |度数矩阵 - 邻接矩阵|,只不过都要乘上个权值。
如果求有向图的生成树的权值和,我们也是可以做的,不过还要分外向树和内向树。外向树的度数矩阵是入度矩阵,内向树的度数矩阵是出度矩阵,并且要删去的那一行一列要恰好是根的那一行一列
其中要把矩阵的最后一行和最后一列删去!
inline void add(int x, int y) {//x和y连一条无向边
a[x][x]++; a[y][y]++;//度数矩阵
a[x][y]--; a[y][x]--;//邻接矩阵
}
inline ll gx(int n) {
ll ans = 1;
for (register int i = 1; i <= n; ++i) {
for (register int j = i + 1; j <= n; ++j) {
while (a[j][i]) {
ll k = a[i][i] / a[j][i];
for (register int p = 1; p <= n; ++p)
a[i][p] = ((a[i][p] - k * a[j][p]) % P + P) % P, swap(a[i][p], a[j][p]);
ans *= -1;
}
}
if (a[i][i] == 0) return 0;
ans = ans * a[i][i] % P;
}
return (ans + P) % P;
}
int main() {
...
//连边,tot为总点数
printf("%lld
", gx(tot - 1));
return 0;
}
线性基
二进制版线性基
实数版线性基
例题:P3265 [JLOI2015]装备购买
这回要来真的线性基了。
我们发现,线性基的数量其实就是矩阵的秩。因此我们用高斯消元求出矩阵的秩就可以了。
具体地说,我们还是开一个数组d存第i个元素的线性基是第几行。当我们发现某个元素还没有其对应的线性基时,我们就把线性基设定成它,统计答案并直接退出该行。如果已经有线性基,就用线性基的那一行消去该行的那个元素,这样这行在那个元素方面就肯定与线性基那行线性无关了。
特别地,如果那个元素为0,就不用管它了,因为它既不能当线性基,又不能与该元素的线性基那行线性相关。就直接进行下一个元素即可。
其实就相当于消成类似上三角矩阵(虽然遇到0就不是了,但是能保证当选线性基的那个元素的左边都是0)。
一行可以被上面的行线性表出的话,这一行最后应该全被消成0。
for (register int i = 1; i <= n; ++i) {
for (register int p = 1; p <= m; ++p) {
if (F(nod[i].a[p]) <= eps) continue;
if (!d[p]) {
d[p] = i;
ans += nod[i].fare;
cnt++;
break;
}
double k = nod[i].a[p] / nod[d[p]].a[p];
for (register int np = p; np <= m; ++np)
nod[i].a[np] -= k * nod[d[p]].a[np];
}
}
附
矩阵乘法,矩阵快速幂(调试用)
struct matrix {
int h[N][N];
int n, m;
matrix() {
memset(h, 0, sizeof(h));
}
void printm() {//debug
puts("print:");
for (register int i = 1; i <= n; ++i) {
for (register int j = 1; j <= n; ++j) {
printf("%d ", h[i][j]);
}
puts("");
}
puts("");
}
matrix operator +(const matrix a) const {
matrix mx; mx.n = n; mx.m = m;
for (register int i = 1; i <= n; ++i) {
for (register int j = 1; j <= m; ++j) {
mx.h[i][j] = (h[i][j] + a.h[i][j]) % P;
}
}
return mx;
}
matrix operator *(const matrix a) const {
matrix mx; mx.n = n; mx.m = a.m;
for (register int k = 1; k <= n; ++k) {
for (register int i = 1; i <= m; ++i) {
for (register int j = 1; j <= a.m; ++j) {
mx.h[i][j] = (mx.h[i][j] + h[i][k] * a.h[k][j]) % P;
}
}
}
return mx;
}
};
inline matrix quickpow(fuhe x, int k) {
--k;
matrix res = x;
while (k) {
if (k & 1) res = res * x;
x = x * x;
k >>= 1;
}
return res;
}
高斯消元模板(调试用)
实数简化版
for (register int i = 1; i <= n; ++i) {//消去第i列的元
int mx = i;//用第mx行来消元
for (register int j = i + 1; j <= n; ++j)//前i行任务已经完成,剩下的为可用行
if (F(a[j][i]) > F(a[mx][i])) mx = j;//防止选第i个元系数为0的方程来消元
if (F(a[mx][i]) <= eps) failed();//第i个元系数全为0,方程有无数解
for (register int j = 1; j <= n; ++j) {//去消掉第j行的第i元系数
if (j == mx) continue;
double k = a[j][i] / a[mx][i];
for (register int p = 1; p <= n + 1; ++p)
a[j][p] -= a[mx][p] * k;
}
for (register int p = 1; p <= n + 1; ++p)//完成任务,放到前面
swap(a[mx][p], a[i][p]);
}
//x[i] = a[i][n + 1] / a[i][i];
实数含特判解情况版:
inline void che() {
for (register int i = n; i; --i) {
if (F(h[i][n + 1]) <= eps) continue;
bool flag = false;
for (register int j = 1; j <= n; ++j) {
if (F(h[i][j]) > eps) {
flag = true; break;
}
}
if (!flag) {
puts("-1");//无解
exit(0);
}
}
if (fg) {
puts("0");//有无数组解
exit(0);
}
}
inline gx() {
for (register int j = 1; j <= n; ++j) {
int id = 0;
for (register int i = j - fg; i <= n; ++i) {
if (F(h[i][j]) <= eps) continue;
id = i; break;
}
if (!id) {
fg++; continue;
}
for (register int i = 1; i <= n; ++i) {
if (i == id) continue;
double k = h[i][j] / h[id][j];
for (register int lie = j; lie <= n + 1; ++lie) {
h[i][lie] -= k * h[id][lie];
}
}
swap(h[j - fg], h[id]);
}
}
取模版
//m = n + 1
for (register int i = 1; i <= n; ++i) {
for (register int j = i; j <= n; ++j)
if (a[j][i]) { swap(a[j], a[i]); break; }
if (!a[i][i]) failed();
ll inv = get_inv(a[i][i]);
for (register int p = i; p <= m; ++p) a[i][p] = a[i][p] * inv % P;
//把改行乘inv,将a[i][i]变为1
for (register int j = 1; j <= n; ++j) {
if (j == i) continue;
ll k = a[j][i];
for (register int p = i; p <= m; ++p)
a[j][p] = ((a[j][p] - a[i][p] * k) % P + P) % P;
}
}
//左边n*n矩阵直接消成单位矩阵
//x[i] = a[i][n + 1];
基尔霍夫矩阵树定理 + 求行列式
辗转相减法
inline void add(int x, int y) {//x和y连一条无向边
a[x][x]++; a[y][y]++;//度数矩阵
a[x][y]--; a[y][x]--;//邻接矩阵
}
inline ll gx(int n) {
ll ans = 1;
for (register int i = 1; i <= n; ++i) {
for (register int j = i + 1; j <= n; ++j) {
while (a[j][i]) {
ll k = a[i][i] / a[j][i];
for (register int p = 1; p <= n; ++p)
a[i][p] = ((a[i][p] - k * a[j][p]) % P + P) % P, swap(a[i][p], a[j][p]);
ans *= -1;
}
}
if (a[i][i] == 0) return 0;
ans = ans * a[i][i] % P;
}
return (ans + P) % P;
}
int main() {
...
//连边,tot为总点数
printf("%lld
", gx(tot - 1));
return 0;
}
逆元法
int n, m, t;
int h[N][N];
inline void gx() {
ll ans = 1;
for (register int i = 1; i <= n; ++i) {
int id = -1;
for (register int hang = i; hang <= n; ++hang) {
if (h[hang][i] != 0) {
id = hang; break;
}
}
if (id == -1) {
puts("0");
return ;
}
ll inv = quickpow(h[id][i], P - 2);//Attention!!
ans = ans * h[id][i] % P;
for (register int j = i; j <= n; ++j) h[id][j] = 1ll * h[id][j] * inv % P;
for (register int hang = i; hang <= n; ++hang) if (hang != id) {
ll k = h[hang][i];
for (register int j = i; j <= n; ++j) {
DEL(h[hang][j], k * h[id][j] % P);
}
}
if (id != i) {
ans = P - ans;
swap(h[id], h[i]);
}
}
ans %= P;
if (ans < 0) ans += P;
printf("%lld
", ans);
}
namespace jzp1 {//无向图求生成树
inline void addedge(int u, int v, int val) {
ADD(h[u][u], val), ADD(h[v][v], val);
DEL(h[u][v], val), DEL(h[v][u], val);
}
inline void sol() {
for (register int i = 1; i <= m; ++i) {
int u, v, w; read(u), read(v), read(w);
addedge(u, v, w);
}
--n;
gx();
}
}
namespace jzp2 {//有向图求外向树
inline void Read(int &cur) {
read(cur);
--cur;
if (!cur) cur = n;
}
inline void addedge(int u, int v, int val) {
DEL(h[u][v], val);
ADD(h[v][v], val);
}
inline void sol() {
for (register int i = 1; i <= m; ++i) {
int u, v, w; Read(u), Read(v), read(w);
if (u != v) addedge(u, v, w);
}
--n;
gx();
}
}