线性代数笔记
线性代数笔记
高斯消元
高斯消元是用于求解 \(n\) 元线性方程组的算法。这里不讲别的,只讲算法流程。
首先,可以用一个 \(n\) 行 \((n + 1)\) 列的矩阵来表示一个 \(n\) 元线性方程组:
可以用下面这个矩阵来表示:
高斯消元会将它通过三个矩阵的初等变换变成一个阶梯型矩阵,下面是三种初等变换:
- 交换矩阵的两行。
- 将矩阵的一行元素全部乘上 \(\lambda\) ( \(\lambda \neq 0\) )。
- 将矩阵的一行减去 / 加上另一行。
阶梯型矩阵是矩阵的一种类型。它的基本特征是:所给矩阵为行阶梯型矩阵,则矩阵中每一行的第一个不为零的元素的左边及其所在列以下全为零。
高斯消元最后形成的阶梯型矩阵形如:
所以得到了消元之后的矩阵,只需要从最后一行不断往上代,就能分别求出 \(x_1, x_{2}, \cdots, x_{n}\)。
同时还有约旦·高斯消元,这种算法最后消出来的矩阵为:
这种消元不用往回代,只需要用最后一列的值除以对应行的系数就能得到 \(x_i\)。
矩阵求逆
定义:
如果矩阵 \(A\) 存在 \(A^{-1}\),满足 \(A^{-1} \times A = A \times A^{-1} = E\),其中 \(E\) 是单位矩阵。那么 \(A^{-1}\) 就是 \(A\) 的逆矩阵,矩阵 \(A\) 是可逆的。
求法:
可以通过下面的公式求得:
其中 \([AB]\) 表示 \(A\) 和 \(B\) 两个矩阵拼起来得到的矩阵。
可以用高斯·约旦消元求出结果,这里举个例子:
求$$\begin{bmatrix}2 & -1 & 0 \\-1 & 2 & -1 \\0 & -1 & 2\end{bmatrix}$$
的逆矩阵,首先写出 \([AE]\): $$\begin{bmatrix}2 & -1 & 0 & 1 & 0 & 0 \\-1 & 2 & -1 & 0 & 1 & 0 \\0 & -1 & 2 & 0 & 0 & 1 \end{bmatrix}$$
对左边进行消元可得: $$\begin{bmatrix}2 & -1 & 0 & 1 & 0 & 0 \\ 0 & \frac{3}{2} & -1 & \frac{1}{2} & 1 & 0 \\0 & 0 & \frac{4}{3} & \frac{1}{3} &\frac{2}{3} & 1 \end{bmatrix}$$
此时已消成上三角矩阵,高斯消元开始回代,用约旦会消成对角矩阵:$$\begin{bmatrix}2 & 0 & 0 & \frac{3}{2} & 1 & \frac{1}{2} \\0 & \frac{3}{2} & 0 & \frac{3}{4} & \frac{3}{2} & \frac{3}{4} \\0 & 0 & \frac{4}{3} & \frac{1}{3} & \frac{2}{3} & 1\end{bmatrix}$$
最后每行除以系数: $$\begin{bmatrix}1 & 0 & 0 & \frac{3}{4} & \frac{1}{2} & \frac{1}{4} \\0 & 1 & 0 & \frac{1}{2} & 1 & \frac{1}{2} \\ 0 & 0 & 1 & \frac{1}{4} & \frac{1}{2} & \frac{3}{4} \end{bmatrix}$$
此时右半边即为所求。
这里要判 \(A\) 是否是一个可逆矩阵,其实就是在高斯·约旦算法消元时判断是否无解。
例题
[JSOI2008] 球形空间产生器:
显然有 \(dist_1 = dist_2 = \cdots = dist_{n + 1}\),所以有 :
接着就可以用高斯消元解决此题。
[USACO09NOV] Lights G:
根据异或的性质,显然一个点操作两次及以上是没有意义的。
所以可以写成一个异或方程组的形式:
这上面 \(x_i\) 表示点 \(i\) 是否被操作过,\(V_{i, j}\) 表示 \(i\) 被操作是否会影响到 \(j\) 的状态,也就是 \(i, j\) 之间是否有连边(在这种定义下,需要钦定 \(V_{i, i} = 1\))。
这种方程组也可以用高斯消元消掉,如果有自由元就要深搜一下自由元状态,然后这题就没了。
[HNOI2011] XOR和路径:
首先考虑 dp,设 \(f_u\) 表示以 \(u\) 为起点的答案,有:
这里 \(deg_u\) 表示 \(u\) 点的度数(注意自环只算一次)。
但是这道题会有环,所以正常 dp 会有后效性,于是考虑用高斯消元来解决后效性问题。上面的式子还不能直接用高斯消元,因为有异或,考虑把异或给转化掉。原式等于:
接着由于期望的线性性,所以可以拆位计算异或,拆位之后,式子变成:
其中 \(w'\) 是 \(w\) 拆位之后的值(值只能是 \(0\) 或 \(1\)),再移项:
对于 \(1 \sim (n - 1)\) 都有一个这样的约束,对于 \(n\),有 \(f_n = 0\)。所以用高斯消元就可以求出 \(f_1\),答案就是 \(\sum\limits_{k = 0}^{\log V} 2^k \cdot f_1'\)。时间复杂度 \(\mathcal{O}(n^3\log V)\)。
[HNOI2013] 游走:
首先可以发现因为期望的可加性,所以答案是每条边的编号乘上每条边的期望经过次数。现在编号是由我们自己定的,每条边的期望经过次数是不变的,由于要最小化答案所以我们一定要让大的乘小的、小的乘大的。现在就考虑求边的期望经过次数,有点难以求得,不如先求点的期望经过次数,再通过点的经过次数得到边的:假设一条边 \((u, v)\) ( \(u, v \ne n\) ),设 \(f_u\) 是点 \(u\) 的期望经过次数,那么 \((u, v)\) 这条边的期望经过次数就等于 \(f_u \times \frac{1}{deg_u} +f_v \times \frac{1}{deg_v}\)。
接着就考虑求 \(f\),\(f\) 有朴素转移式子:
并且有 \(f_n = 1\),这是一个显然有后效性的东西(因为原图不是 DAG),所以考虑用高斯消元求:
最后算出边的期望就行了。
First Knight
这道题题意就是将 \((n, m)\) 大小的矩阵,每个点有概率往上下左右走,且不可能走出矩阵。要求求出 \((1, 1)\) 到 \((n, m)\) 的期望步数。
很显然,有一个 dp,设 \(f_{i, j}\) 表示从 \((i, j)\) 到 \((n, m)\) 的期望步数,那么有:
然后就可以移项,变成一个高斯消元,看起来是 \(\mathcal{O}(n^6)\) 的。但是实际上,在消其它项的系数的时候,可以发现真正被消的只有 \(\mathcal{O}(1)\) 个系数,所以实际上是 \(\mathcal{O}(n^4 \sim n^5)\) 的(太蒻了不会算)。
[USACO10HOL] Driving Out the Piggies G:
这题和游走非常相似啊,设 \(f_i\) 表示炸弹在 \(i\) 爆炸的概率,有:
接着移项然后高斯消元就行了。
矩阵树定理
行列式的定义
行列式是一个函数定义,取值是一个标量。
对一个 \(n \times n\) 的矩阵 \(A\),其 \(n\) 阶行列式写作 \(\det(A)\) 或者 \(| A |\),定义为:
\(P\) 是一个排列,\(\tau(P)\) 表示一个排列的逆序对数。
数学上的定义是:行列式可以理解为所有列向量所夹的几何体的有向体积,这样可以结合从几何直观出发理解为线性变换的伸缩因子。
但是 OI 中,不用管那么多数学意义。只需求出行列式的值即可。
行列式的性质
行列式的性质决定了高斯消元求行列式的可行性,性质如下:
- 交换行列式的两行,行列式变号。这个很好证:交换了两行,相当于对于每个排列,都交换了两个元素。排列中交换两个元素会使逆序对数奇偶性改变,首先交换排列中两个相邻元素会使奇偶性改变,而交换两个元素相当于交换 \(2x - 1\) 次相邻元素。\((1)\)
- 交换排列的一行和一列,行列式不变。 \((2)\)
- 如果 \(C\) 中有一行是 \(A\) 对应的行与 \(B\) 对应的行之和,其余元素都是相同的。有 \(\det(C) = \det(A) + \det(B)\):\((3)\)
- 带入式子时,用乘法分配律不难证明。
- 由 \((3)\) 可以推出,当矩阵的某一行集体乘上 \(\lambda\) 时,行列式也乘上 \(\lambda\)。 \((4)\)
- 如果矩阵中,有一行与另一行成比例,那么行列式的值为 \(0\): \((5)\)
- 证明:首先根据 \((4)\) 将 \(\lambda\) 提出来。再根据 \((1)\) 将相同的两行交换,此时矩阵应该不变,并且由 \((1)\) 行列式的值应该变号。由于 \(\det(A) = -\det(A)\),解得 \(\det(A) = 0\)。
- 将矩阵的 一行集体乘上 \(\lambda\) 的值 加到矩阵的另一行,行列式不变: \((6)\)
- 证明:由 \((3)\),可以将式子变成:
- 不难发现后面那坨东西的行列式是 \(0\)。
行列式求值
暴力:\(\mathcal{O}(n^2 \times n!)\)。
高斯消元:
由于行列式的性质 \((1), (4), (6)\),可以用高斯消元将其消成上三角矩阵:
这种特殊行列式的值是 \(\prod\limits_{i = 1}^n a_{i, i}\)。
因为考虑 \(\prod\limits_{i = 1}^n a_{i, P_i}\) 中一旦某个 \(a_{i, P_i}\) 选到 \(0\) 整个就为 \()\) 了。所以:\(P_{n} = 1 \sim (n - 1)\) 时乘积都为 \(0\),所以 \(P_{n} = n\),而 \(P_{n - 1}\) 由于 \(P_n\) 取了 \(n\) 了只能取 \(n - 1\),以此类推。对答案有贡献的排列 \(P\) 只有 \(P_i = i\) 这一个。
所以用高斯消元将其化为上三角式子后,再判断一下符号就能求出行列式的值了。
但是高斯消元会用到除法,有的时候会发现模数不是个质数,这样有很多数会找不到逆元。此时就要用辗转相除法。也就是像 \(\gcd\) 那样,不断地交换并相减,根据 \((1), (6)\),这样只会使符号改变,解决了模数(或者精度)问题。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
template <class T>
T power(T a, long long b) {
T res = 1;
for (; b; b >>= 1, a *= a) {
if (b & 1)
res *= a;
} return res;
}
template <long long mod>
class ModLL {
public:
long long n;
static long long Mod;
constexpr ModLL() : n{} {}
constexpr ModLL(long long x) : n(norm(x % getmod())) {}
constexpr long long norm(long long x) {
if (x >= getmod()) x %= getmod();
if (x <= -getmod()) x %= getmod();
if (x < 0) x += getmod();
return x;
}
constexpr long long getmod() {return (mod > 0 ? mod : Mod);}
explicit constexpr operator long long() const {return n;}
constexpr ModLL operator -() const {ModLL a; a.n = norm(getmod() - n); return a;}
constexpr ModLL inv() {assert(n != 0); return power((*this), getmod() - 2);}
constexpr ModLL &operator += (ModLL w) & {n = norm( n + w.n); return (*this);}
constexpr ModLL &operator -= (ModLL w) & {n = norm( n - w.n); return (*this);}
constexpr ModLL &operator *= (ModLL w) & {n = norm( 1LL * n * w.n % getmod()); return (*this);}
constexpr ModLL &operator /= (ModLL w) & {return (*this) *= w.inv();}
friend constexpr ModLL operator + (ModLL a, ModLL b) {ModLL res = a; res += b; return res;}
friend constexpr ModLL operator - (ModLL a, ModLL b) {ModLL res = a; res -= b; return res;}
friend constexpr ModLL operator * (ModLL a, ModLL b) {ModLL res = a; res *= b; return res;}
friend constexpr ModLL operator / (ModLL a, ModLL b) {ModLL res = a; res /= b; return res;}
friend constexpr bool operator == (ModLL a, ModLL b) {return (a.n == b.n);}
friend constexpr bool operator != (ModLL a, ModLL b) {return (a.n != b.n);}
friend constexpr istream &operator >> (istream &is, ModLL &a) {
long long x = 0; is >> x;
a = ModLL(x); return is;
}
friend constexpr ostream &operator << (ostream &os, const ModLL &a) {return (os << (a.n));}
} ;
template <int mod>
class ModInt {
public:
int n;
static int Mod;
constexpr ModInt() : n{} {}
constexpr ModInt(int x) : n(norm(x % getmod())) {}
constexpr int norm(int x) {
if (x >= getmod()) x %= getmod();
if (x <= -getmod()) x %= getmod();
if (x < 0) x += getmod();
return x;
}
constexpr static int getmod() {return (mod > 0 ? mod : Mod);}
explicit constexpr operator int() const {return n;}
constexpr ModInt operator -() const {ModInt a; a.n = norm(getmod() - n); return a;}
constexpr ModInt inv() const {assert(n != 0); return power((*this), getmod() - 2);}
constexpr ModInt &operator += (ModInt w) & {n = norm( n + w.n); return (*this);}
constexpr ModInt &operator -= (ModInt w) & {n = norm( n - w.n); return (*this);}
constexpr ModInt &operator *= (ModInt w) & {n = norm( 1LL * n * w.n % getmod()); return (*this);}
constexpr ModInt &operator /= (ModInt w) & {return (*this) *= w.inv();}
friend constexpr ModInt operator + (ModInt a, ModInt b) {ModInt res = a; res += b; return res;}
friend constexpr ModInt operator - (ModInt a, ModInt b) {ModInt res = a; res -= b; return res;}
friend constexpr ModInt operator * (ModInt a, ModInt b) {ModInt res = a; res *= b; return res;}
friend constexpr ModInt operator / (ModInt a, ModInt b) {ModInt res = a; res /= b; return res;}
friend constexpr bool operator == (ModInt a, ModInt b) {return (a.n == b.n);}
friend constexpr bool operator != (ModInt a, ModInt b) {return (a.n != b.n);}
friend constexpr istream &operator >> (istream &is, ModInt &a) {
int x = 0; is >> x;
a = ModInt(x); return is;
}
friend constexpr ostream &operator << (ostream &os, const ModInt &a) {return (os << (a.n));}
} ;
template <>
long long ModLL <0> :: Mod = (long long)(1E18) + 9;
template <>
int ModInt <0> :: Mod = 998244353;
using P = ModInt <0>;
const int N = 605;
int n;
P *a[N], _a[N][N];
P det(P **a) {
bool fh = 0; P ans = 1;
for (int i = 1; i <= n; ++i) {
for (int j = i + 1; j <= n; ++j) {
while (a[i][i].n > 0) {
int div = a[j][i].n / a[i][i].n;
for (int k = 1; k <= n; ++k)
a[j][k] -= P(div) * a[i][k];
swap(a[i], a[j]); fh ^= 1;
}
swap(a[i], a[j]); fh ^= 1;
}
}
for (int i = 1; i <= n; ++i)
ans *= a[i][i];
if (fh) ans *= P(-1);
return ans;
}
signed main(void) {
ios :: sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n; int p; cin >> p; P :: Mod = p;
for (int i = 1; i <= n; ++i) a[i] = _a[i];
for (int i = 1; i <= n; ++i) for (int j = 1; j <= n; ++j)
cin >> a[i][j];
cout << det(a) << '\n';
return 0;
}
下面引用了 Achtoria 的这篇文章。
矩阵树定理定义
- 对应无向图,定义 \(D(G)\) 为图 \(G\) 的度数矩阵,其中: $$ D(G)(i, j) = \begin{cases}deg_i & (i = j) \\0 & (i \ne j)\end{cases}$$
- 对于有向图,定义 \(D^{in}(G)\) 为 \(G\) 的入度矩阵,\(D^{out}(G)\) 为图 \(G\) 的出度矩阵,其中:$$D^{in}(G)(i, j) = \begin{cases}in_i & (i = j) \\ 0 & (i \ne j)\end{cases}, D^{out}(G)(i, j) = \begin{cases}out_i & (i = j) \\ 0 * (i \ne j)\end{cases}$$
- 定义 \(A(G)\) 为图 \(G\) 的邻接矩阵,其中:$$A(G)(i, j) = \text{the numebr of the edges from } i \text{ to } j$$
- 定义图 \(G\) 的基尔霍夫矩阵 \(K(G) = D(G) - A(G)\)。
- 定义无向图 \(G\) 的生成树数量为 \(t(G)\),有向图内向树生成树(根为 \(u\))数量为 \(t^{out}(G, u)\),有向图外向树数量为 \(t^{in}(G, u)\)。
矩阵树定理与扩展
- 经典:
给出一个无向无权图 \(G\),令 \(K'(G)\) 为 \(K(G)\) 去掉第 \(k\) 行和第 \(k\) 列(\(k\) 任意)的 \(n - 1\) 阶主子式。
\(\det(K')\) 为 \(G\) 的生成树数量。
不会证明,有兴趣可以看看上面引用的博客。 - 加权扩展:
根据乘法原理,对于某种生成树的形态,其对答案的贡献是每条边重的次数的乘积。
所以带权边 \((u, v, w)\) 相当于是建了 \(w\) 条 \((u, v)\) 这条边。注意这里度数矩阵也要边。 - 有向扩展:
有向图上也是可以对外向树或内向树计数的,这里有定理:
定义 \(K^{in}(G) = D^{in}(G) - A(G)\),\(K^{out}(G)\) 同理;
则:

浙公网安备 33010602011771号