【总结】线性基(高斯消元 线性代数)- BZOJ2115: [Wc2011] Xor

    在多日线性基的洗脑中,本蒟蒻决定写一篇关于线性基的总结。


文章目录

    • 引入
    • 线代意义
    • 与高斯消元
    • 实际应用


引入

线性基是一种特殊的基,它通常会在异或运算中出现,它的意义是:通过原集合S的某一个最小子集S1使得S1内元素相互异或得到的值域与原集合S相互异或得到的值域相同。

    以上摘自百度百科。
    这概括出了线性基的性质,它实际上是对于某种特殊性质的特殊解决的算法。本蒟蒻是在看到一道异或相关的题,类似于洛谷-【模板】线性基,可任意选择这个条件使得trie树+主席树的方法废掉了,于是此类问题,就交给线性基来解决。
    思考一下就会发现,异或不同于其他的运算方式,在二进制表示下,位与位之间是不相关的,也就是说,某一位上的改变并不会对任何其他位产生影响。这也是下文将提到的与线性代数之间的联系了。
    首先高亮一篇博文,这一定是__我看过__(虽然也没看过几篇,所以就不要反驳啦)写的最好的关于线性基的博文:线性基学习笔记-Sengxian。(%%%希望本蒟蒻有一天也可以写出这样的学习笔记)


线代意义

    “基”本身即线性代数中的一个概念:

  • 基(也称为基底)是描述、刻画向量空间的基本工具。
  • 向量空间的基是它的一个特殊的子集,基的元素称为基向量。
  • 向量空间中任意一个元素,都可以唯一地表示成基向量的线性组合。
  • 如果基中元素个数有限,就称向量空间为有限维向量空间,将元素的个数称作向量空间的维数。

    以上摘自百度百科。
    通过基的概念我们可以联想到对于某几个数异或的集合是否也可以用一组基(基底)来表示,类似于二维平面中用某两个不同向的单位向量来表示。
    再引入两个概念,解释一下为什么可以用基底表示向量空间:

  • 线性无关(线性独立):指一组向量中__任意__一个向量都不能由其它几个向量线性表示。
  • 线性组合:设α₁,α₂,…,αₑ(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₃α₃.

    以上摘自百度百科。
    同样,对于二进制下的数(以下默认讨论int范围( 2 31 − 1 2^{31} -1 2311)内的整形非负数),我们可以看作是一个有30个维度的向量空间,则需要一组(30个)基底来表示,而这30个基底一定能够表示出集合内的任意异或值(可以自己思考一下)。


与高斯消元

    既然线性基的产生是由线代出发,那么我们同样可以从矩阵方程的角度来进行阐述。
    这里简单讲一下异或方程组(经典的开关灯问题),此类方程满足以下规则:

  • 方程系数均为0或1
  • 多元且均为0或1
  • 等式右侧常数项均为0或1
  • 除系数与元间是相乘关系(实际上仅代表有无)外,所有计算均为异或的形式

    而 n n n个数的异或和集合同样可以列出一组 n n n元一次异或方程组(然而仅表示等式右侧),而等式左侧的不同取值(对应二进制的不同位,即对于每一位都列一个方程,该位上为0的数系数即为0,反之则为1)即为这 n n n个数异或和集合的基底的线性组合的集合。
    而这个方程组,我们也可以先利用高斯消元消掉某些元,这里很关键:消掉的元实际上就是在除去该数后仍有某几个数异或后能够表示出该数。所以在消到剩下的行都线性无关(矩形的秩)后,系数仍不为零的数即为该集合的基底,也就是所说的“线性基”。


实际应用

    我们接下来从线性基的实际意义(对解决问题的贡献)来看下面这道模板:
    给定n个整数(数字可能重复),求在这些数中选取任意个,使得他们的异或和最大。
    数据范围: 1 ≤ n ≤ 50 , 0 ≤ S i ≤ 2 50 1\leq n\leq 50,0\leq S_i\leq 2^{50} 1n50,0Si250
    说了是道模板,不解释直接贴代码。

#include
using namespace std;
typedef long long ll;
int n;
ll now,ans,p[51]; 
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%lld",&now);
        for(int j=50;j>=0;j--){
            if((now>>j)&1){
                if(!p[j]){p[j]=now;}
                now^=p[j];
            }
        }
    }
    for(int j=50;j>=0;j--){
        if(ans<(ans^p[j])){ans^=p[j];}
    }
    printf("%lld\n",ans);
    return 0;
}

    然而本蒟蒻仔细一想,发现事情并不简单,果然某神犇抛出了这样一个升级版的模板:
    将查询改为在某区间上进行,即在 l i , r i l_i,r_i li,ri区间内查询,其余条件同上。
    然而本蒟蒻的线段树T的飞起。
    事实上该神犇考验了本蒟蒻对于线性基本质的理解。妙妙的解法如下,我们可以先把询问离线下来,然后枚举左端或右端,不断加入到线性基中,若某个基底超过了该次询问的区间,就舍去,因为不断加入的过程中,如果找到新的可以在这个维上做基底(即系数不为0)的数,我们就用这个数去更新基底,异或两次就好了。

BZOJ2115Xor
一道图论包装下的模板题,dfs把圈圈找出来就好了。
(然而我用的是高斯消元)

#include
using namespace std;
typedef long long ll;
const int M=2e5+20;
const int N=5e4+10;
int n,m;
int tot,head[M],to[M],nxt[M],cc,sum;
ll w[M],cir[M],d[M],ans;
int vis[N];

inline void dfs(int x,int fr)
{
	vis[x]=1;
	for(int i=head[x];i;i=nxt[i]){
		int e=to[i];
		if(e==fr) continue;
		if(!vis[e]){
			d[e]=d[x]^w[i];
			dfs(e,x);
		}else{
			cir[++cc]=d[e]^d[x]^w[i];
		}
	}
}

inline void gauss()
{
	ans=d[n];
	for(int i=60;i>=0;i--){
		int now=sum+1;
		while(now<=cc && !(cir[now]&(1LL<<i))) now++;
		if(now>cc) continue;sum++;
		swap(cir[now],cir[sum]);
		for(int k=1;k<=cc;k++){
			if(k!=sum && (cir[k]&(1LL<<i))){
				cir[k]^=cir[sum];
			}
		}
		
	}
	for(int i=1;i<=sum;i++){
			ans=max(ans,ans^cir[i]);
	}
}

int main(){
	scanf("%d%d",&n,&m);ll c;
	for(int u,v,i=1;i<=m;i++){
		scanf("%d%d%lld",&u,&v,&c);
		nxt[++tot]=head[u];head[u]=tot;to[tot]=v;w[tot]=c;
		nxt[++tot]=head[v];head[v]=tot;to[tot]=u;w[tot]=c;
	}
	dfs(1,0);gauss();
	printf("%lld\n",ans);
	return 0;
}

你可能感兴趣的:(---线性代数---,线性基)