• 线性基


    简介

    线性基常用来处理异或问题。对于一个值域 \([1,n]\) ,就可以用一个长度为 \(\lceil \log_2 n \rceil\) 的数组来描述一个线性基。
    特别地,线性基中第 \(i\) 位上的数在二进制下也有 \(i\) 位。

    性质

    线性基满足:对于它所表示的数的集合 \(S\)\(S\) 中任意多个数异或的结果均可以用线性基中的数异或的结果表示。

    插入

    令插入的数为 \(x\) ,设 \(x\) 在二进制下有 \(i\) 位。
    若线性基的第 \(i\) 位为 \(0\) ,则在第 \(i\) 位上插入 \(x\) ,结束。
    反之,则将 \(x\) 异或上当前线性基中第 \(i\) 位的值 \(a_i\) ,并继续插入。
    重复以上操作,直到 \(x\) 变为 \(0\)
    如果结束时 \(x=0\) ,因为 \(x \oplus x=0\) ,所以在原来的线性基中就可以表示 \(x\) 了。
    反之,则表示此时往线性基中加入了一个新元素,此时也能表示 \(x\) 了。

    判断

    用上面插入的方法判断,若最后 \(x=0\) ,则能表示,反之,不能表示。

    查询异或最小值

    根据线性基的定义,线性基中的所有数在二进制下的位数都不同。
    所以,考虑线性基中最小的数,它异或上其它数一定比它原先的值大。
    所以,线性基中(即原序列中)的数能通过异或产生的最小值即为线性基中最小的数。

    查询异或最大值

    考虑贪心,从高位到低位遍历线性基,若这一位上的值 \(a_i\) 在二进制下的第 \(i\) 位为 \(1\) 并且此时答案在二进制下的第 \(i\) 位为 \(0\) ,那么就将答案异或上 \(a_i\) ,因为这样答案一定不会变劣。
    因此,遍历完线性基之后得到的答案一定就是线性基中(即原序列中)的数能通过异或产生的最大值。

    例题

    1
    最板子的板子。

    #include<iostream>
    #include<cstdio>
    #define int long long
    using namespace std;
    const int N=60;
    int n,a[N],b[N];
    int max_(int x,int y)
    {
    	return x>y?x:y;
    }
    void ins(int x)
    {
    	for(int i=N-1;i>=0;i--)
    		if(x&((int)1<<i))
    		{
    			if(!b[i])
    			{
    				b[i]=x;
    				return;
    			}
    			x^=b[i];
    		}
    }
    int query()
    {
    	int sum=0;
    	for(int i=N-1;i>=0;i--)
    		sum=max_(sum,sum^b[i]);
    	return sum;
    }
    signed main()
    {
    	scanf("%lld",&n);
    	for(int i=1;i<=n;i++)
    		scanf("%lld",&a[i]),ins(a[i]);
    	printf("%lld",query());
    	return 0;
    }
    

    2
    先进行一步转化,把开关的控制转化为异或上一个数。
    很显然,这样的转化是成立的。
    于是,把所有开关代表的数丢进线性基,最终,求出线性基中元素的数量 \(x\)
    可以得到答案为 \(2^x\)
    (每个元素都有选与不选两种情况,同时,线性基中的数不可能异或得到相同的数。)

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #define int long long
    using namespace std;
    const int N=60;
    int n,m,b[N];
    char a[N];
    void ins(int x)
    {
    	for(int i=N-1;i>=0;i--)
    		if(x&((int)1<<i))
    		{
    			if(!b[i])
    			{
    				b[i]=x;
    				return;
    			}
    			x^=b[i];
    		}
    }
    signed main()
    {
    	scanf("%lld%lld",&n,&m);
    	for(int i=1;i<=m;i++)
    	{
    		scanf("%s",a);
    		int sum=0;
    		for(int j=0;j<n;j++)
    			sum+=((int)1<<j)*(a[j]=='O');
    		//cout<<sum<<endl;
    		ins(sum);
    	}
    	int ans=0;
    	for(int i=0;i<N;i++)
    		if(b[i]) ans++;
    	//cout<<ans<<endl;
    	printf("%lld",((int)1<<ans)%(int)2008);
    	return 0;
    }
    

    3
    \(XOR\) 最大值有关,考虑用线性基求解。
    考虑题目中一句重要的话:
    路径可以重复经过某些点或边,当一条边在路径中出现了多次时,其权值在计算 XOR 和时也要被计算相应多的次数。
    一条边走多遍,一定不会得到更大的贡献,但是,可以让你不花费更大的代价走到一个环里并走回来。
    题目保证图为连通图,所以每个环都一定能走到。
    注:下文中链和环的价值即为链和环的 \(XOR\) 和。
    把所有环的价值都丢进线性基里,选一条链的价值作为初值,求 \(XOR\) 最大值。
    如何选择作为初值的链?
    假设 \(1\) ~ \(n\) 的路径有 \(A\)\(B\) 两条。
    \(A\)\(B\) 一定会共同组成一个环。
    假设我们先选择了 \(A\) 作为初值,如果 \(B\) 更优,因为所有环的价值都已经丢进了线性基里,所以求最大值时一定会发现异或上这个环的价值会使答案更优,所以最终也能得到 \(B\) 的价值。
    因为所有 \(1\) ~ \(n\) 的路径一定会两两组成若干个环,所以无论选择哪条链的价值作为初值,最终都可以得到答案。
    综上,可以随意选择一条 \(1\) ~ \(n\) 的路径的价值作为初值。

    #include<iostream>
    #include<cstdio>
    #include<vector>
    #define int long long
    using namespace std;
    const int N=5e4+10;
    const int M=65;
    struct node
    {
    	int to,val;
    };
    vector <node> e[N];
    int n,m,cnt[N],vis[N];
    int b[M];
    int max_(int x,int y)
    {
    	return x>y?x:y;
    }
    void ins(int x)
    {
    	for(int i=M-2;i>=0;i--)
    		if(x&(int)((int)1<<i))
    		{
    			if(b[i]) x^=b[i];
    			else
    			{
    				b[i]=x;
    				return;
    			}
    		}
    }
    int query(int x)
    {
    	int sum=x;
    	for(int i=M-2;i>=0;i--)
    		sum=max_(sum,sum^b[i]);
    	return sum;
    }
    void dfs(int x,int sum)
    {
    	vis[x]=1,cnt[x]=sum;
    	for(int i=0;i<e[x].size();i++)
    	{
    		if(!vis[e[x][i].to])
    			dfs(e[x][i].to,sum^e[x][i].val);
    		else ins(sum^e[x][i].val^cnt[e[x][i].to]);
    	}
    }//判环,并且将环的价值扔进线性基里
    signed main()
    {
    	scanf("%lld%lld",&n,&m);
    	for(int i=1;i<=m;i++)
    	{
    		int x,y,z;
    		scanf("%lld%lld%lld",&x,&y,&z);
    		e[x].push_back((node){y,z});
    		e[y].push_back((node){x,z});
    	}
    	dfs(1,0);
    	printf("%lld",query(cnt[n]));
    	return 0;
    }
    

    4
    考虑普通 NIM 游戏,若所有元素的异或和不为0,则先手必胜。
    因为要最小化不能加入线性基的数的和,所以将元素从大到小排列,然后能取则取。
    判断能不能取的部分用线性基实现。

    #include<algorithm>
    #include<iostream>
    #include<cstdio>
    using namespace std;
    const int N=110;
    const int M=60;
    int n,a[N],b[M];
    long long ans;
    bool ins(int x)
    {
    	for(int i=M-1;i>=0;i--)
    		if(x&(1<<i))
    		{
    			if(b[i]) x^=b[i];
    			else
    			{
    				b[i]=x;
    				return true;
    			}
    		}
    	return false;
    }
    bool cmp(int x,int y)
    {
    	return x>y;
    }
    int main()
    {
    	scanf("%d",&n);
    	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    	sort(a+1,a+n+1,cmp);
    	for(int i=1;i<=n;i++)
    		if(!ins(a[i])) ans+=(long long)a[i];
    	printf("%lld",ans);
    	return 0;
    }
    

    5
    用和LCA一样的方法倍增维护路径上的线性基。
    合并线性基时暴力将一个线性基内的元素插入另一个线性基里。
    复杂度约为 \(O(q \log n \log G_i)\) ,还有一定的常数,需要卡常。
    卡常技巧:
    1.插入的数为0时直接不插。
    2.若当前线性基已经满了直接不插。

    #include<iostream>
    #include<cstdio>
    #define ll long long
    using namespace std;
    const int N=2e4+10;
    const int M=64;
    struct node
    {
    	ll A[M];
    	bool flag;
    }F[N][20];
    struct edge
    {
    	int nxt,to;
    }e[N*2];
    int cnt,head[N];
    int n,q,d[N];
    ll a[N];
    int f[N][20],lg[N];
    ll MAX(ll x,ll y)
    {
    	return x>y?x:y;
    }
    void add(int x,int y)
    {
    	e[++cnt].to=y;
    	e[cnt].nxt=head[x];
    	head[x]=cnt;
    }
    node operator + (node x,ll y)
    {
    	if(!y) return x;
    	if(x.flag) return x;
    	for(int i=M-1;i>=0;i--)
    		if(y&(1ll<<i))
    		{
    			if(x.A[i]) y^=x.A[i];
    			else
    			{
    				x.A[i]=y;
    				break;
    			}
    		}
    	return x;
    }
    node operator + (node x,node y)
    {
    	bool f=false;
    	for(int i=M-1;i>=0;i--) x=x+y.A[i];
    	for(int i=0;i<M;i++)
    		if(!x.A[i])
    		{
    			f=true;
    			break;
    		}
    	if(!f) x.flag=true;
    	return x;
    }
    ll query(node x)
    {
    	ll sum=0;
    	for(int i=M-1;i>=0;i--)
    		sum=MAX(sum,sum^x.A[i]);
    	return sum;
    }
    void dfs(int x,int y)
    {
    	f[x][0]=y,F[x][0]=F[x][0]+a[x];
    	d[x]=d[f[x][0]]+1;
    	for(int i=1;i<=lg[d[x]]+1;i++)
    		f[x][i]=f[f[x][i-1]][i-1];
    	for(int i=1;i<=lg[d[x]]+1;i++)
    		F[x][i]=F[x][i-1]+F[f[x][i-1]][i-1];
    	for(int i=head[x];i;i=e[i].nxt)
    		if(e[i].to!=y) dfs(e[i].to,x);
    }
    ll LCA(int x,int y)
    {
    	node ans;
    	for(int i=0;i<M;i++) ans.A[i]=0;
    	ans.flag=false;
    	if(d[x]<d[y]) swap(x,y);
    	while(d[x]>d[y])
    	{
    		ans=ans+F[x][lg[d[x]-d[y]]];
    		x=f[x][lg[d[x]-d[y]]];
    	}
    	if(x==y)
    	{
    		ans=ans+F[x][0];
    		return query(ans);
    	}
    	for(int i=lg[d[x]];i>=0;i--)
    		if(f[x][i]!=f[y][i])
    		{
    			ans=ans+F[x][i],ans=ans+F[y][i];
    			x=f[x][i],y=f[y][i];
    		}
    	ans=ans+F[x][0],ans=ans+F[y][0],ans=ans+F[x][1];
    	return query(ans);
    }
    int read()
    {
    	int sum=0;
    	char x=getchar();
    	while(!(x>='0'&&x<='9')) x=getchar();
    	while(x>='0'&&x<='9') sum=sum*10+x-'0',x=getchar();
    	return sum;
    }
    int main()
    {
    	for(int i=2;i<N;i++) lg[i]=lg[i/2]+1;
    	scanf("%d%d",&n,&q);
    	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
    	for(int i=1;i<n;i++)
    	{
    		int x=read(),y=read();
    		add(x,y),add(y,x);
    	}
    	dfs(1,0);
    	while(q--)
    	{
    		int x=read(),y=read();
    		printf("%lld\n",LCA(x,y));
    	}
    	return 0;
    }
    
  • 相关阅读:
    CSS截断字符串
    [VB.NET] 打印DataGridView类
    [C#] 打印DataGridView类
    [MySQL] errno:150
    [C#] 用户自定义控件(含源代码)透明文本框
    [分享] 微软面试题
    [C#] 用户自定义控件(含源代码)圆角Panel
    [MySQL] 史上最全的MySQL备份方法
    [C#(WebForm)] 利用递归遍历文件夹和文件存入TreeView
    [VB] (开源)VB高仿Windows7桌面炫丽版
  • 原文地址:https://www.cnblogs.com/zhs1/p/13708371.html
Copyright © 2020-2023  润新知