Dyd's Blog

He who has a strong enough why can bear almost any how.

矩阵

系统的看一下

前面递归式学习时浅尝其苦,现在真的来受苦

矩阵

前置知识

定义类

  • 矩阵:由 $m \times n$ 个元素排列成 $m$ 行 $n$ 列的数表叫矩阵,即:
    $$
    \begin{aligned}
    A =
    \begin{bmatrix}
    a_{1, 1}, a_{1, 2}, …, a_{1, n} \\
    a_{2, 1}, a_{2, 2}, …, a_{2, n} \\
    … \\
    a_{m, 1}, a_{m, 2}, …, a_{m, n} \\
    \end{bmatrix}
    \end{aligned}
    $$

  • 奇/偶排列:逆序对数为奇/偶数的排列,一下记排列 $a_1, a_2, …, a_n$ 的逆序对数为 $\tau(a_1, a_2, …, a_n)$

  • 方阵: $n \times n$ 的矩阵,一般记作方阵 $A, B, C …$ ,其中 $n$ 叫做该方阵的

  • 行列式:一个 $n$ 阶方阵的行列式等于取自所有不同行不同列的 $n$ 个元素的乘积的“代数和”(即按奇/偶排列带负/正号),记作 $det(A)$ (也有记作 $\mid A \mid$ ),形式化的:
    $$
    \begin{aligned}
    对于 n 阵方阵 A & =
    \begin{bmatrix}
    a_{1, 1}, a_{1, 2}, …, a_{1, n} \\
    a_{2, 1}, a_{2, 2}, …, a_{2, n} \\
    … \\
    a_{n, 1}, a_{n, 2}, …, a_{n, n} \\
    \end{bmatrix} \\
    其行列式 det(A) & =
    \begin{vmatrix}
    a_{1, 1}, a_{1, 2}, …, a_{1, n} \\
    a_{2, 1}, a_{2, 2}, …, a_{2, n} \\
    … \\
    a_{n, 1}, a_{n, 2}, …, a_{n, n} \\
    \end{vmatrix} \\
    & = \sum_{j_1, j_2, …j_n 是 1 \sim n 的一个排列} (-1)^{\tau(j_1, j_2, …, j_n)} a_{1, j_1} a_{2, j_2} … a_{n, j_n}
    \end{aligned}
    $$
    不难看出,直接求 $det$ 是 $O(n!)$ 的

  • 代数余子式:把一个 $n$ 阶行列式去掉第 $i$ 行和第 $j$ 列后,留下的 $n - 1$ 阶行列式叫做元素 $a_{i, j}$ 的余子式,记作 $M_{i, j}$ ,记 $A_{i, j} = (-1)^{i + j} M_{i, j}$ ,叫做 $a_{i, j}$ 的代数余子式

  • 主/副对角线:对于一个方阵,所有 $i = j$ 的元素 $a_{i, j}$ 组成其主对角线;所有 $i + j = n + 1$ 的元素 $a_{i, j}$ 组成其副对角线

  • 上/下三角矩阵:主对角线左下方/右上方全为 $0$ 的矩阵

  • 同型矩阵:行列数一样的矩阵

  • 主子式:在 $n$ 阶行列式中,任意选取 $i$ 个行号,再选取与行号相同的列号,只保留所选行、列的行列式即为 $n$ 阶行列式的 $i$ 阶主子式

  • 给定一个无向图 $G(V, E)$

    • 度数矩阵 $D$ :当 $i \ne j$ 时, $D[i][j] = 0$ ,否则 $D[i][j] = 点 i(j) 的度数$

    • 邻接矩阵 $A$ :当 $(u, v) \in E$ 时, $A[u][v] = 1$ ,否则 $A[u][v] = 0$

    • 基尔霍夫矩阵( Kirchhoff ) $K$ ,也称拉普拉斯算子:定义为 $K = D - A$ ,如图:

      K

    • 关联矩阵:对于一个 $n$ 个点 $m$ 条边的无向图,我们把边任意定向,那么其关联矩阵是一个 $n$ 行 $m$ 列的矩阵,其中元素满足:
      $$
      b_{i, j} =
      \begin{cases}
      -1 & (i 是边 j 的终点) \\
      1 & (i 是边 j 的起点) \\
      0 & (其它)
      \end{cases}
      $$

计算类

  • 线性运算:

    1. 加法:两个同型矩阵 $A, B$ 的加法运算得到同型矩阵 $C$ ,且 $C$ 中元素满足 $c_{i, j} = a_{i, j} + b_{i, j}$

    2. 减法:两个同型矩阵 $A, B$ 的加法运算得到同型矩阵 $C$ ,且 $C$ 中元素满足 $c_{i, j} = a_{i, j} - b_{i, j}$

    3. 数乘:一个矩阵 $A$ 乘一个数 $\lambda $ 得到同型矩阵 $C$ ,且 $C$ 中元素满足 $c_{i, j} = \lambda a_{i, j}$

      显然,数乘满足分配律和结合律

  • 矩阵乘法:若 $A$ 是 $m \times n$ 的矩阵, $B$ 是 $n \times p$ 的矩阵,它们的乘积是一个 $m \times p$ 的矩阵 $C$ ,且满足 $c_{i, j} = \sum_{r = 1}^n a_{i, r} b_{r, j}$

    矩阵乘法满足结合律、左/右分配律,但没有交换律

  • 转置:把矩阵的行换成同序数的列叫做矩阵的转置,记作 $A^{T}$ ,例如:

    $$
    \begin{bmatrix}
    1, 2 , 3 \\
    4, 5, 6 \\
    \end{bmatrix}
    ^{T}
    =
    \begin{bmatrix}
    1, 4 \\
    2, 3 \\
    5, 6 \\
    \end{bmatrix}
    $$
    明显有以下运算律:
    $$
    \begin{aligned}
    & (A^T)^T = A \\
    & (\lambda A)^T = \lambda A^T \\
    & (AB)^T = B^T A^T (这里注意顺序)\\
    \end{aligned}
    $$

  • 行列式的数学计算式: $n$ 阶方阵的行列式等于其任意行/列元素与对应代数余子式乘积之和,即 $det(A) = \sum_{j = 1}^n a_{i, j} A_{i, j} = \sum_{i = 1}^n a_{i, j} A_{i, j}$ (注意代数余子式是有符号的)

矩阵快速幂

一般的,对于线性递推式 $f_i = a_1 f_{i - 1} + a_2 f_{i - 2} + … + a_k f_{i - k}$ ,我们令 $F = \begin{bmatrix} f_{i - 1} \\ f_{i - 2} \\ … \\ f_{i - k} \end{bmatrix}, F’ = \begin{bmatrix} f_{i} \\ f_{i - 1} \\ … \\ f_{i - k + 1} \end{bmatrix}$ ,不难得到转移矩阵 $A = \begin{bmatrix} a_1 & a_2 & … & a_{k - 1} &a_k \\ 1 &0 &… &0 &0 \\ 0 &1 &… &0 &0 \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 &0 &… &1 &0 \end{bmatrix}$ ,于是有 $F’ = AF$ ,那么,若 $F_0$ 已知,则 $F_i = A^i F_0$ ,可以先用类似快速幂的方法求 $A^i$ ,把时间降至 $O(k^3 \log n)$

行列式

性质

关于行列式,有几个性质:

  1. 交换对应方阵的 $1$ 行和 $1$ 列(进行一次矩阵转置),行列式不变;直接定义展开可证,其实没啥用

  2. 交换对应方阵的 $2$ 行(列),行列式取反;

    简证:交换一个排列中的两个数,除开它们两个,其它数的逆序对数一定成对变化(变化偶数),只有它们两个导致逆序对变 $1$ ,所以 $det$ 中每一项都取反

  3. 若有一行(列)相等,行列式为 $0$

    简证:交换相等的这两行,得 $det = -det$

  4. 任意行(列)所有元素等比例变化,则行列式也等比例变化;展开后由乘法分配律可得

  5. 任意两行/列成比例,行列式为 $0$ ;由性质 $3, 4$ 可得

  6. 如果行列式对应方阵 $A$ 中有一行(列),是对应2个方阵 $B, C$ 中分别的 $2$ 行(列)所有元素之和,且 $A$ 中其它元素都等于 $B, C$ 中对应元素,则有 $det(A) = det(B) + det(C)$ ;写成行列式,由乘法分配律可证

  7. 把一个矩阵的一行(列)的值全部乘一个常数加到另一行(列)上,行列式值不变;由性质 $5, 6$ 可得

  8. 上三角矩阵的行列式就等于对角线元素乘积;展开可得

  9. $det(AB) = det(A) \times det(B)$

计算

直接计算行列式是 $O(n!)$ ,用数学算法要大量分治递归(实测 $n > 12$ 时几乎就是暴力),所以我们考虑性质 $8$ ,只要通过行列式的变换把它变成上三角矩阵,我们就可以 $O(n)$ 计算了,又由性质 $2, 4, 7$ ,我们可以直接高斯消元 $O(n^3)$

实现时注意若没有逆元,要辗转相除,此时虽然多个 $\log $ ,但每次做两次消元 $a_{i, i}$ 至少会变为原来的一半,而这个势能是不会上升的,均摊下去复杂度为 $O(n^2 (\log p + n))$

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
#include <bits/stdc++.h>
using LL = long long;
const int N = 600 + 100;
int n, p;
struct Mtx
{
int x[N][N];
int* operator [] (int id){ return x[id]; }
} a;
void adj(int &x){ x += (x >> 31) & p; }
int det(Mtx x)
{
int res = 1, i, j, t, k, f = 1;
for (i = 1; i <= n; ++i)
{
for (j = i + 1; j <= n; ++j) //这里不找最大直接消是因为在取模意义下没有精度误差
{
while (x[i][i]) //对第i行和第j行做辗转相减
{
t = x[j][i] / x[i][i];
for (k = i; k <= n; ++k) adj(x[j][k] -= LL(t) * x[i][k] % p);
std::swap(x.x[i], x.x[j]), f ^= 1;
}
std::swap(x.x[i], x.x[j]), f ^= 1;
}
res = LL(res) * x[i][i] % p;
}
if (!f) adj(res = -res); //注意res=0时
return res;
}
int main()
{
scanf("%d %d", &n, &p);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
{
scanf("%d", &a[i][j]);
adj(a[i][j] %= p); //保证正数
}
printf("%d\n", det(a));
return 0;
}

当然,若模数是质数,还是直接取逆元好,而且长得和gauss 消元更像(但其实这是高斯-约旦消去法):

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
#include <bits/stdc++.h>
using LL = long long;
const int N = 600 + 100;
int n, p;
struct Mtx
{
int x[N][N];
int* operator [] (int id){ return x[id]; }
} a;
void adj(int &x){ x += (x >> 31) & p; }
int qpow(int x, int y)
{
int res = 1;
for (; y; y >>= 1, x = LL(x) * x % p) if (y & 1) res = LL(res) * x % p;
return res;
}
int det(Mtx x)
{
int res = 1, i, j, t, k, iv, f = 1;
for (i = 1; i <= n; ++i)
{
for (j = i; j <= n; ++j) if (x[j][i])
{
std::swap(x.x[i], x.x[j]);
if (i != j) f ^= 1;
break;
}
if (!x[i][i]) return 0;
for(j = i + 1, iv = qpow(x[i][i], p - 2);j <= n; ++j)
{
t = LL(x[j][i]) * iv % p;
for(k = i; k <= n; ++k) adj(x[j][k] -= LL(t) * x[i][k] % p);
}
res = LL(res) * x[i][i] % p;
}
if (!f) adj(res = -res);
return res;
}
int main()
{
scanf("%d %d", &n, &p);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
{
scanf("%d", &a[i][j]);
adj(a[i][j] %= p); //gauss 消元要保证正数
}
printf("%d\n", det(a));
return 0;
}

关于行列式的运用,还有几个东西,我懒了,可以查阅 OI Wiki ,它们是

  • LGV 引理

  • BEST 定理

  • Binet-Cauchy定理

咕咕咕

Matrix-Tree 定理(矩阵树定理)

无向图 $G$ 的所有生成树个数等于其 kirchhoff 矩阵 $K(G)$ 任何一个 $n - 1$ 阶主子式的绝对值

几条引理

由于证明太麻烦就不写了,可以参考这个

  • 一个 kirchhoff 矩阵 $K$ 的行列式总是为 $0$

  • 如果图 $G$ 不构成树,那么 $K(G)$ 的任意一个 $n - 1$ 阶主子式都为 $0$

  • 如果图 $G$ 构成树,则 $K(G)$ 的任意一个 $n - 1$ 阶主子式绝对值都为 $1$

  • 设图 $G$ 的关联矩阵为 $B$ ,有 $B B^T = K$

常见变形

  • 若存在必选边,考虑将必选边连接的点缩点,对压缩后的图求生成树
  • 如果图中边有边权,可以把度数矩阵 $D$ 变成边的权值和,直接用Matrix-Tree 定理,求得的就是所有生成树边权乘积的总和
  • 如果求有向图生成树,首先要把邻接矩阵 $A$ 变成有向图的邻接矩阵,然后对于 $D$ ,如果它记录的是到该点入的边权总和,那么求得的就是外向树 (从根向外),即 $D[i][j] = \sum_{j = 1}^n A[j][i]$ ;类似的,如果它记录的是到该点出的边权总和,那么求得的就是内向树 (从外向根),关于如何保证根,巨佬们说:去掉第 $k$ 行 $k$ 列就是以 $k$ 为根

线性方程

一般来说,线性方程形式如下:
$$
\begin{cases}
a_{1, 1} x_1 + a_{1, 2} x_2 + … + a_{1, n} x_n = b_1 \\
a_{2, 1} x_1 + a_{2, 2} x_2 + … + a_{2, n} x_n = b_2 \\
… \\
a_{m, 1} x_1 + a_{m, 2} x_2 + … + a_{m, n} x_n = b_m \\
\end{cases}
$$
我们常记为 $AX = B$ ,其中 $A, B$ 是系数/常数矩阵,而 $X$ 是未知数构成的矩阵( $X, B$ 也可理解为向量)

guass 消元

不多说,给代码

实数版:

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
#include <bits/stdc++.h>
using DB = double;
const int N = 100 + 100;
const DB eps = 1e-8;
int n;
struct Mtx
{
DB x[N][N];
DB* operator [] (int id){ return x[id]; }
} a;
int guass(Mtx &x, int len) //注意传实参
{
int i, j, k, t, now;
for (i = now = 1; i <= len; ++i)
{
t = now;
for (j = i; j <= len; ++j) if (fabs(x[j][i]) > fabs(x[t][i])) t = j;
if (fabs(x[t][i]) < eps) continue;
if (t != now) std::swap(x.x[t], x.x[now]);
for (j = len + 1; j >= i; --j) x[now][j] /= x[now][i];
for (j = now + 1; j <= len; ++j) if (fabs(x[j][i]))
for (k = len + 1; k >= i; --k) x[j][k] -= x[j][i] * x[now][k];
++now;
}
if (now <= len)
{
for (i = now; i <= len; ++i) if (fabs(x[i][len + 1]) > eps) return -1;
return len - now + 1;
}
for (i = len; i; --i)
for (j = i + 1; j <= len; ++j) x[i][len + 1] -= x[i][j] * x[j][len + 1];
for (i = len; i; --i) if (fabs(x[i][len + 1]) < eps) x[i][len + 1] = 0; //防止出现-0.00
return 0;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n + 1; ++j) scanf("%lf", &a[i][j]);
if (!guass(a, n)) for (int i = 1; i <= n; ++i) printf("%.2lf\n", a[i][n + 1]);
else puts("No Solution");
return 0;
}

取模版:

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
int qpow(int x, int y)
{
int res = 1;
for (; y; y >>= 1, x = LL(x) * x % P) if (y & 1) res = LL(res) * x % P;
return res;
}
void adj(int &x){ x += (x >> 31) & P; }
int guass(Mtx &x, int len) //进入前保证系数非负
{
int i, j, k, now, t;
for (i = now = 1; i <= len; ++i)
{
for (t = i; t <= len; ++t) if (x[t][i]) break;
if (t > len) continue;
if (t != now) std::swap(x.x[t], x.x[now]);
if (x[now][i] != 1) for (j = len + 1, t = qpow(x[now][i], P - 2); j >= i; --j) x[now][j] = LL(t) * x[now][j] % P;
for (j = i + 1; j <= len; ++j) if (x[j][i])
for (k = len + 1; k >= i; --k) adj(x[j][k] -= LL(x[j][i]) * x[i][k] % P);
++now;
}
if (now <= len)
{
for (i = now; i <= len; ++i) if (x[i][len + 1]) return -1;
return len - now + 1;
}
for (i = len; i; --i)
for (j = i + 1; j <= len; ++j) adj(x[i][len + 1] -= LL(x[i][j]) * x[j][len + 1] % P);
return 0;
}

LU分解法

在系数矩阵不变,仅仅是常数改变时,LU分解法有很大的优势

顾名思义,LU分解就是把系数矩阵 $A$ 分解成两个矩阵 $L, U$ ,满足 $LU = A$ 且 $L$ 为下三角矩阵且主对角线为 $1$ , $U$ 为上三角矩阵,即:
$$
\begin{bmatrix}
a_{1, 1} & a_{1, 2} & … & a_{n, n} \\
a_{2, 1} & a_{2, 2} & … & a_{2, n} \\
\vdots & \vdots & \ddots & \vdots \\
a_{n, 1} & a_{n, 2} & … & a_{n, n} \\
\end{bmatrix}
=
\begin{bmatrix}
1 & 0 & … & 0 \\
l_{2, 1} & 1 & … & 0 \\
\vdots & \vdots & \ddots & \vdots \\
l_{n, 1} & l_{n, 2} & … & 1 \\
\end{bmatrix}
\begin{bmatrix}
u_{1, 1} & u_{1, 2} & … & u_{1, n} \\
0 & u_{2, 2} & … & u_{2, n} \\
\vdots & \vdots & \ddots & \vdots \\
0 & 0 & … & u_{n, n} \\
\end{bmatrix}
$$
于是,有 $LUX = B$ ,即 $UX = Y, LY = B$

你可能在担心该如何求 $L, U, X, Y$ ,没有关系,前人已经帮我们总结了公式(这也叫 Doolittle 算法):

  1. 求 $L, U$ ,对于 $k = 1, 2, …, n$ :
    $$
    \begin{cases}
    u_{k, j} = a_{k, j} - \sum_{r = 1}^{k - 1} l_{k, r} u_{r, j} & j = k, k + 1, …, n \\
    u_{i, k} = (a_{i, k} - \sum_{r = 1}^{k - 1} l_{i, r} u_{r, k}) / u_{k, k} & i = k, k + 1, …, n\\
    l_{k, k} = 1
    \end{cases}
    $$

  2. 求 $Y$ ,对于 $k = 1, 2, …, n$ :
    $$
    y_k = b_k - \sum_{r = 1}^{k - 1} l_{k, r} y_{r}
    $$

  3. 求 $X$ ,对于 $k = n, n - 1, …, 1$ :
    $$
    x_k = (y_k - \sum_{r = k + 1}^n u_{k, r} x_r) / u_{k, k}
    $$

不难发现,第一步是 $O(n^3)$ ,另外两步都是 $O(n^2)$ 的,要支持取模也很方便(背公式就好了,这不比高斯香?)

ps:如果方程有多解或者无解, $L, U$ 中会有 nan ,注意处理

矩阵求逆

在 guass 的同时对单位矩阵进行一样的操作即可(一定要先操作单位矩阵)

ps:其实,对于 $AX = B$ ,可以 $X = A^{-1} B$ 来解

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
54
55
56
#include <bits/stdc++.h>
using LL = long long;
const int N = 400 + 100, P = 1e9 + 7;
int n;
struct Mtx
{
int x[N][N];
int* operator [] (int id){ return x[id]; }
} a, b;
int qpow(int x, int y)
{
int res = 1;
for (; y; y >>= 1, x = LL(x) * x % P) if (y & 1) res = LL(res) * x % P;
return res;
}
void adj(int &x){ x += (x >> 31) & P; }
bool mtxinv(Mtx &x, Mtx &as, int len)
{
int i, j, k, t;
for (i = 1; i <= len; ++i)
{
for (t = i; t <= len; ++t) if (x[t][i]) break;
if (t > len) return false;
if (t != i) std::swap(x.x[t], x.x[i]), std::swap(as.x[t], as.x[i]);
if (x[i][i] != 1)
{
for (j = len, t = qpow(x[i][i], P - 2); j >= 1; --j) as[i][j] = LL(t) * as[i][j] % P;
for (j = len, t = qpow(x[i][i], P - 2); j >= i; --j) x[i][j] = LL(t) * x[i][j] % P;
}
for (j = i + 1; j <= len; ++j) if (x[j][i])
for (k = len; k >= 1; --k) adj(as[j][k] -= LL(x[j][i]) * as[i][k] % P);
for (j = i + 1; j <= len; ++j) if (x[j][i])
for (k = len; k >= i; --k) adj(x[j][k] -= LL(x[j][i]) * x[i][k] % P);
}
for (i = len; i; --i)
for (j = i + 1; j <= len; ++j)
{
for (k = len; k >= 1; --k) adj(as[i][k] -= LL(x[i][j]) * as[j][k] % P);
adj(x[i][len + 1] -= LL(x[i][j]) * x[j][len + 1] % P);
}
return true;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) scanf("%d", &a[i][j]);
for (int i = 1; i <= n; ++i) b[i][i] = 1;
if (mtxinv(a, b, n))
{
for (int i = 1; i <= n; ++i, puts(""))
for (int j = 1; j <= n; ++j) printf("%d ", b[i][j]);
}
else puts("No Solution");
return 0;
}