1. 程式人生 > 其它 >【筆記】線性基

【筆記】線性基

來自\(\texttt{SharpnessV}\)省選複習計劃中的矩陣/線性基


P3390 【模板】矩陣快速冪

模板。

矩陣乘法是\(N^3\)的,快速冪是\(\log T\),總的時間複雜度為\(\rm O(N^3\log T)\)

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 105
#define P 1000000007
using namespace std;
int n;long long k;
struct node{
	int a[N][N];
	node(){memset(a,0,sizeof(a));}
	void init(){rep(i,1,n)a[i][i]=1;}
};
node operator*(node x,node y){
	node now;
	rep(i,1,n)rep(j,1,n)rep(w,1,n)
		now.a[i][j]=(now.a[i][j]+1LL*x.a[i][w]*y.a[w][j])%P;
	return now;
}
node operator^(node x,long long y){
	node now;now.init();
	for(;y;y>>=1,x=x*x)if(y&1)now=now*x;
	return now;
}
int main(){
	scanf("%d%lld",&n,&k);
	node cur;
	rep(i,1,n)rep(j,1,n)scanf("%d",&cur.a[i][j]);
	cur=cur^k;
	rep(i,1,n){rep(j,1,n)printf("%d ",cur.a[i][j]);putchar('\n');}
	return 0;
}

P1939 【模板】矩陣加速(數列)

定義初始矩陣。

\[A=\begin{bmatrix}f_1&f_2&f_3\end{bmatrix} \]

定義轉移矩陣。

\[B=\begin{bmatrix}0&0&1\\1&0&0\\1&0&1\end{bmatrix} \]\[A\times B^{n-1}=\begin{bmatrix}f_n&f_{n+1}&f_{n+2}\end{bmatrix} \]

最後矩陣快速冪即可。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define P 1000000007LL
#define N 3
typedef long long ll;
using namespace std;
struct mat{
	int a[N][N];
	mat(){memset(a,0,sizeof(a));}
	void init(){
		rep(i,0,2)a[i][i]=1;
	}
	void build(){
		a[0][0]=a[0][1]=a[1][2]=a[2][0]=1;
	}
};
mat operator*(const mat x,const mat y){
	mat now;
	rep(i,0,2)rep(j,0,2)rep(k,0,2)
		now.a[i][j]=1LL*(now.a[i][j]+1LL*x.a[i][k]*y.a[k][j])%P;
	return now;
}
mat operator^(mat x,int y){
	mat now;now.init();
	for(;y;y>>=1,x=x*x)if(y&1)now=now*x;
	return now;
}
int main(){
	int T;scanf("%d",&T);
	while(T--){
		int n;scanf("%d",&n);
		if(n<=3){puts("1");continue;}
		mat now;now.build();now=now^(n-3);
		printf("%lld\n",((now.a[0][0]+now.a[1][0])%P+now.a[2][0])%P);
	}
	return 0;
}

P1962 斐波那契數列

P1349 廣義斐波那契數列

P5343 【XR-1】分塊

矩陣遞推模板,留白。

P5337 [TJOI2019]甲苯先生的字串

矩陣還可以用來解決一類圖上模型。

我們給定鄰接矩陣\(A\),令\(B=A^n\),則\(B_{i,j}\)表示節點 \(i\) 經過 \(n\) 步到達 \(i\) 的方案數。

這道題相鄰兩個字母可以看成字母 \(i\) 到字母 \(j\) 的轉移,經過 \(\rm Len-1\) 步轉移。直接套用模板即可。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 27
#define P 1000000007
using namespace std;
long long n;
struct node{
	int a[N][N];
	node(){memset(a,0,sizeof(a));}
	void init(){
		rep(i,0,25)a[i][i]=1;
	}
	node operator*(node o){
		node now;
		rep(i,0,25)rep(j,0,25)rep(k,0,25)
			now.a[i][j]=(now.a[i][j]+1LL*a[i][k]*o.a[k][j])%P;
		return now;
	}
	node operator^(long long y){
		node x=*this,now;now.init();
		for(;y;y>>=1,x=x*x)if(y&1)now=now*x;
		return now;
	}
}w;
char s[1<<20];
int main(){
	scanf("%lld",&n);
	rep(i,0,25)rep(j,0,25)w.a[i][j]=1;
	scanf("%s",s+1);int m=strlen(s+1);
	rep(i,1,m-1)w.a[s[i]-'a'][s[i+1]-'a']=0;
	w=w^(n-1);int ans=0;
	rep(i,0,25)rep(j,0,25)ans=(ans+w.a[i][j])%P;
	printf("%d\n",ans);
	return 0;
}

P6772 [NOI2020] 美食家

廣義矩陣乘法,希望不會有人再因為基礎科技不會而簽到題丟分。

這裡我們定義矩陣乘法 \(C_{i,j}=\max\limits_{k}\{A_{i,k}+ B_{k,j}\}\) 。不難證明這仍然滿足結合律。

\[dp_{k}=dp_{k-1}\times G \]

我們仍然使用矩陣快速冪優化,一個小技巧是預處理矩陣的\(2^k\)次冪。

程式碼


【模板】高斯消元法

基礎科技。先消成上三角矩陣,然後回代即可,時間複雜度\(\rm O(N^3)\)

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 105
using namespace std;
int n;double a[N][N];
void solve(){
	rep(i,1,n){
		double cur=a[i][i];
		if(!a[i][i]){puts("No Solution");return;}
		rep(j,i,n+1)a[i][j]/=cur;
		rep(j,i+1,n)pre(k,n+1,i)a[j][k]-=a[j][i]*a[i][k];
	}
	pre(i,n,1)rep(j,1,i-1)a[j][n+1]-=a[j][i]*a[i][n+1];
	rep(i,1,n)printf("%.2lf\n",a[i][n+1]);
}
int main(){
	scanf("%d",&n);
	rep(i,1,n)rep(j,1,n+1)scanf("%lf",&a[i][j]);
	solve();
	return 0;
} 

【模板】矩陣求逆

一個沒多大實際作用的演算法,掛這裡。


P4035 [JSOI2008]球形空間產生器

高斯消元。我們將球心每一維的座標當作未知數,可以得到 \(n+1\)\(n+1\) 元的二次方程。

方程之間差分,可以得到 \(n\) 元線性方程,高斯消元即可。

程式碼


【模板】線性基

線性基用於解決一類異或問題。

\(N\)個數中選出若干個數,使得他們的異或和最大。如果是取出兩個或三個數,我們還可以通過使用 \(\rm Trie\) 樹獲得最優的複雜度,但任意取顯然是做不到的。

線性基基於這樣一個事實,就是 \(N\) 個數任意選的方案數是\(2^N\),如果最高位數是\(k\),則異或和的取值方案不超過 \(2^k\)。如果 \(N\) 大於 \(k\),我們可以保留 \(k\) 個最具代表性的數代替 \(N\) 個數。

這就是線性基的核心,用若干個最高位不同的關鍵數替換原來的\(N\)個數,使得在求異或和的情況下等價。

線性基是動態的,可以支援插入操作,每次我們拆入一個數,找到第一個沒有取值的位 \(i\) ,將第 \(i\) 的取值置為當前數。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 66
using namespace std;
int n;long long x,a[N];
int main(){
	scanf("%d",&n);
	rep(i,1,n){
		scanf("%lld",&x);
		pre(i,50,0)if((x>>i)&1){
			if(!a[i]){a[i]=x;break;} 
			x^=a[i];
		}
	}
	long long ans=0;
	pre(i,50,0)if(!((ans>>i)&1))ans^=a[i];
	printf("%lld\n",ans);
	return 0;
}

P3857 [TJOI2008]彩燈

因為線性基中的 \(k\) 個數等價於原 \(N\) 個數,且線性基中的數異或和互不相同,所以答案就是 \(2^k\)

程式碼

P4570 [BJWC2011]元素

貪心。

如果存在若干個數 \(\rm a_1\ xor\ a_2\ \cdots xor\ a_n=0\),則一定要去除且僅其中的一個數,顯然去除的數權值越小越好。

所以我們將所有數按照從大到小排序,如果能加入線性線性基就選擇當前數,否則就把它丟棄。

程式碼

P4301 [CQOI2013] 新Nim遊戲

第一回合只用將石子取到剩下的石子無論再取多少堆異或和都\(\neq 0\)就贏了。

轉換一下就是留下最多的石子使得子集異或和不為 \(0\)。所以本質和上面是同一題。

P3292 [SCOI2016]幸運數字

線性基支援動態加入,當然可以支援樹上倍增。

程式碼


P4151 [WC2011]最大XOR和路徑

我們可以任取一條\(1\to N\)的路徑,然後發現如果要改變路徑,則需要異或一個環的異或和。

我們把所有環的權值插入線性基,然後查詢異或最大值即可。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 50005
using namespace std;
typedef long long ll;
int h[N],tot;
struct edge{
	int to,nxt;ll val;
}e[N<<2];
void add(int x,int y,ll z){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].val=z;
} 
int n,m;bool v[N];
queue<int>q;ll p[63],d[N];
void ins(ll x){
	pre(i,60,0){
		if(!x)return;
		if(!((x>>i)&1))continue;
		if(!p[i]){p[i]=x;return;}
		x^=p[i];
	}
}
void dfs(int x,ll val){
	d[x]=val;v[x]=1;
	for(int i=h[x];i;i=e[i].nxt)
		if(!v[e[i].to])dfs(e[i].to,val^e[i].val);
		else ins(d[e[i].to]^d[x]^e[i].val);
}
ll ask(ll x){
	ll ans=x;
	pre(i,60,0)if(!((ans>>i)&1))ans^=p[i];
	return ans;
}
int main(){
	scanf("%d%d",&n,&m);
	rep(i,1,m){
		int x,y;ll z;
		scanf("%d%d%lld",&x,&y,&z);
		add(x,y,z);add(y,x,z);
	}
	dfs(1,0);printf("%lld\n",ask(d[n]));
	return 0;
}