【筆記】線性基
來自\(\texttt{SharpnessV}\)的省選複習計劃中的矩陣/線性基。
模板。
矩陣乘法是\(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; }
定義初始矩陣。
\[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; }
矩陣遞推模板,留白。
矩陣還可以用來解決一類圖上模型。
我們給定鄰接矩陣\(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; }
廣義矩陣乘法,希望不會有人再因為基礎科技不會而簽到題丟分。
這裡我們定義矩陣乘法 \(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;
}
一個沒多大實際作用的演算法,掛這裡。
高斯消元。我們將球心每一維的座標當作未知數,可以得到 \(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;
}
因為線性基中的 \(k\) 個數等價於原 \(N\) 個數,且線性基中的數異或和互不相同,所以答案就是 \(2^k\) 。
貪心。
如果存在若干個數 \(\rm a_1\ xor\ a_2\ \cdots xor\ a_n=0\),則一定要去除且僅其中的一個數,顯然去除的數權值越小越好。
所以我們將所有數按照從大到小排序,如果能加入線性線性基就選擇當前數,否則就把它丟棄。
第一回合只用將石子取到剩下的石子無論再取多少堆異或和都\(\neq 0\)就贏了。
轉換一下就是留下最多的石子使得子集異或和不為 \(0\)。所以本質和上面是同一題。
線性基支援動態加入,當然可以支援樹上倍增。
我們可以任取一條\(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;
}