[学习笔记] 线性基
哎,好久没有写博客了呢。。。
以下内容可能引起肠胃不适,请三思后食用。。。。。。
一些基本的线代概念:
矢量(向量):矢量(vector)是一种既有大小又有方向的量,又称为向量。一般来说,在物理学中称作矢量,例如速度、加速度、力等等就是这样的量。舍弃实际含义,就抽象为数学中的概念──向量。在计算机中,矢量图可以无限放大永不变形。
向量空间:向量空间又称线性空间,是线性代数的中心内容和基本概念之一。在解析几何里引入向量概念后,使许多问题的处理变得更为简洁和清晰,在此基础上的进一步抽象化,形成了与域相联系的向量空间概念。譬如,实系数多项式的集合在定义适当的运算后构成向量空间,在代数上处理是方便的。单变元实函数的集合在定义适当的运算后,也构成向量空间,研究此类函数向量空间的数学分支称为泛函分析。
线性组合:线性代数的基本概念之一.设α₁,α₂,…,αₑ(e≥1)是域P上线性空间V中的有限个向量.若V中向量α可以表示为:α=k₁α₁+k₂α₂+…+kₑαₑ(kₑ∈P,e=1,2,…,s),则称α是向量组α₁,α₂,…,αₑ的一个线性组合,亦称α可由向量组α₁,α₂,…,αₑ线性表示或线性表出.例如,在三维线性空间P3中,向量α=(a₁,a₂,a₃)可由向量组α₁=(1,0,0),α₂=(0,1,0),α₁=(0,0,1)线性表出:α=a₁α₁+a₂α₂+a₃α₃。
线性相关:在线性代数里,矢量空间的一组元素中,若没有矢量可用有限个其他矢量的线性组合所表示,则称为线性无关或线性独立 (linearly independent),反之称为线性相关(linearly dependent)。
基:在线性代数中,基(basis)(也称为基底)是描述、刻画向量空间的基本工具。向量空间的基是它的一个特殊的子集,基的元素称为基向量。其实基向量就是向量空间的最大线性无关组,啥意思呢?——就是说有了这个基向量,我们就可以通过它的线性组合表示出线性空间内任意一个向量。
为什么说最大线性无关组就是基呢?——很简单,如果我们可以用当前基的线性组合来表示出本属于向量空间但不属于基中的一个向量时,假如我们下次需要用到这个向量时只要用基的线性组合把它表示出来即可。
以上内容除红字外均摘自百度百科,红字是本蒟蒻自己的一些理解QAQ。
线性基在OI中的应用:
线性基这种东西,在OI中主要就应用于xor操作。比如说:告诉你几个数,要你从中任取几个,使得他们的xor值最大。
这种题目我们总不能像麻瓜一样地去枚举吧,那样的话复杂度肯定是承受不了的,这时候就要用到这种叫做线性基的神奇东西!
我们需要构造出这几个数的基,那么在刚刚的问题中向量空间显然就是那些数。
还是先上代码吧,接下来我回解释,为啥这么做就能构造出基向量。
void insert(ll x){ for (int i=63;i>=0;i--) if (x>>(ll)i&1) if (b[i]==0) {b[i]=x; return;} else x^=b[i]; } for (int i=1;i<=n;i++) scanf("%lld",&x),insert(x);
这里的b数组就是线性基的数组,其意为:用于构成基向量的最高位为 i 的数。
我们先来举个例子吧:4,5,11,9,7这四个数依次执行insert操作。
4加入:b[0-3]:0 0 4 0
5加入:b[0-3]:1 0 4 0
11加入:b[0-3]:1 0 4 11
9加入:b[0-3]:1 2 4 11
7加入(好吧,其实没加入):b[0-3]:1 2 4 11
最终得到基向量:{1,2,4,11}。
当加入4时,因为它和当前线性基中元素线性无关(也就是不能用当前线性基中的元素得到4这个值)所以就加入到了b[2]。
加入5时,因为4这个部分是线性基不需要(已经有4了嘛)所以用5^4。
接着加入11,因为线性无关,直接加入b[3]。
然后是9,虽然二进制的第3位为1,但是因为已经有11了,如果需要取8这个值我们会优先选择11,所以我们需要得到9与11不同的部分,用9^11得到2,加入b[1]。
最后是7,因为线性相关(5^11^9=7)所以就并没有啥变化。
通过这波模拟,对线性基的理解有加深吗?那我们就来小小的总结一下吧:
其实上面这段代码的工作原理就是——判断我们要加入的元素是否与线性基中的元素线性相关,相关则跳过,否则就插到对应位置。
当然在大多数情况下,我们都会选择把线性基构造成只有0/1元素(当然是因为方便啦),因为xor操作符合线性规则,所以只要左右xor一下就可以了。
代码是这样的:
void insert(int x){ for (int i=30;i>=0;i--) if (x>0&&(x&(1<<i))) if (b[i]) x^=b[i]; else{ b[i]=x; for (int j=30;j>i;j--) if (b[j]&(1<<i)) b[j]^=b[i]; for (int j=i-1;j>=0;j--) if (b[i]&(1<<j)) b[i]^=b[j]; return; } }
关于线性基的应用:
1、求最大值
把只有0/1的线性基,所有1的位对应的二进制数加起来就可以了。(所以说比较方便喽,都不用证明啥的)
2、求第k大
把所有的1的位单独拎出来,然后用按照k的二进制表示把值加上去就OK啦!代码如下:
for (int i=0;i<=63;i++) a[++cnt]=i; for (int i=cnt;i>=0;i--) if (k>>i) ans+=1<<a[i];
一些例题:
例1:albus就是要第一个登场
题目大意:给你n个数,任取几个数进行xor操作(可以不取),得到2^n个数(可以有重复),把这些数从小到大排序,求k第一次出现时的下标。
数据范围:n<=100000,所有数均小于10^9。
来源:洛谷P4896
题解:问题在于会有重复,怎么解决呢?
如果你对线性基的理解够深刻的话,很快就能意识到——每一个线性相关的元素都会使得每一个值重复的元素数量乘以2。
那么问题就已经很清楚了,设线性基中一共有cnt个1,我们只需要求出k是第几大的,然后再乘个2^(n-cnt)就可以了。
代码:
#include<bits/stdc++.h> using namespace std; int b[35]; int n,k,cnt,tot,x,now,p; void insert(int x){ for (int i=30;i>=0;i--) if (x>0&&(x&(1<<i))) if (b[i]) x^=b[i]; else{ b[i]=x; for (int j=30;j>i;j--) if (b[j]&(1<<i)) b[j]^=b[i]; for (int j=i-1;j>=0;j--) if (b[i]&(1<<j)) b[i]^=b[j]; return; } } int sqr(int x){ return x*x%p; } int power(int x,int y){ if (!y) return 1; if (y&1) return sqr(power(x,y/2))*x%p; else return sqr(power(x,y/2)); } int main(){ scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%d",&x),insert(x); scanf("%d",&k); p=10086; for (int i=0;i<=30;i++) if (b[i]){ if (k&(1<<i)) now+=1<<tot; tot++; } long long ans=(now%p*power(2,n-tot)+1)%p; printf("%lld\n",ans); return 0; }
例2:幸运数字
题目大意:一颗带点权的树,求树上两点间异或值最大子集的异或值。
来源:SCOI 2016,洛谷 P3292。
题解:在倍增的同时,记下与f[u][i](倍增数组)对应的base[u][i],base[u][i]意为从点u往上跳2^i这些节点能够构造的线性基。
那么我们应该如何来合并两个线性基呢?——很简单,在一个基的基础上,把另一个基中那些非0位的数在再插入这个基就可以了。
因为第二个基那些非0位的数可以表示出整个空间,所以另外那些都是可以用这些东西的线性组合表示出来的,我们只要做最多63次插入操作即可。
注意:不要漏判起点和终点相同的情况。(我就是被他坑了QAQ)
代码:
#include<bits/stdc++.h> #define ll long long using namespace std; const int N=20005; struct node{ ll b[65]; } base[N][16]; int head[N],nxt[N<<1],vet[N<<1],depth[N],f[N][16],x,y,n,m,tot; ll a[N],ans; void add(int x,int y){ nxt[++tot]=head[x]; vet[tot]=y; head[x]=tot; } node insert(node a,ll x){ node res=a; for (int i=63;i>=0;i--) if (x>>i) if (res.b[i]) x^=res.b[i]; else {res.b[i]=x; break;} return res; } node merge(node a,node b){ node res=a; for (int i=63;i>=0;i--) if (b.b[i]) res=insert(res,b.b[i]); return res; } void dfs(int u,int father,int dep){ depth[u]=dep,f[u][0]=father; base[u][0]=insert(base[u][0],a[u]); base[u][0]=insert(base[u][0],a[father]); for (int i=1;i<=15;i++){ f[u][i]=f[f[u][i-1]][i-1]; base[u][i]=merge(base[u][i-1],base[f[u][i-1]][i-1]); } for (int i=head[u];i;i=nxt[i]){ int v=vet[i]; if (v==father) continue; dfs(v,u,dep+1); } } node lca(int x,int y){ if (depth[y]<depth[x]) swap(x,y); node res; for (int i=0;i<=63;i++) res.b[i]=0; for (int i=15;i>=0;i--) if (depth[x]<=depth[f[y][i]]) res=merge(res,base[y][i]),y=f[y][i]; if (x==y) {res=insert(res,a[x]);return res;} for (int i=15;i>=0;i--) if (f[x][i]!=f[y][i]){ res=merge(res,base[x][i]),res=merge(res,base[y][i]); x=f[x][i],y=f[y][i]; } res=merge(res,base[x][0]),res=merge(res,base[y][0]); return res; } int main(){ scanf("%d %d",&n,&m); for (int i=1;i<=n;i++) scanf("%lld",&a[i]); for (int i=1;i<n;i++) scanf("%d %d",&x,&y),add(x,y),add(y,x); dfs(1,0,1); for (int i=1;i<=m;i++){ scanf("%d %d",&x,&y); node b=lca(x,y); ans=0; for (int j=63;j>=0;j--) if ((ans^b.b[j])>ans) ans^=b.b[j]; printf("%lld\n",ans); } return 0; }