【笔记】矩阵/线性基
来自\(\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;
}
定义初始矩阵。
定义转移矩阵。
最后矩阵快速幂即可。
#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}\}\) 。不难证明这仍然满足结合律。
我们仍然使用矩阵快速幂优化,一个小技巧是预处理矩阵的\(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;
}