线性代数
行列式
定义
定义 \(n(n\ge 1)\) 阶行列式
的值为 \(\sum\limits_{p} (-1)^{\tau(p)}\prod\limits_{i=1}^n a_{i, p_i}\) 其中 \(p\) 为 \(1\sim n\) 的全排列,\(\tau(p)\) 为 \(p\) 的逆序对个数。我们用 \(\det(D)\) 或者 \(|D|\) 表示行列式 \(D\) 的值。
我们称 \(n\) 阶行列式
的转置为
我们把行列式 \(D\) 的转置记作 \(D^T\)。
性质
性质 1:对于上三角行列式 \(\begin{vmatrix}a_{1, 1} & a_{1, 2} & \cdots & a_{1, n} \\0 & a_{2, 2} & \cdots & a_{2, n} \\\vdots & \vdots & \ddots & \vdots \\0 & 0 & \cdots & a_{n, n} \\\end{vmatrix}\),其值为 \(\prod\limits_{i=1}^n a_{i, i}\)。
性质 2:对于任意的行列式 \(D\),有 \(\det(D)=\det(D^T)\)。
性质 3:互换行列式的两行(列),行列式变号。
推论 3.1:若行列式有两行(列)相同,则行列式的值为 \(0\)。
性质 4:用 \(k(k\ne0)\) 乘行列式的某一行(列)中所有元素,等于用 \(k\) 乘此行列式。
推论 4.1:行列式中某一行(列)的公因子可以提到行列式符号外面。
推论 4.2:若行列式有两行(列)成比例,则行列式的值为 \(0\)。
性质5:若存在行列式 \(A\),\(B\),\(C\),满足 \(C_{i, j} = A_{i, j} + B_{i, j}\),那么 \(\det(C) = \det(A) + \det(B)\)。
推论 5.1:行列式的某一行(列)的所有元素乘以同一数 \(k\) 后再加到另一行(列)对应的元素上去,行列式的值不变。
计算
按照行列式的定义,我们可以暴力穷举全排列,从而得到行列式的值。时间复杂度 \(\mathcal{O}(n\times n!)\),难以接受。
于是我们可以通过上述性质将任意一个行列式转化为一个上三角行列式。
例:求行列式
的值。
根据性质 5.1,将第一行的值分别乘上 \(1, -2, -1\),并分别加到第 2,3,4 行上去,得到:
接着,由于第二行第二个元素的值为 \(0\),无法直接使用消数,所以我们根据性质 3,交换行列式的第 2 和第 4 行,得到:
然后根据性质 5.1,将第二行的值分别乘上 \(-2, 0\),加到第 3,4 行上去,得到:
重复上述过程,最后得到的行列式为:
根据性质 1,答案就是 \(-(1\times 1\times (-10) \times 4.5)=45\)。
例题:P7112 【模板】行列式求值
本质上就是上述过程,但是注意到取模数不保证是质数,所以我们可以用类似辗转相除的方法进行消数。具体地,对于两个元素 \(a\) 和 \(b\),我们可以将较大数对较小数取模,直到某个数变为 \(0\)。总时间复杂度看似是 \(\mathcal{O}(n^3\log p)\),但是实际上,由于每个数最多被除 \(\log p\) 次,所以总时间复杂度是 \(\mathcal{O}(n^2(n+\log p))\)。
#include<iostream>
#define int long long
using namespace std;
int a[605][605];
signed main() {
int n, p;
scanf("%lld%lld", &n, &p);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
scanf("%lld", &a[i][j]);
}
}
int f = 1;
for(int i = 1; i <= n; i++) {
for(int j = i + 1; j <= n; j++) {
while(a[i][i]) {
int d = a[j][i] / a[i][i];
for(int k = i; k <= n; k++) {
a[j][k] = (a[j][k] - a[i][k] * d % p + p) % p;
swap(a[i][k], a[j][k]);
}
f = -f;
}
swap(a[i], a[j]); f = -f;
}
}
long long ans = 1;
for(int i = 1; i <= n; i++) {
ans = ans * a[i][i] % p;
}
printf("%lld", (f * ans + p) % p);
return 0;
}
高斯消元
已知 \(n\) 元线性一次方程组。
求该方程组的解,无解输出 -1
,无穷解输出 0
。
我们使用一个简单的三元一次方程组来举例:
我们可以用系数矩阵的形式来描述这个方程组:
为了求解该方程组,我们把方程右面的得数加到该矩阵的最后一列,我们把这个矩阵称为增广矩阵:
我们可以采用消元法来解决这个问题,即把该矩阵化为如下形式的矩阵,此时右面的系数 \(a,b,c\) 即为 \(x,y,z\) 的解:
我们成如下三种形式的变换为基本变换,经过这些基本变换后,方程的解不会发生改变:
变换 1:将矩阵的第 \(i\) 行与第 \(j\) 列互换。
变换 2:将矩阵的第 \(i\) 行的所有数加 \(k\)。
变化 3:将矩阵的第 \(i\) 行的所有数乘 \(k\)。
使用上面的三种基本变换,我们可以把任意一个增广矩阵消元成解集形式,具体做法如下:
依然拿上面提到的方程组举例,把 \(x\) 看作主元,将第一行的所有元素乘上 \(-5\) 加到第二行上,得到:
接着把 \(y\) 看作主元,分别乘上 \(0.5, 1\),分别加到第一、三行上去,得到:
最后,把 \(z\) 看作主元,分别乘上 \(\dfrac{10}{17},-\dfrac{6}{17}\),分别加到第二、三行上去,得到:
于是我们得到了该方程组的解:
那么方程组有无穷解或无解的情况怎么判断呢?在消到某一行的时候,发现这一列上的元素为 \(0\),无法继续消元。这时我们需要继续向下扫描,直到遇到某一行的这一列的值不为 \(0\),就可以把这两行互换,继续进行消元。如果从这一行开始直到最后,所有这一列上的元素都为 \(0\),只能跳过这一列,进行同行下一列的消元。最后,如果遇到某一行出现了 \(0x=0\) 的情况,就产生了无穷解;如果遇到了 \(0x\ne 0\) 的情况,那么该方程无解。注意,要先判无解,再判无穷解。
例题 1:P3389 【模板】高斯消元法
板子题,直接模拟上述过程即可。
#include<iostream>
#include<cmath>
using namespace std;
const double eps = 1e-9;
double a[105][105];
int main() {
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n + 1; j++) {
scanf("%lf", &a[i][j]);
}
}
for(int i = 1; i <= n; i++) {
if(fabs(a[i][i]) <= eps) {
for(int j = i + 1; j <= n; j++) {
if(fabs(a[j][i]) > eps) {swap(a[i], a[j]); break;}
}
}
if(fabs(a[i][i]) <= eps) return puts("No Solution") & 0;
for(int j = n + 1; j >= i; j--) {
a[i][j] /= a[i][i];
}
for(int j = 1; j <= n; j++) {
if(i == j) continue;
double d = a[j][i] / a[i][i];
for(int k = i; k <= n + 1; k++) {
a[j][k] -= a[i][k] * d;
}
}
}
for(int i = 1; i <= n; i++) {
printf("%.2lf\n", a[i][n + 1]);
}
return 0;
}
例题 2:P2455 [SDOI2006]线性方程组
依然是板子题,注意无解和无穷解的判断。
#include<iostream>
#include<cmath>
using namespace std;
const double eps = 1e-9;
double a[55][55];
int main() {
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n + 1; j++) {
scanf("%lf", &a[i][j]);
}
}
int cur = 1; // cur 表示当前行
for(int i = 1; i <= n; i++) {
if(fabs(a[cur][i]) <= eps) {
for(int j = cur + 1; j <= n; j++) {
if(fabs(a[j][i]) > eps) {swap(a[cur], a[j]); break;}
}
}
if(fabs(a[cur][i]) <= eps) continue;
for(int j = n + 1; j >= i; j--) {
a[cur][j] /= a[cur][i];
}
for(int j = 1; j <= n; j++) {
if(cur == j) continue;
double d = a[j][i] / a[cur][i];
for(int k = i; k <= n + 1; k++) {
a[j][k] -= a[cur][k] * d;
}
}
cur++;
}
if(cur <= n) {
for(int i = cur; i <= n; i++) {
if(fabs(a[i][n + 1]) > eps) return puts("-1") & 0;
}
puts("0");
return 0;
}
for(int i = 1; i <= n; i++) {
printf("x%d=%.2lf\n", i, a[i][n + 1]);
}
return 0;
}