线性基 系列
线性基
线性基是一种擅长处理异或问题的数据结构。
设原数组值域为 [1,n][1,n][1,n],我们用一个长度与 nnn 的二进制下位数相等的数组(即长度为 ⌈logn⌉\left\lceil\log n\right\rceil⌈logn⌉)描述一个线性基。
线性基 X\mathbb{X}X 满足一些性质:
- 其第 iii 位上的数二进制最高位也是第 iii 位,即每个元素的二进制位数不相同;
- X\mathbb{X}X 中选定若干个数异或起来的结果的集合,等价于原数组 aia_iai 选定若干个数异或起来的结果的集合,即等价性;
- X\mathbb{X}X 是满足上一性质的所有集合中,元素个数最少的,即最小性;
- 前面 333 条结论,隐含着 X\mathbb{X}X 元素的线性无关性,保证 X\mathbb{X}X 中不同的子集异或和结果不同;
- X\mathbb{X}X 不存在异或和为 000 的子集,因为若X1xorX2xorX3=0\mathbb{X}_1\ \mathrm{xor}\ \mathbb{X}_2\ \mathrm{xor}\ \mathbb{X}_3=0X1 xor X2 xor X3=0,那么 X1xorX2=X3\mathbb{X}_1\ \mathrm{xor}\ \mathbb{X}_2=\mathbb{X}_3X1 xor X2=X3,违背了线性无关性。
因此线性基把规模为 2n2^n2n 的组合,缩小为了规模为 2⌈logn⌉2^{\left\lceil\log n\right\rceil}2⌈logn⌉ 的集合。
根据线性无关性,对于线性基 X\mathbb{X}X,记 k=∣X∣k=|\mathbb{X}|k=∣X∣ 为 X\mathbb{X}X 元素个数,除去空集 000,能够组合出的结果有 2k−12^k-12k−1 种。
怎么构造线性基呢?对于原数组 ai={8,13,15}a_i=\{8,13,15\}ai={8,13,15},二进制下是:ai={1000,1101,1111}2a_i=\{1000,1101,1111\}_2ai={1000,1101,1111}2。最高位是第 444 位。为了使得每个元素的二进制位数不相同,即以第 iii 位为最高位的 X\mathbb{X}X 元素只有 111 个,我们需要与已存在的高位 X\mathbb{X}X 元素异或一下,把高位“砍掉”:
- 第 111 个数最高位 333 位(000 开始低位),此时 X3\mathbb{X}_3X3 为空,因此 X3=(1000)2=8\mathbb{X}_3=(1000)_2=8X3=(1000)2=8;
- 第 222 个数最高位也是 333 位,将其与 X3=(1000)2\mathbb{X}_3=(1000)_2X3=(1000)2 异或得到 (101)2(101)_2(101)2,此时最高位为 222 位,因此 X2=(101)2=5\mathbb{X}_2=(101)_2=5X2=(101)2=5;
- 第 333 个数最高位也是 333 位,将其与 X3\mathbb{X}_3X3 和 X2\mathbb{X}_2X2 异或,得到 (10)2(10)_2(10)2。此时最高位为 111 位且 X1\mathbb{X}_1X1 为空,因此 X1=(10)2=2\mathbb{X}_1=(10)_2=2X1=(10)2=2;
- 最后得到线性基为 X={8,5,2,null}\mathbb{X}=\{8,5,2,\mathrm{null}\}X={8,5,2,null}。可以手模一下是不是每个子集异或和都不同。
注:如果一个数砍掉所有都加不进 X\mathbb{X}X,那么异或和子集就会出现 000,打上 zerozerozero 标记(这个标记在后面的做题很有用处)。
将 nnn 个数插入构造线性基,显然复杂度为 O(nlogn)O(n\log n)O(nlogn)。
这只是其中一种构造方法,对于一个数列生成线性基,线性基不唯一。
查询最小异或和,考虑插入的过程,因为每一次跳转操作,Xi\mathbb{X}_iXi 的二进制最高位必定单调降低,所以不可能插入两个二进制最高位相同的数。而此时,线性基中最小值异或上其他数,必定会增大。所以,直接输出线性基中的最小值即可。
考虑异或最大值,从高到低遍历线性基,考虑到第i位时,如果当前的答案 retretret 第 iii 位为 000,就将 retretret 异或上 Xi\mathbb{X}_iXi ;否则不做任何操作。显然,每次操作后答案不会变劣,最终的 retretret 即为答案。
struct bas
{ll a[N];//a[i]:最高位在i位的元素 bool zero=0;void insert(ll x){for(int i=60;i>=0;i--){if(x&(1ll<<i)){if(!a[i]){a[i]=x;return;}else x^=a[i];}}zero=1;//异或为0标记,x的每一位都有替代,存在子集xor和为x,即x加入后存在0 }bool check(ll x)//是否能异或得到数x {for(int i=60;i>=0;i--){if(x&(1ll<<i)){if(!a[i])return 0;else x^=a[i];}}return 1;}ll query_ma(){ll ret=0;for(int i=60;i>=0;i--)ret=max(ret,ret^a[i]);return ret;}ll query_mi(){if(zero)return 0;for(int i=0;i<=60;i++)if(a[i])return a[i];}
}X;
1.nfls #3473 线性基1 / 洛谷 P3812 【模板】线性基
题意
给定由 nnn 个数组成的可重集 SSS,求一个 T⊆ST\subseteq ST⊆S,使得 xori=1∣T∣Ti\mathrm{xor}_{i=1}^{|T|}T_ixori=1∣T∣Ti 最大。
1≤n≤501\le n\le 501≤n≤50,0≤Si≤2500\le S_i\le 2^{50}0≤Si≤250。
思路
根据线性基与原数组异或运算的等价性,即在线性基上异或操作等价于在原数组上异或操作,我们生成 SSS 的线性基,然后计算线性基异或最大值即可。
代码
int main()
{scanf("%lld",&n);for(int i=1;i<=n;i++){ll x;scanf("%lld",&x);X.insert(x);}printf("%lld",X.query_ma());return 0;
}
2.洛谷 P3857 TJOI2008 彩灯
题意
Peter 亲自设计了一组彩灯,想给女朋友一个惊喜。已知一组彩灯是由一排 NNN 个独立的灯泡构成的,并且有 MMM 个开关控制它们。从数学的角度看,这一排彩灯的任何一个彩灯只有亮与不亮两个状态,所以共有 2N2^N2N 个样式。
由于技术上的问题,Peter 设计的每个开关控制的彩灯没有什么规律,当一个开关被按下的时候,它会把所有它控制的彩灯改变状态(即亮变成不亮,不亮变成亮)。假如告诉你他设计的每个开关所控制的彩灯范围,你能否帮他计算出这些彩灯有多少种样式可以展示给他的女朋友?
MMM 个开关控制着 NNN 盏灯,开关对彩灯的控制情况用 MMM 个字符串表述,从第 111 个字符开始算起,如果第 iii 个字母是 O
,表示开关控制彩灯 iii,否则第 iii 个字母是 X
表示开关不控制彩灯 iii。
注: 开始时所有彩灯都是不亮的状态。
1≤n,m≤501\le n,m\le 501≤n,m≤50。
思路
操作一个开关,控制某个灯的改变改灯状态,不控制的就不管,这不就是 xor1\mathrm{xor}\ 1xor 1 和 xor0\mathrm{xor}\ 0xor 0 吗?
我们把开关的控制状态根据 O
和 X
的状态转成二进制序列,问题转化为这些二进制数中选取若干个异或起来有多少种不同的结果。
我们直接将所有二进制数扔上线性基 k=Xk=\mathbb{X}k=X,然后根据 ∣X∣|\mathbb{X}|∣X∣(最高位 iii 有数的数量)算出,共有 2k2^k2k 种结果(算上了初始时一盏灯不开的空集情况)。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=62,mod=2008;
ll n,m;
struct bas
{ll a[N],tot;bool zero=0;void insert(ll x){for(int i=60;i>=0;i--){if(x&(1ll<<i)){if(!a[i]){a[i]=x;tot++;return;}//tot统计有1的最高位 else x^=a[i];}}zero=1;}
}X;
int main()
{scanf("%lld%lld",&n,&m);for(int i=1;i<=m;i++){string s;cin>>s;s='*'+s;ll x=0;for(int j=1;j<=n;j++)if(s[j]=='O')x|=(1ll<<j);X.insert(x);}printf("%lld",(1ll<<X.tot)%mod);//异或的结果种树,就是线性基的子集数 return 0;
}
3.洛谷 P4570 BJWC2011 元素
题意
题目传送门,建议前往阅读题面和样例。
思路
题目要求找到一个子集,使得子集中元素的 numnumnum 异或和不为 000 的情况下,求 ∑val\sum val∑val 的最大值。
我们想要贪心地取 valvalval 更大的矿石,如果遇到一个 numnumnum 加入会使原数列产生异或和为 000 的矿石就把它扔掉。
为什么这样的贪心是正确的呢?假若矿石按照 valvalval 从大到小排序,现在判断矿石 iii 能否加入。假若前面的矿石存在某个子集 TTT,使得 xorx∈Tnumx=numi\mathrm{xor}_{x\in T}num_x=num_ixorx∈Tnumx=numi,iii 加入肯定是不合法的,但是如果拿 iii 换掉 TTT 中的任意一个矿石都是不优的:
首先用 iii 换掉子集 TTT 中的某个矿石,这个子集的异或和仍然不变;其次 TTT 中矿石的编号都小于 iii,即这些矿石的 valvalval 都大于 valival_ivali,用 iii 换其中的矿石显然不优。
怎么判断加入矿石 iii,会出现异或和为 000 的子集呢?在上面我们提到,如果一个数如果砍掉所有高位都无法加进线性基,说明异或和子集出现 000,此时 zerozerozero 标记打成 111:我们只要判断加入 numinum_inumi 的时候,zerozerozero 是否被打成 111 即可。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1002,M=62;
ll n;
struct node
{ll num,val;
}b[N];
bool cmp(node x,node y)
{return x.val>y.val;
}
struct bas
{ll a[M];bool zero=0;void insert(ll x){for(int i=60;i>=0;i--){if(x&(1ll<<i)){if(!a[i]){a[i]=x;return;}else x^=a[i];}}zero=1;}
}X;
int main()
{scanf("%lld",&n);for(int i=1;i<=n;i++){ll num,val;scanf("%lld%lld",&num,&val);b[i]=(node){num,val};}sort(b+1,b+n+1,cmp);ll ans=0;for(int i=1;i<=n;i++){X.zero=0;X.insert(b[i].num);if(!X.zero)ans+=b[i].val;}printf("%lld",ans);return 0;
}
4.洛谷 P4301 CQOI2013 新Nim游戏
题意
传统的 Nim 游戏是这样的:有一些火柴堆,每堆都有若干根火柴(不同堆的火柴数量可以不同)。两个游戏者轮流操作,每次可以选一个火柴堆拿走若干根火柴。可以只拿一根,也可以拿走整堆火柴,但不能同时从超过一堆火柴中拿。拿走最后一根火柴的游戏者胜利。
本题的游戏稍微有些不同:在第一个回合中,双方可以直接拿走若干个整堆的火柴。可以一堆都不拿,但不可以全部拿走。从第二个回合(又轮到第一个游戏者)开始,规则和 Nim 游戏一样。
如果你先拿,怎样才能保证获胜?如果可以获胜的话,还要让第一回合拿的火柴总数尽量小。
1≤k≤1001 \leq k \leq 1001≤k≤100,1≤ai≤1091 \leq a_i \leq 10^91≤ai≤109。
思路
就是允许拿走几堆火柴(先手拿走这几堆,后手不拿也可以直接开始),再开始传统的 Nim 游戏。
Nim 游戏取 nnn 堆石子 aia_iai,当 Nim 和 xori=1nai=0\mathrm{xor}_{i=1}^na_i=0xori=1nai=0 时,先手为必败态;否则后手为必败态。
我们考虑拿走后,剩下的几堆不存在异或和为 000 的子集,剩下的几堆剩下的石子个数尽量多(等价于拿走的尽量少)。这就和 3.3.3. 一样了。按照石子个数从大到小排序,遇到一个使得 zerozerozero 为 111 的直接拿走。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=103,M=62;
ll n,c[N];
bool cmp(ll x,ll y)
{return x>y;
}
struct bas
{ll a[M];bool zero=0;void insert(ll x){for(int i=60;i>=0;i--){if(x&(1ll<<i)){if(!a[i]){a[i]=x;return;}else x^=a[i];}}zero=1;}
}X;
int main()
{scanf("%lld",&n);for(int i=1;i<=n;i++)scanf("%lld",&c[i]);sort(c+1,c+n+1,cmp);ll ans=0;for(int i=1;i<=n;i++){X.zero=0;X.insert(c[i]);if(X.zero)ans+=c[i];//加入c[i]异或总和为0,舍弃 }printf("%lld",ans);return 0;
}
5.nfls #3474 线性基2 / HDU3939 XOR
题意
给由 nnn 个数组成的一个可重集 SSS,每次给定一个数 kkk,求一个子集 T⊆ST\subseteq ST⊆S,使得集合 TTT 在 SSS 的所有非空子集的不同异或和中,其异或和 xori=1∣T∣Ti\mathrm{xor}_{i=1}^{|T|}T_ixori=1∣T∣Ti 是第 kkk 小的。
思路
这是线性基又一个经典问题。
线性基 X\mathbb{X}X 的第 iii 位,二进制下最高位也是 iii 位。我们想要进一步简化 X\mathbb{X}X:我们考虑将 i∼i−ki\sim i-ki∼i−k 位线性基的最高位变成阶梯状,使得最高位的低一位是 000,形如:
10001010000010100011\begin{matrix} 10001\\ 01000\\ 00101\\ 00011 \end{matrix}10001010000010100011
这 444 个元素是 555 位二进制的,组成 151515 个非空子集,假若不看最后一位,那么 Xixor\mathbb{X}_i\ \mathrm{xor}Xi xor 任何一个 Xj\mathbb{X}_jXj,异或结果的第 iii 位和第 jjj 位都会是 111。如此一来选择的 iii 位线性基可选,第 iii 位选或者不选就能和排名的增减强相关:譬如我要选第 555 小的,5=(0101)25=(0101)_25=(0101)2,我就选第 222 位和第 000 位异或起来,即 01000xor00011=0101101000\ \mathrm{xor}\ 00011=0101101000 xor 00011=01011。
(这和高斯消元有异曲同工之妙,这本质上就是高斯消元)
怎么得到这样的阶梯矩阵呢?比如 Xi\mathbb{X}_iXi 最高位是 iii,我们只要把后面的有 111 的位都消掉就行了,如果 Xi\mathbb{X}_iXi 的第 jjj 位(j<ij<ij<i 位)有 111,因为 Xj\mathbb{X}_jXj 的最高位就是 jjj 位,直接拿 Xi←XixorXj\mathbb{X}_i\leftarrow \mathbb{X}_i\ \mathrm{xor}\ \mathbb{X}_jXi←Xi xor Xj 就行了。
为什么不强制修改 Xi\mathbb{X}_iXi 的有 111 的第 jjj 位而是选择异或 Xj\mathbb{X}_jXj 呢?首先在线性基内进行异或操作等价于在原数组异或,其次设 Xi′=t=XixorXj\mathbb{X}_i'=t=\mathbb{X}_i\ \mathrm{xor}\ \mathbb{X}_jXi′=t=Xi xor Xj,我们用 Xi′xorXj\mathbb{X}_i'\ \mathrm{xor}\ \mathbb{X}_jXi′ xor Xj 依然能得到原来的 Xi\mathbb{X}_iXi。
我们把所有 Xi\mathbb{X}_iXi 记录下来,然后像上文举得例子一样拆 kkk 的二进制位,来选择线性基的位异或起来就行了。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=62;
ll n,Q;
struct bas
{ll a[N];//a[i]:最高位在i位的元素 bool zero=0;void insert(ll x){for(int i=60;i>=0;i--){if(x&(1ll<<i)){if(!a[i]){a[i]=x;return;}else x^=a[i];}}zero=1;}ll tem[N];ll query(ll k)//异或第k小 {if(zero)k--;if(k==0)return 0;ll tot=0,ret=0;for(int i=0;i<=60;i++){for(int j=i-1;j>=0;j--)if(a[i]&(1ll<<j))a[i]^=a[j];if(a[i])tem[tot++]=a[i];}if(k>=(1ll<<tot))return -1;for(int i=0;i<tot;i++)if(k&(1ll<<i))ret^=tem[i];return ret;}
}X;
int main()
{scanf("%lld",&n);for(int i=1;i<=n;i++){ll x;scanf("%lld",&x);X.insert(x);}scanf("%lld",&Q);while(Q--){ll k;scanf("%lld",&k);printf("%lld\n",X.query(k));}return 0;
}