1行列式與基本概念
1.1一些概念與性質
1.1.1概念
- 逆序對( \(\tau\) ):設 A 為一個有 n 個數字的有序集 (n>1),其中所有數字各不相同。
如果存在正整數 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],則 <A[i], A[j]> 這個有序對稱為 A 的一個逆序對。 - 排列:一般地,從n個不同元素中取出m(m≤n)個元素,按照一定的順序排成一列,叫做從n個元素中取出m個元素的一個排列。特別地,當m=n時,這個排列被稱作全排列,這個全排列被稱作n階排列。
如無特殊說明,一下排列都指全排列。
- 對換:交換排列的任意兩個數,得到另一組排列。這個操作叫做對換。
- 如果一個排列的逆序對數為奇數,則稱這個排列為奇排列,如果為偶數,則稱這個排列為偶排列。
1.1.2性質
-
對換改變排列的奇偶性。
證明:設排列\(a_i(1{\le}i{\le}n)\),易得如果交換兩個相鄰得數\(a_i,a_{i-1}\)則滿足上述性質,因為除了\(a_i,a_{i-1}\)的之間逆序對數改變\(a_i\)左邊的數和\(a_{i+1}\) 右邊的數對這兩個數的逆序對數不改變。如果交換的是兩個不相鄰的數,即\(a_1,a_2,...a_{i-1},a_i,a_{i+1},...a_{j-1},a_{j},a_{j+1}...a_n\)現在要交換\(a_i\)和\(a_j\),那么可以看做是先把\(a_i\)做j-i次相鄰的對換得到\(a_1,a_2,...a_{i-1},a_{i+1},...a_{j-1},a_{j},a_i,a_{j+1}...a_n\),注意\(a_i\)的位置變換,在這個基礎上,在進行j-i-1次相鄰對換,得到\(a_1,a_2,...a_{i-1},a_{j},a_{i+1},...a_{j-1},a_i,a_{j+1}...a_n\)。這樣,總共進行了2i-2j-1次對換,因為i、j為整數,那么2i-2j-1肯定為奇數,奇數次改變排列的奇偶性,則對換肯定改變排列的奇偶性。
-
在全部n階排列中,奇、偶排列各占一半。
證明:對於一個奇排列,交換\(a_1,a_2\)一定能夠得到一個對應的偶排列,而對於一個偶排列,可以通過相同的方式得到一個奇排列,所以奇排列與偶排列是一一對應的。故得證。 -
任意一個排列可以經過n次對換變為自然排列(n階自然排列為\(1,2,3,4,5...n\)),且n與原排列奇偶性相同。
證明:首先前半句話肯定是對的,否則所有基於對換的排序算法都是錯的。考慮每一次對換都改變排列的奇偶性,並且自然排列是一個偶排列(逆序對數為0),所以n一定與原排列奇偶性相同,這是顯然的。
1.2行列式定義、概念和定理
1.2.1定義
N階行列式由\(N^2\)個數\(a_{ij}(1{\le}i,j{\le},n)\)組成。
1.2.2行列式的值
求和公式:
這里sgn函數為如果該排列為偶,則返回1,否則則返回-1。即\(sgn(j1j2j3{\cdots}jn)=(-1)^{\tau(j_1,j_2,...j_n)}\)
j是一個1至n的排列
上面這個求和公式用普通話翻譯過來就是,枚舉j的所有全排列,用a去乘,把所得結果相加,實質是從每一行,每一列中都取出一個數來,相乘,把枚舉所得到的所有結果相加,就是最終答案。
1.2.3定理
1.行列互換,值不變。
證明,個人認為這個是顯然的。行列互換的意思是
變成
本質是以為枚舉全排列還是那幾個數。
2.用一個數乘行列式的某行等於用次數乘此行列式。即
注意這里可以不是第2行,可以把k乘到任意行上去。
證明:
根據求和公式:
在兩邊同時乘上k可以得到
利用乘法分配律,我們把k乘進去那么做加法的每一項都將會有且只有一個k,這與行列式
進行求和的答案是一樣的,因為求和的本質是在每一行每一列取一個數,這樣取的話,求和每一項都有且只有一個k。
3.如果行列式中某一行等於兩個行列式之和,則這個行列式等於兩個行列式之和,這兩個行列式分別以這兩組數為改行,其余各行相同。什么意思?我們畫圖來理解一下:
有行列式
(這里還是以第2行為例,其實可以是任意行),如果有\(b_{2j}+c_{2j}=a_{2j}(1{\le}j{\le}n)\) 那么就有
證明:把這三個行列式的求和公式寫一下,會發現對右邊可以用乘法分配律,這里不再贅述。
4.對換行列式兩行位置,行列式反號。
證明:設第i行我們取的數為\(j_i\)那么我們假設隨着行列式兩行交換,所有我們枚舉的全排列對應的\(j_i,j_k\) 這兩個數也互換,易知,交換后的全排列沒有重復,但所有的全排列奇偶性全部取反,由行列式計算公式可以得到,行列式反號。
5.如果行列式中有兩行成比例,則行列式等於0。
證明:設這個行列式第i行與第j行成比例,則有\(a_{ik}=u*{a_{jk}} (1{\le}k{\le}n)\),那么根據性質2,可以把這個u提出來,提出來后,這個行列式有相等的兩行。設這個行列式的值為A,我們交換這兩個相等的兩行,根據定理4,交換后行列式的值為-A,但是又因為交換前后兩個行列式完全等同,所以有A=-A,由此可得A=0。
6.把一行乘以某數加到另一行,行列式的值不變。
加完后的這個行列式,根據定理3,可以把它看作原行列式,和另一行列式A的和,由定理5得A=0,所以得證。
這是行列式的重要定理,下面我們用這些定理講一講高斯消元。
1.3高斯消元
本質上高斯消元是是我們計算行列式的一種方法。如果我們用計算公式來求行列式的話,很明顯該算法復雜度可以達到\(O(n!*n)\)(n!枚舉全排列,而n來計算)
如果這個行列式的從左上到右下的對角線一下全部為0,即
這個行列式的值如何計算?注意,任何有0參與的乘法結果都將是0,所以在選數計算的過程中不用考慮選0的情況,只需要考慮不選0的情況,又根據每行每列只取一個數的原則,我們只有一種方案可以選,即對角線,所以這個行列式的值就應該等於\(\prod\limits_{i=1}^na_{ii}\)。所以高斯消元所做的工作就是把一個行列式轉化成上述行列式的形式。
如何轉化?用行列式定義6。
考慮用\(a_{11}\)去消第一列,我們要做的就是先算個比值,即\(\frac{a_{21}}{a_{11}}\)用第二行的每一個數,都減去第一行同一列的數乘上這個比值,就可以在把\(s_{21}\)消成0的同時,保證行列式的值不變。由此用\(a_{11}\)去消第一列,再用\(a_{22}\)去消第二列,最終可以得到我們想要的行列式,再把對角線相乘即可得到行列式的值。
1.3.1代碼1
#define dd double
#define N 101
dd z[N][N];int n;
inline dd guass(){
dd x=1.0;
for(int a=1;a<=n;a++){
for(int b=a+1;b<=n;b++){
dd d=z[b][a]/z[a][a];
for(int c=1;c<=n;c++) z[b][c]-=k*z[a][c];
}
}
for(int i=1;i<=n;i++) x*=z[i][i];
return x;
}
通過這個代碼我們其實可以知道這個算法的時間復雜度為\(o(n^3)\)
但其實這個算法還有點小問題,就是z[a][a]可能原本就是個0,除以一個0這個程序就無法運行。我們利用定理4,把一個此位置非0的一行對換上來,行列式值取反。同時,如果這一列全都是0,那m這個行列式的值就肯定是0了,所以我們在兌換前加一步處理0,同時判斷一列全為0的情況。即:(眾所周知,實數中的0位1e-8)
#define dd double
#define N 101
dd a[N][N];int n;
inline dd guass(){
dd x=1;
for(int a=1;a<=n;a++){
for(int b=a;b<=n;b++)
if(fabs(z[b][a])>1e-8){//
if(b==a) break;
x=-x;//
for(int c=1;c<=n;c++) swap(z[a][c],z[b][c]);
break;//
}
if(fabs(z[a][a])<=1e-8) return 0.0;//
for(int b=a+1;b<=n;b++){
dd k=z[b][a]/z[a][a];
for(int c=1;c<=n;c++) z[b][c]-=k*z[a][c];
}
}
for(int i=1;i<=n;i++) x*=z[i][i];
return x;
}
時間復雜度為\(o(n^3)\)
這其中加了“//”的都是容易忘的,尤其是fabs這個實數取絕對值函數,一定不要忘。(還有定理四中行列式值取反也不要忘)
但是如果你要拿這個模板去過洛谷的高斯消元是過不去的,其原因在於,這個的精度不准的太嚴重了,上面這個代碼,在競賽中也是不會用的。試想一下,如果一個全部是整數的行列式,其值也一定是整數,但是我們用這個算法算出來的值卻一定是一個實數。怎樣優化?回答這個問題之前,先回答下面這個問題。
1.3.2優化
考慮一個實數1.12341214235341...,對他進行兩個操作,乘100000000,和除以100000000,都存入一個double里,那個結果精度更高?
答案可能會讓你大吃一驚————除以100000000的精度會更高。
原因:
double能存的下的數是一定的,也就是說,double的空間是一定的,對於乘100000000來說,他的空間有一部分給了整數部分,而對於除以10000000來說,整數部分就是個0,它大部分的空間都給了小數部分,所以除以100000000的精度會更高。也就是說,除以的這個數,絕對值越大,對精度的貢獻也就越高。
對於上面的程序,我們的精度消耗在哪里了呢?顯然:dd k=z[b][a]/z[a][a];
這行代碼消耗的精度更多。我們總希望z[a][a]的絕對值越大。所以我們就想辦法把絕對值越大的弄到上面來,於是我們就從a開始,從上往下掃一遍讓z[a][a]這個地方的絕對值最大,同時如果取了最大絕對之后,這個位置上的值仍然為0,那么說明這一列都是0。這種算法叫做主元高斯消元法。代碼:
#define dd double
#define N 101
dd z[N][N];int n;
inline dd guass(){
dd x=1;
for(int a=1;a<=n;a++){
for(int b=a+1;b<=n;b++)
if(fabs(z[b][a])>fabs(z[a][a]){//
x=-x;//
for(int c=1;c<=n;c++) swap(z[a][c],z[b][c]);
}
if(fabs(z[a][a])<=1e-8) return 0.0;//
for(int b=a+1;b<=n;b++){
dd k=z[b][a]/z[a][a];
for(int c=1;c<=n;c++) z[b][c]-=k*z[a][c];
}
}
for(int i=1;i<=n;i++) x*=z[i][i];
return x;
}
這個代碼可以過掉洛谷上的高斯消元模板。
但實際上,這個代碼並沒有解決掉精度問題,為了一勞永逸的解決精度問題,我們引出輾轉相消高斯消元。
1.3.3輾轉相消高斯消元
我們參考一下輾轉相除求最大公因數的思路。通過gcd(a,b)=gcd(b,a%b)后,知道a能整除b,也就是a%b等於0,如果我們類似這個過程,對\(a_{ii}\)和\(a_{i+1i}\)進行輾轉相消,以達到使\(a_{i+1i}\)等於0的目的,因為參與運算的都是整數,所以就不存在精度問題。代碼:
int z[N][N];int n;
inline int guass(){
int x=1;
for(int a=1;a<=n;a++)
for(int b=a+1;b<=n;b++)
while(z[b][a]!=0){
int k=z[a][a]/z[b][a];
for(int c=1;c<=n;c++) z[a][c]-=z[b][c]*k;
for(int c=1;c<=n;c++) swap(z[a][c],z[b][c]);
x=-x;
}
for(int i=1;i<=n;i++) x*=z[i][i];
return x;
}
都是重災區。。。一定注意輾轉相消完全模擬了輾轉相除的過程,一定是用第b行取消第a行,while下前兩行相當於取模,后一行交換,在此過程中注意保證行列式的值不變。
這里有一個while,那么這個算法的復雜度是多少呢?事實上,因為int的計算要比double快很多,尤其是除法,所以這個算法消耗的時間和前兩個差不多。實際上多了一個進行n個數取最大公約數的過程。bfor和while是對n個數輾轉相除的過程,復雜度\(O(n+log_n)\),afor和cfor是\(O(n^2)\) 所以總體的復雜度為\(O(n^2*(n+log_n))=O(n^3+n^2log_n)=O(n^3)\)
1.3.4例題
https://www.luogu.com.cn/problem/P3389
別看是個藍題,其實很簡單,像我這種蒟蒻都會。
對於一個n階行列式來說,這個數據多了一列,但是沒關系,我們就帶着這一列去計算,我們最終想要的行列式應該是這樣的:
你會發現\(a_{nn+1}\)為數b,\(a_{nn}\)是未知數\(x_n\)的系數,由此我們可以算出\(x_n\)的值,再把它的值往回代,算出其它未知數的值即可得出答案。
代碼:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 102
#define M number
using namespace std;
int n;
dd z[N][N],ans[N];
inline void print(){
for(int i=1;i<=n;i++){
for(int j=1;j<=n+1;j++) printf("%0.2lf ",z[i][j]);
printf("\n");
}
printf("\n");
}
inline bool guass(){
for(int a=1;a<=n;a++){
for(int b=a+1;b<=n;b++)
if(fabs(z[b][a])>fabs(z[a][a]))
for(int c=1;c<=n+1;c++) swap(z[a][c],z[b][c]);
if(fabs(z[a][a])<=1e-8) return 0;
for(int b=a+1;b<=n;b++){
dd k=z[b][a]/z[a][a];
for(int c=1;c<=n+1;c++) z[b][c]-=z[a][c]*k;
}
// print();
}
return 1;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=n+1;j++)
scanf("%lf",&z[i][j]);
if(!guass()){
printf("No Solution");
return 0;
}
for(int i=n;i>=1;i--){//n-i ans i+1->n
dd x=0.0;
for(int j=i+1;j<=n;j++){
x+=ans[j]*z[i][j];
}
ans[i]=(z[i][n+1]-x)/z[i][i];
}
for(int i=1;i<=n;i++){
printf("%0.2lf\n",ans[i]);
}
return 0;
}
2矩陣
在數學中,矩陣(Matrix)是一個按照長方陣列排列的復數或實數集合。(n*m)
aaaa還有矩陣,真是不想寫。。。
2.1特殊矩陣
1.零矩陣:所有元素均為0
2.對角矩陣:除了主對角線,其余元素均為0(n*n)
3.單位矩陣:主對角線元素均為1的對角矩陣。
2.2普通計算與關系
相等:每個位置的元素對應相等,大小相等。
加法:大小相同的兩個矩陣,對應位置相加得到的新矩陣。
減法:由加法的逆運算可得到。
數量乘法:一個數乘一個矩陣等於這個數乘上這個矩陣的每一個元素所形成的的新矩陣。
2.3矩陣乘法:
若有矩陣A、B、C,由AB=C,則A的大小為nm,B的大小為mk,C的大小就為nk,也就是說,兩個能相乘的矩陣的大小關系是有關聯的,這里的這個例子可以很明顯的表示出來。
如何計算呢,這里給出計算規則:\({C_{ij}}={\sum}A_{ik}*B_{kj}{(1{\le}k{\le}m)}\)
很難用語言表達出來,\(C_{ij}\)這個位置等於A矩陣中第i行與B中第j列對應元素相乘再相加得到。
2.4矩陣乘法用途:
2.4.1矩陣加速遞推。
例題https://www.luogu.com.cn/problem/P1939
原理:我們首先要構造出一個矩陣。1.確定矩陣大小。2.設未知數列方程。3.根據遞推式求解方程。4.矩陣快速冪求解答案
這是做這個題的基本思路。
代碼:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 4
#define M number
using namespace std;
const ll mod=1e9+7;
int t;
struct matrix{
int n,m;
ll a[N][N];
inline matrix(){
n=m=0;
memset(a,0,sizeof(a));
}
inline void dwh(){
m=n;
for(int i=1;i<=n;i++) a[i][i]=1;
}
inline void out(){
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++) printf("%d ",a[i][j]);
printf("\n");
}
}
};
inline matrix operator * (const matrix &a,const matrix &b){
matrix c;
c.n=a.n;c.m=a.m;
for(int i=1;i<=c.n;i++)
for(int j=1;j<=c.m;j++)
for(int k=1;k<=a.m;k++){
c.a[i][j]+=(a.a[i][k]*b.a[k][j])%mod;
c.a[i][j]%=mod;
}
return c;
}
inline void ksm(matrix &a,ll b){
matrix c;
c.n=a.n;
c.dwh();
while(b){
if(b&1) c=c*a;
// c.out();
a=a*a;
b>>=1;
}
a=c;
}
int main(){
scanf("%d",&t);
while(t--){
matrix a;a.n=1;a.m=3;a.a[1][1]=a.a[1][2]=a.a[1][3]=1;
matrix b;b.n=b.m=3;b.a[1][1]=b.a[1][2]=b.a[2][3]=b.a[3][1]=1;
ll n;scanf("%lld",&n);
ksm(b,n-1);
b=b*a;
printf("%d\n",b.a[1][1]);
}
return 0;
}
相同思路的題還有:https://www.luogu.com.cn/problem/P1962
代碼:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 3
#define M number
using namespace std;
const ll mod=1e9+7;
struct matrix{
int n,m;
ll a[N][N];
inline matrix(){
n=m=0;
memset(a,0,sizeof(a));
}
inline void dwh(const matrix b){
m=n=b.n;
for(int i=1;i<=n;i++) a[i][i]=1;
}
inline void out(){
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++) printf("%d ",a[i][j]);
printf("\n");
}
}
};
inline matrix operator * (const matrix &a,const matrix &b){
matrix c;
c.n=a.n;c.m=b.m;
for(int i=1;i<=c.n;i++)
for(int j=1;j<=c.m;j++)
for(int k=1;k<=a.m;k++){
c.a[i][j]+=(a.a[i][k]*b.a[k][j])%mod;
c.a[i][j]%=mod;
}
return c;
}
inline void ksm(matrix &a,ll b){
matrix c;c.dwh(a);
while(b){
if(b&1) c=c*a;
// c.out();
a=a*a;
b>>=1;
}
a=c;
}
int main(){
ll n;scanf("%lld",&n);
if(n==1||n==2){
printf("1");
return 0;
}
matrix a;
a.n=1;a.m=2;a.a[1][1]=a.a[1][2]=1;
matrix b;
b.n=b.m=2;
b.a[1][2]=b.a[2][1]=b.a[2][2]=1;
ksm(b,n-1);
// b.out();
b=a*b;
printf("%lld",b.a[1][1]);
return 0;
}
2.4.2矩陣加速DP
凡是符合\(f_{ij}={\sum}f_{i-1k}*M_{kj}\)形式的DP方程都可以用矩陣優化。
把\(f_i\)看做是一個1行m列的矩陣,則有f\(f_i=f{i-1}*M\) 故可以用矩陣優化。