行列式学习笔记
定义
\(n\) 阶矩阵
的行列式为:
其中 \(p\) 取遍所有 \(n\) 阶排列。\(\tau(p)\) 定义为排列 \(p\) 的逆序对数目。
性质
结论
- 交换两行,行列式结果乘以 \(-1\)。
- 将一行乘以某个数加到另一行上时,行列式结果不变。
- 将某一行乘以 \(k\),则行列式的值会乘以 \(k\)。
- \(|A\times B|=|A||B|\)
- 矩阵转置(列变成行,行变成列),行列式不变。
证明
以后再说。
应用
【模板】行列式求值
定义 \(n\) 阶矩阵 \(D\) 中的一个元素 \(a_{i,j}\) 的代数余子式 \(A_{i,j}\) 为 \((-1)^{i+j}|M_{i,j}|\),其中 \(M_{i,j}\) 是从 \(D\) 中抽掉第 \(i\) 行和第 \(j\) 列后得到的矩阵(\(|M_{i,j}|\) 被称为余子式)。则有如下结论:
如果我们将最后一行中的数运用性质消成只剩最后一个数非零(即 \(i<n,a_{n,i}=0,a_{n,n}\ne 0\)),容易发现 \(|D|=a_{n,n}A_{n,n}\)。而 \(M_{n,n}\) 是一个 \(n-1\) 阶矩阵,于是可以递归求解。由于每次取的都是 \(A_{i,i}=(-1)^{2i}|M_{i,i}|=|M_{i,i}|\),所以不用特殊处理。
消成最后一行只有最后一个数非零的方法是把每一列(设为第 \(i\) 列)都加上最后一列乘以 \(-\dfrac{a_{n,i}}{a_{n,n}}\) 的值。
最后会得到一个上三角矩阵或者下三角矩阵,答案就是 \(\prod_{i=1}^na_{i,i}\)。
下面的实现是照着第一篇题解写的。这种做法运用了辗转相减的技巧。意思是我们本来要直接 \(a_{j,k}\gets a_{j,k}-da_{i,k}\),然后达到使得 \(a_{j,i}=0\) 的目的,但是直接这么做会导致精度或者逆元不存在的问题(这道题中是后者),所以我们一次只让两列变得尽量小,不断操作达到消元的目的。
ll ret=1;
for(int i=1,d;i<=n;i++){// 求解 n 次,每次求解 n-i+1 阶的子矩阵
for(int j=i+1;j<=n;j++){// 逐行消元,使得 (j,i) 变成 0,得到下三角矩阵
while(a[i][i]){// 辗转相减
d=a[j][i]/a[i][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],a[j]);ret=-ret;//交换两列行列式的值要乘以 -1
}
swap(a[i],a[j]);ret=-ret;
}
}
for(int i=1;i<=n;i++)ret=(ret*a[i][i])%P;
return (ret%P+P)%P;
[NOI 2021] 路径交点
首先考虑 \(k=2\) 的情况。我们用一个二元组 \((x,y)\) 表示一条连接第一层第 \(x\) 个点和第二层第 \(y\) 个点的路径。
考虑每一个 \((x_1,y_1)\),对于后面的一条路径 \((x_2,y_2)\),是否与其有交点(“后面的” 意为 \(x_1<x_2\))。显然只有 \(y_1>y_2\) 的时候有交点。然后发现这和逆序对很像,所以如果两层之间每一个点之间都有边,要求的就是一个 \(n\) 阶排列逆序对为偶数的情况减去逆序对为奇数的情况。
于是发现这又和行列式求值中的 \((-1)^{\tau(p)}\) 很像,我们可以把两层之间的连边情况写成一个 \(n\) 阶矩阵。在行列式求值的式子中,枚举排列代表的就是第一层的点向哪一个点连边。显然我们要求如果出现不合法的情况后面那个式子就是 \(0\),否则就是 \(1\),于是就是邻接矩阵。
然后考虑 \(k>2\) 的情况。手玩一下会发现一个神奇的性质:仍然用一个排列 \(p\) 表示一条路径,其中起点为 \(i\) 的路径终点为 \(p_i\),那么两条路径 \(i<j\) 只要有奇数个交点,必然有 \(p_i>p_j\);只要有偶数个交点,必然有 \(p_i<p_j\),与中途经过了那些点无关。证明也比较显然。
考虑构造这个矩阵,使 \(\prod_{i=1}^na_{i,p_i}\) 能够表示选择这种排列的方案数。发现由于路径要求不相交好像构造不出来?考虑问题的整体性,即使路径相交,将两个相交的路径的终点交换,会使逆序对的奇偶性发生变化,而同样的相交情况仍然会被枚举到,因此直接考虑所有方案就是对的(如果这里不能理解,可以画图思考或者去看题解中更详细的解释)。所以用 \(a_{i,j}\) 表示起点为 \(i\),终点为 \(j\) 的路径的条数。这个矩阵可以用所有相邻两层的邻接矩阵按顺序相乘得到。容易发现最后的矩阵是 \(n_1\times n_k\) 的,可以进行行列式求值。
这道题也可以用 LGV 引理做,但是我不会。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=205,P=998244353;
ll t[N][N];
inline ll solve(int n){
ll d,ret=1;bool x=0;
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
while(t[i][i]){
d=t[j][i]/t[i][i];
for(int k=i;k<=n;k++){
t[j][k]=(t[j][k]-t[i][k]*d%P+P)%P;
}
swap(t[i],t[j]);x^=1;
}
swap(t[i],t[j]);x^=1;
}
}
for(int i=1;i<=n;i++)ret=ret*t[i][i]%P;
return x?-ret:ret;
}
inline void ct(){
int K,n[N>>1],m[N>>1];
ll e[N][N],s[N][N];
memset(t,0,sizeof(t));
for(int i=1;i<N;i++)t[i][i]=1;
scanf("%d",&K);
for(int i=1;i<=K;i++)scanf("%d",n+i);
for(int i=1;i<K;i++)scanf("%d",m+i);
for(int i=1,u,v;i<K;i++){
memset(e,0,sizeof(e));
memset(s,0,sizeof(s));
for(int j=1;j<=m[i];j++){
scanf("%d%d",&u,&v);e[u][v]=1;
}
for(int i1=1;i1<=n[1];i1++)for(int i2=1;i2<=n[i+1];i2++){
for(int k=1;k<=n[i];k++)s[i1][i2]=(s[i1][i2]+t[i1][k]*e[k][i2]%P)%P;
}
memcpy(t,s,sizeof(t));
}
printf("%lld\n",(solve(n[1])%P+P)%P);
}
int main(){
int T;scanf("%d",&T);
while(T--)ct();
return 0;
}

浙公网安备 33010602011771号