线性代数相关
0. 行列式
0.1. 符号与定义
0.2. 基本性质
-
Lemma 0.2.1. 对于一个上三角矩阵,它的行列式等于主对角线所有值的乘积。
Proof. 根据行列式定义,要使
, 只能取 ;要使 , 只能取 或 ,而 已经等于 所以 只能等于 …… 以此类推,得到唯一的排列 ,此时 。 -
Lemma 0.2.2. 单位矩阵的行列式为
。即 。根据
以及 Lemma 0.2.1. 易证。 -
Lemma 0.2.3. 交换矩阵两行,行列式变号。
Proof. 交换排列的两个数会让排列逆序对数量改变奇偶性,因此相当于为每个
乘上了 。根据乘法分配律将 提出。 -
Lemma 0.2.4. 将矩阵某一行乘上常数,行列式乘上相同常数。
Proof. 不妨设
,记变换得到的矩阵为 。因为 且 则 。根据乘法分配律将 提出。 -
Lemma 0.2.5. 若矩阵有相同的两行,则行列式为
。Proof. 交换相同的两行会让行列式变号,但矩阵保持不变。即
,得到 。 -
Lemma 0.2.6. 若矩阵有两行存在倍数关系,则行列式为
。结合 Lamma 0.2.4. 与 0.2.5. 易证。
-
Lemma 0.2.7. 若两个矩阵至多有一行不等,则将这不等的一行相加得到的新矩阵的行列式等于原矩阵行列式之和。
Proof. 不妨设不等的行编号为
,则 。根据乘法分配律得 ,即 。 -
Lemma 0.2.8. 将矩阵的某一行加上另一行的倍数,行列式不变。
Proof. 不妨设
,令操作后的 矩阵为 。令 ,则 。根据 Lemma 0.2.6. 可知 。根据 Lemma 0.2.7 可知 ,即 。
1. 高斯消元
1.1. 算法介绍
高斯消元用来求解形如
的
高斯消元的过程如下:
- 记已经处理好的方程个数为
。枚举每一个未知数 作为主元。 - 找到
使得 最大。交换 与 两行。 - 若
,则 要么无解,要么有无数解。回到第一步枚举下一个主元,即 。 - 将
中的每个数除以 。注意需要预先存储 或倒序枚举第二维。该操作后 。 - 将
消为 。具体来说,将 减去 。因为 ,故枚举 并将 减去 。
最终我们得到一个上三角矩阵,其中第
如果最终
此时显然有
接下来只需要将每个解回带即可。
- 显然
已经是 的解。 - 枚举从大到小枚举行数
,只需要将所有 带入即可。注意此时 已经是 的解,所以只需要用 减去 即为 的解。
时间复杂度
下面给出
int gauss(){ // 返回方程自由元的个数
int r=0,c=0;
for(;c<n;c++){
int cur=r;
for(int i=r;i<n;i++)if(fabs(a[i][c])>fabs(a[cur][c]))cur=i;
if(fabs(a[cur][c])<eps)continue;
swap(a[cur],a[r]);
for(int i=n;i>=c;i--)a[r][i]/=a[r][c];
for(int i=r+1;i<n;i++)if(fabs(a[i][c])>eps)
for(int j=n;j>=c;j--)a[i][j]-=a[r][j]*a[i][c];
r++;
}
if(r<n){
for(int i=r;i<n;i++)if(fabs(a[i][n])>eps)return -1;
return n-r;
}
for(int i=n-2;~i;i--)for(int j=i+1;j<n;j++)
a[i][n]-=a[j][n]*a[i][j];
return 0;
}
1.2. 应用:矩阵求逆
给出
将
注意点:此时枚举列下标时要从
时间复杂度
1.3. 应用:求行列式
根据定义直接求的时间复杂度为
根据 Lemma 0.2.3(
1.4. 例题
I. P3389 【模板】高斯消元法 - 洛谷
板子题。
#include <bits/stdc++.h>
using namespace std;
#define gc getchar()
#define pb push_back
inline int read(){
int x=0; char s=gc;
while(!isdigit(s))s=gc;
while(isdigit(s))x=(x<<1)+(x<<3)+s-'0',s=gc;
return x;
}
const int N=105;
const double eps=1e-8;
int n;
double a[N][N];
int gauss(){
int r=0,c=0;
for(;c<n;c++){
int cur=r;
for(int i=r;i<n;i++)if(fabs(a[i][c])<fabs(a[cur][c]))cur=i;
if(fabs(a[cur][c])<eps)continue;
swap(a[cur],a[r]);
for(int i=n;i>=c;i--)a[r][i]/=a[r][r];
for(int i=r+1;i<n;i++)if(fabs(a[i][c])>eps)
for(int j=n;j>=c;j--)a[i][j]-=a[r][j]*a[i][c];
r++;
}
if(r<n){
for(int i=r;i<n;i++)if(fabs(a[i][n])>eps)return -1;
return n-r;
}
for(int i=n-2;~i;i--)for(int j=i+1;j<n;j++)
a[i][n]-=a[j][n]*a[i][j];
return 0;
}
int main(){
cin>>n;
for(int i=0;i<n;i++)for(int j=0;j<=n;j++)cin>>a[i][j];
if(gauss()!=0)puts("No Solution"),exit(0);
for(int i=0;i<n;i++)printf("%.2lf\n",a[i][n]);
return 0;
}
II. P2455 [SDOI2006]线性方程组
仍然是板子题。
#include <bits/stdc++.h>
using namespace std;
#define gc getchar()
#define pb push_back
inline int read(){
int x=0; char s=gc;
while(!isdigit(s))s=gc;
while(isdigit(s))x=(x<<1)+(x<<3)+s-'0',s=gc;
return x;
}
const int N=105;
const double eps=1e-8;
int n;
double a[N][N];
int gauss(){
int r=0,c=0;
for(;c<n;c++){
int cur=r;
for(int i=r;i<n;i++)if(fabs(a[i][c])>fabs(a[cur][c]))cur=i;
if(fabs(a[cur][c])<eps)continue;
swap(a[cur],a[r]);
for(int i=n;i>=c;i--)a[r][i]/=a[r][c];
for(int i=r+1;i<n;i++)if(fabs(a[i][c])>eps)
for(int j=n;j>=c;j--)a[i][j]-=a[r][j]*a[i][c];
r++;
}
if(r<n){
for(int i=r;i<n;i++)if(fabs(a[i][n])>eps)return -1;
return 0;
}
for(int i=n-2;~i;i--)for(int j=i+1;j<n;j++)
a[i][n]-=a[j][n]*a[i][j];
return 1;
}
int main(){
cin>>n;
for(int i=0;i<n;i++)for(int j=0;j<=n;j++)cin>>a[i][j];
int c=gauss();
if(c!=1)cout<<c<<endl;
else for(int i=0;i<n;i++)printf("x%d=%.2lf\n",i+1,a[i][n]);
return 0;
}
III. P4783 【模板】矩阵求逆
板子题。具体算法见 6.2.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=4e2+5;
const int mod=1e9+7;
ll ksm(ll a,ll b){
ll s=1;
while(b){
if(b&1)s=s*a%mod;
a=a*a%mod,b>>=1;
}
return s;
}
ll inv(ll x){return ksm(x,mod-2);}
ll n,a[N][N],b[N][N];
void gauss(){
for(int i=1;i<=n;i++){
int p=-1;
for(int j=i;j<=n;j++)if(a[j][i]){p=j; break;}
if(p==-1)puts("No Solution"),exit(0);
swap(a[i],a[p]),swap(b[i],b[p]);
ll iv=inv(a[i][i]);
for(int j=1;j<=n;j++)a[i][j]=a[i][j]*iv%mod,b[i][j]=b[i][j]*iv%mod;
for(int j=i+1;j<=n;j++){
ll c=a[j][i];
for(int k=n;k;k--)
b[j][k]=(b[j][k]-c*b[i][k]%mod+mod)%mod,
a[j][k]=(a[j][k]-c*a[i][k]%mod+mod)%mod;
}
}
for(int i=n-1;i;i--)
for(int j=i+1;j<=n;j++)
for(int k=1;k<=n;k++)
b[i][k]=(b[i][k]-a[i][j]*b[j][k]%mod+mod)%mod;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>a[i][j],b[i][j]=i==j;
gauss();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cout<<b[i][j]<<(j==n?'\n':' ');
return 0;
}
IV. P7112 【模板】行列式求值
注意到给出的模数
如果直接让两行辗转相除,就不需要用到精确的除法也可以进行消元。时间复杂度看似
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=600+5;
ll n,p,a[N][N],sym,prod=1;
int main(){
cin>>n>>p;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>a[i][j],a[i][j]%=p;
for(int i=1;i<n;i++){
for(int j=i+1;j<=n;j++){
while(a[i][i]){
ll q=a[j][i]/a[i][i];
for(int k=i;k<=n;k++)a[j][k]=(a[j][k]-q*a[i][k]%p+p)%p;
swap(a[i],a[j]),sym^=1;
} swap(a[i],a[j]),sym^=1;
}
}
for(int i=1;i<=n;i++)prod=(prod*a[i][i])%p;
cout<<(sym?p-prod:prod)%p<<endl;
return 0;
}
2. Matrix-Tree 定理
2.1. 算法介绍
前置知识:高斯消元。
记
可以有重边和自环。可以发现自环对
证明略。
2.2. 扩展:有向图生成树个数
既然是有向图那么生成树就有根。不妨设根为
记
2.3. 扩展:边权积的和
对于一条
2.4. 扩展:边权和的和
考虑将加法转化为乘法。不难想到将边权看做形如
乘法:
除法:
2.5. 例题
I. P6178 【模板】Matrix-Tree 定理
板子题。时间复杂度
#include <bits/stdc++.h>
using namespace std;
#define gc getchar()
#define ll long long
inline int read(){
int x=0,sign=0; char s=gc;
while(!isdigit(s))sign|=s=='-',s=gc;
while(isdigit(s))x=(x<<1)+(x<<3)+(s-'0'),s=gc;
return sign?-x:x;
}
const int N=300+5;
const int mod=1e9+7;
ll ksm(ll a,ll b){
ll s=1;
while(b){
if(b&1)s=s*a%mod;
a=a*a%mod,b>>=1;
} return s;
}
ll n,m,t,a[N][N];
void add(ll &x,ll y){x=(x+y+mod)%mod;}
ll det(){
ll sgn=0,prod=1;
for(int i=2;i<n;i++){
for(int j=i+1;j<=n;j++)if(a[j][i]&&!a[i][i])swap(a[i],a[j]),sgn^1;
ll inv=ksm(a[i][i],mod-2);
for(int j=i+1;j<=n;j++){
ll tmp=inv*a[j][i]%mod;
for(int k=i;k<=n;k++)add(a[j][k],-tmp*a[i][k]%mod);
}
}
for(int i=2;i<=n;i++)prod=(prod*a[i][i])%mod;
return (sgn?mod-prod:prod)%mod;
}
int main(){
cin>>n>>m>>t;
for(int i=1;i<=m;i++){
int u=read(),v=read(),w=read();
add(a[u][v],-w),add(a[v][v],w);
if(!t)add(a[v][u],-w),add(a[u][u],w);
}
cout<<det()<<endl;
return 0;
}
*II. P3317 [SDOI2014]重建
不那么套路的 Matrix-Tree。
首先对于一个生成树它的权值应该是
一步比较巧妙的转化:
如果出现
#include <bits/stdc++.h>
using namespace std;
const int N=50+5;
const double eps=1e-8;
int n;
double ans=1,a[N][N];
double det(){
for(int i=2;i<n;i++){
int pos=i;
for(int j=i+1;j<=n;j++)if(fabs(a[j][i])>eps)pos=j;
swap(a[i],a[pos]);
if(fabs(a[i][i])<eps)return 0;
for(int j=i+1;j<=n;j++)
for(int k=n;k>=i;k--)
a[j][k]-=a[j][i]/a[i][i]*a[i][k];
}
for(int i=2;i<=n;i++)ans*=a[i][i];
return fabs(ans);
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
cin>>a[i][j];
if(i!=j){
if(1-a[i][j]<eps)a[i][j]-=eps;
if(i<j)ans*=(1-a[i][j]);
a[i][j]=-a[i][j]/(1-a[i][j]);
}
}
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(i!=j)a[i][i]-=a[i][j];
printf("%.5lf\n",det());
return 0;
}
III. P4111 [HEOI2015]小 Z 的房间
对相邻的两个 .
连一条边,则答案为最终连出来的无向图的生成树个数。时间复杂度
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=85;
const int mod=1e9;
ll n,m,cnt,a[N][N],buc[N][N];
char s[N][N];
void add(int u,int v){a[u][u]++,a[v][v]++,a[u][v]--,a[v][u]--;}
int gauss(){
ll sgn=0,ans=1;
for(int i=2;i<=cnt;i++)
for(int j=i+1;j<=cnt;j++){
while(a[i][i]){
ll q=a[j][i]/a[i][i];
for(int k=i;k<=n*m;k++)a[j][k]=(a[j][k]-q*a[i][k]%mod+mod)%mod;
swap(a[i],a[j]),sgn^=1;
}
swap(a[i],a[j]),sgn^=1;
}
for(int i=2;i<=cnt;i++)ans=ans*a[i][i]%mod;
return (sgn?mod-ans:ans)%mod;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)cin>>s[i][j],buc[i][j]=cnt+=s[i][j]=='.';
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(s[i][j]=='.'){
if(s[i+1][j]=='.')add(buc[i][j],buc[i+1][j]);
if(s[i][j+1]=='.')add(buc[i][j],buc[i][j+1]);
}
cout<<gauss()<<endl;
return 0;
}
*IV. P6624 [省选联考 2020 A 卷] 作业题
从大到小枚举
总时间复杂度
注意到连接某个点的所有边的不同约数个数至多为
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pll pair <ll,ll>
#define fi first
#define se second
const int N=30+5;
const int mod=998244353;
const int W=152502;
ll ksm(ll a,ll b){
ll s=1;
while(b){
if(b&1)s=s*a%mod;
b>>=1,a=a*a%mod;
}
return s;
}
pll operator + (pll a,pll b){return {(a.fi+b.fi)%mod,(a.se+b.se)%mod};}
pll operator - (pll a,pll b){return {(a.fi-b.fi%mod+mod)%mod,(a.se-b.se%mod+mod)%mod};}
pll operator * (pll a,pll b){return {a.fi*b.fi%mod,(a.fi*b.se+a.se*b.fi)%mod};}
pll operator / (pll a,pll b){
ll inv=ksm(b.fi,mod-2);
return {a.fi*inv%mod,(a.se*b.fi-a.fi*b.se%mod+mod)%mod*inv%mod*inv%mod};
}
ll n,m,cnt,d[W],sum[W],ans[W],e[N][N];
pll a[N][N];
ll solve(int x){
bool sgn=0; pll ans={1,0};
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
a[i][j]={0,0};
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(e[i][j]&&e[i][j]%x==0)
a[i][i]=a[i][i]-(a[i][j]={mod-1,mod-e[i][j]});
for(int i=2;i<n;i++){
for(int j=i+1;j<=n;j++)
if(a[i][j].fi){
swap(a[i],a[j]),sgn^=1;
break;
}
for(int j=i+1;j<=n;j++){
pll inv=a[j][i]/a[i][i];
for(int k=i;k<=n;k++)a[j][k]=a[j][k]-inv*a[i][k];
}
}
for(int i=2;i<=n;i++)ans=ans*a[i][i],assert(a[i][i].fi>=0&&a[i][i].se>=0);
return (sgn?mod-ans.se:ans.se)%mod;
}
int main(){
for(int i=1;i<W;i++)for(int j=i;j<W;j+=i)d[j]++;
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v,w; cin>>u>>v>>w;
e[u][v]=e[v][u]=w,sum[u]+=d[w],sum[v]+=d[w];
}
int id=1;
for(int i=2;i<=n;i++)if(sum[i]<sum[id])id=i;
for(int i=W-1;i;i--){
bool ok=0;
for(int j=1;j<=n;j++)ok|=e[id][j]&&e[id][j]%i==0;
if(ok){
ans[i]=solve(i);
for(int j=i+i;j<W;j+=i)ans[i]=(ans[i]-ans[j]+mod)%mod;
cnt=(cnt+i*ans[i])%mod;
}
}
cout<<cnt<<endl;
return 0;
}
V. P4336 [SHOI2016]黑暗前的幻想乡
感动,终于有一道自己想出来的 Matrix-Tree 定理题目了。
时间复杂度
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define mem(x,v) memset(x,v,sizeof(x))
#define pii pair <int,int>
#define fi first
#define se second
const int N=17;
const int mod=1e9+7;
ll ksm(ll a,ll b){
ll s=1;
while(b){
if(b&1)s=s*a%mod;
b>>=1,a=a*a%mod;
} return s;
}
int n;
vector <pii> e[N];
ll a[N][N],ans;
ll solve(int x){
mem(a,0);
for(int i=0;i<n-1;i++)
if((x>>i)&1)
for(pii it:e[i])
a[it.fi][it.se]=--a[it.se][it.fi];
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)
if(i!=j)
a[i][i]-=a[i][j];
if(a[i][i]==0)return 0;
}
ll sgn=0,ans=1;
for(int i=2;i<=n;i++){
for(int j=i+1;j<=n;j++)
if(a[j][i]){
swap(a[i],a[j]),sgn^=1;
break;
}
ll inv=ksm(a[i][i],mod-2);
for(int j=i+1;j<=n;j++)
for(int k=n;k>=i;k--)
a[j][k]=(a[j][k]-a[j][i]*inv%mod*a[i][k]%mod+mod)%mod;
}
for(int i=2;i<=n;i++)ans=ans*(mod+a[i][i])%mod;
return (sgn?mod-ans:ans)%mod;
}
int main(){
cin>>n;
for(int i=0;i<n-1;i++){
int m,u,v; cin>>m;
while(m--)cin>>u>>v,e[i].push_back({u,v});
}
for(int i=1;i<(1<<n-1);i++){
int cnt=0;
for(int j=0;j<n-1;j++)cnt+=(i>>j)&1;
if((n-1-cnt)&1)ans=(ans-solve(i)+mod)%mod;
else ans=(ans+solve(i))%mod;
}
cout<<ans<<endl;
return 0;
}
VI. P4455 [CQOI2018]社交网络
板子题。边的节点颠倒一下就是求以
#include <bits/stdc++.h>
using namespace std;
const int N=255;
const int mod=1e4+7;
int ksm(int a,int b){
int s=1;
while(b){
if(b&1)s=s*a%mod;
b>>=1,a=a*a%mod;
} return s;
}
int n,m,sgn,ans=1,a[N][N];
int main(){
cin>>n>>m;
for(int i=1,u,v;i<=m;i++)cin>>u>>v,a[v][u]=-1;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
a[j][j]-=(i!=j)*a[i][j];
for(int i=2;i<=n;i++){
for(int j=i+1;j<=n;j++)
if(a[j][i]){
swap(a[i],a[j]),sgn^=1;
break;
}
int inv=ksm(a[i][i],mod-2);
for(int j=i+1;j<=n;j++)
for(int k=n;k>=i;k--)
a[j][k]=(a[j][k]-a[j][i]*inv%mod*a[i][k]%mod+mod)%mod;
}
for(int i=2;i<=n;i++)ans=ans*(a[i][i]+mod)%mod;
cout<<(sgn?mod-ans:ans)%mod<<endl;
return 0;
}
*VII. CF917D Stranger Trees
nb tea!
类似求边权和的和,如果我们将给出树中的边权看做
显然对这个多项式矩阵做矩阵树定理是不现实的。但是假设最终得到的多项式为
一共要插
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=100+5;
const ll mod=1e9+7;
ll ksm(ll a,ll b){
ll s=1; while(b){
if(b&1)s=s*a%mod;
b>>=1,a=a*a%mod;
} return s;
}
ll n,e[N][N],a[N][N],c[N][N];
void solve(ll x){
memset(a,0,sizeof(a));
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(i!=j){
if(e[i][j])a[i][j]=-x;
else a[i][j]=-1;
a[i][i]-=a[i][j];
}
ll sgn=0,ans=1;
for(int i=2;i<=n;i++){
for(int j=i+1;j<=n;j++)
if(a[j][i]){
swap(a[i],a[j]),sgn^=1;
break;
}
ll inv=ksm(a[i][i],mod-2);
for(int j=i+1;j<=n;j++)
for(int k=n;k>=i;k--)
a[j][k]=(a[j][k]-a[j][i]*inv%mod*a[i][k])%mod;
}
for(int i=2;i<=n;i++)ans=ans*(a[i][i]+mod)%mod;
c[x][n+1]=(sgn?mod-ans:ans)%mod;
for(ll i=1,j=1;i<=n;i++)c[x][i]=j,j=j*x%mod;
}
void gauss(){
memcpy(a,c,sizeof(c));
for(int i=1;i<n;i++){
for(int j=i+1;j<=n;j++)
if(a[j][i]){
swap(a[i],a[j]);
break;
}
ll inv=ksm(a[i][i],mod-2);
for(int j=i+1;j<=n;j++)
for(int k=n+1;k>=i;k--)
a[j][k]=(a[j][k]-a[j][i]*inv%mod*a[i][k])%mod;
}
for(int i=n-1;i;i--)
for(int j=i+1;j<=n;j++)
a[i][n+1]=(a[i][n+1]-a[i][j]*a[j][n+1]%mod*ksm(a[j][j],mod-2))%mod;
}
int main(){
cin>>n;
for(int i=1,u,v;i<n;i++)cin>>u>>v,e[u][v]=e[v][u]=1;
for(int i=1;i<=n;i++)solve(i);
gauss();
for(int i=1;i<=n;i++)cout<<(a[i][n+1]*ksm(a[i][i],mod-2)%mod+mod)%mod<<" ";
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现