还不会用 Markdown
的时候写的文章……重修&复习了一遍。主要修改的还是习题部分。
0 - 意义
线性基是向量空间的一组基,通常可以解决有关异或的一些题目。
简单讲就是由一个集合构造出来的另一个集合,这个集合大小最小且能异或出原来集合中的任何一个数,并且不能表示出除了原集合的其他数。
性质
-
线性基能相互异或得到原集合的所有相互异或得到的值。
-
线性基是满足性质1的最小的集合
-
线性基没有异或和为 (0) 的子集。
-
假设线性基中有 (cnt) 个数,线性基能异或出的数的集合大小为 (2^{cnt}-1)(去掉一个都不取),也就是说,线性基中不同的组合异或出的数都不一样。
1 - 构造
设当前插入的数是 (x) ,线性基数组为 (a) ,从高位向低位走,考虑所有为 (1) 的当前位 (i) ,
- 如果线性基的第 (i) 位为 (0) ,那么直接在这一位插入 (x) ,退出;
- 否则,令 (x=xoplus a[i])
- 重复上述操作直到 (x=0)
如果退出循环的时候 (x=0) ,那么说明原有的线性基已经可以表示 (x) ,无需再插入;反之,则说明为了表示 (x) 插入了一个新的元素。
void Insert( ll x )
{
for ( int i=30; ~i; i-- )
if ( x&(1ll<<i) )
if ( !a[i] ) { a[i]=x; return; }
else x^=a[i];
flag=1;
}
检验存在
检查一个数是否能被某个线性基表示出来。
和插入类似,只要中途或者最后变成 (0) 了,就说明能够表示。
bool check( ll x )
{
for ( int i=30; ~i; i-- )
if ( x&(1ll<<i) )
if ( !a[i] ) return 0;
else x^=a[i];
return 1;
}
2 - 查询异或最值
最小值
查询最小值相对比较简单。
考虑在插入的过程中,每一次异或 (a[i]) 的操作,(x) 的二进制最高位都在降低,所以不可能插入两个二进制最高位相同的数。
此时线性基中的最小值异或上其他的数,必然会增大,所以直接输出线性基中的最小值即可。
注意要特判能否异或出 (0) . 因为线性基有性质:没有异或和为 (0) 的子集。特判也很简单,只要一个数在插入过程中没有被插入到某个 (a[i]) ,那么就被异或成了 (0) ,说明 (0) 是可以取到的。
ll Query_min( ll res=0 )
{
if ( fl ) return 0; //flag 是 Insert 中传出来的变量,表示是否能表示 0
for ( int i=0; i<=30; i++ )
if ( a[i] ) return a[i];
}
最大值
从高到低遍历线性基,设当前考虑到第 (i) 位。如果当前答案 (res) 的第 (i) 位为 (0) ,就将 (res=resoplus a[i]) ;否则不操作。或者说,更简便的写法是直接和异或后的值取 (max) (其实是一个道理,高位从 (0 o 1) 一定是变大的嘛)
这是显然的,求最小值部分已经说过,线性基中数的最高位显然单调递减,那么每次这样的操作之后答案都不会变劣。
这是对序列中元素求相互异或的最大值。如果是另一个给定的数 (x) ,那么用类似的方式可以解决,只需要把 (res) 的初始值改变即可。
ll Query_max( ll res=0 )
{
for ( int i=30; ~i; i-- )
res=max( res,res^a[i] );
return res;
}
模板
写到这里就可以写 模板题 了。代码:
//Author:RingweEH
const int N=55,MX=50;
int n;
ll a[N];
void Insert( ll x )
{
for ( int i=MX; ~i; i-- )
if ( x>>i&1 )
{
if ( !a[i] ) { a[i]=x; return; }
x^=a[i];
}
}
ll Query_mx( ll res=0 )
{
for ( int i=MX; ~i; i-- )
res=max( res,res^a[i] );
return res;
}
int main()
{
n=read();
for ( int i=1; i<=n; i++ )
{
ll x=read(); Insert( x );
}
printf( "%lld
",Query_mx() );
return 0;
}
3 - 求第 k 小
首先,线性基的构造方式跟之前不太一样了,我们知道,线性基是以每个二进制为最高位存一个数的,容易想到把 k 二进制分解,这样的话,只需要改点限制:规定 (a[i]) 的值最高位是第 (i) 位,且在此基础上 (a[i]) 最小。
考虑之前的 (a[i]) ,它除了在第 (i) 位有个 (1) 外,在更低的位还有若干个 (1) 。那是否可以用线性基中的某些数,尽量消去低位的那些 (1) ? 这个很好做,往线性基插入一个新数时,用这个 (a[i]) 更新 (a) 数组的其它所有值就行了。
详细做法:
- 对于低位
现在插入的一个数放到了 (a[i]) ,它在一个更低的二进制位(设其为第 (j) 位)上为 (1) ,且 (a[j]) 已被赋过值,那就把 (a[i]) 更新为 (a[i]oplus a[j]) 。为了方便,从大到小枚举 (j) 即可。
- 对于高位
只考虑低位显然不对,因为有可能 (a[i]) 的第 (j) 个二进制位为 (1) ,而 (a[j]) 此时可能没有值,但它以后被赋了值,这种情况下也应该用 (a[j]) 更新 (a[i]) 。我们只能用赋值晚的更新赋值早的,所以对于插入的一个数 (a[i]) ,不仅要用更低位的 (a[j]) 更新它,还要用它更新更高位的 (a[j]) 。依然从大到小枚举 (j) 。
代码:
for ( int i=N; ~i; i-- )
if ( x>>i&1 )
{
if ( a[i] ) x^=a[i];
else
{
a[i]=x;
for ( int j=i-1; ~j; j-- )
if ( a[j] && (a[i]>>j&1) ) a[i]^=a[j];
for ( int j=N; j>i; j-- )
if ( a[j]>>i&1 ) a[j]^=a[i];
return;
}
}
其实这才是线性基的通用构造方式(比如对于模板题,多出来的要求对答案没有影响,因此该构造方案可以兼容使用)。
换个角度看这个构造方式,其实就是标准的高斯消元,所谓的把不在对角线上的 (1) 能消掉就消掉,其实也就是让每行的数最小。
如上改变线性基的构造方式后,把 (k) 二进制分解,若第 (i) 位为 (1) 就把 (ans) 异或上 (p_i) 即可。
注意也要特判能否异或出 (0) ,并且在插入完之后要压缩线性基数组,只留下 (a[i] eq 0) 的部分。(这个显然,为 (0) 相当于是无效位,对 (k) 没有任何贡献,当然也不能算进位数里面)
//Author:RingweEH
const int N=63,M=65;
int n,cnt;
ll a[M],b[M];
bool fl=0;
void Insert( ll x )
{
for ( int i=N; ~i; i-- )
if ( x>>i&1 )
{
if ( a[i] ) x^=a[i];
else
{
a[i]=x;
for ( int j=i-1; ~j; j-- )
if ( a[j] && (a[i]>>j&1) ) a[i]^=a[j];
for ( int j=N; j>i; j-- )
if ( a[j]>>i&1 ) a[j]^=a[i];
return;
}
}
if ( x==0 ) fl=1;
}
int main()
{
int T=read();
for ( int cas=1; cas<=T; cas++ )
{
memset( a,0,sizeof(a) ); fl=0; cnt=0;
n=read();
for ( int i=1; i<=n; i++ )
{
ll x=read(); Insert( x );
}
for ( int i=0; i<=N; i++ )
if ( a[i] ) b[cnt++]=a[i];
int q=read(); printf( "Case #%d:
",cas );
while ( q-- )
{
ll k=read(),ans=0; k-=fl;
if ( k>=(1ll<<cnt) ) { printf( "-1
" ); continue; }
for ( int i=0; i<cnt; i++ )
if ( k>>i&1 ) ans^=b[i];
printf( "%lld
",ans );
}
}
return 0;
}
4 - 习题
也许大概或许可能是按难度排序的吧(
彩灯
有一个长度为 (N) 的01串,初始全 (0) 。给出 (M) 个操作,每个操作能使特定的几位取反,问能产生几种不同的 (01) 串。
Solution
显然是裸题。将每个操作看成一个数,构造线性基,题目也就是问能表示出多少个数。
注意到有性质:
假设线性基中有 (cnt) 个数,线性基能异或出的数的集合大小为 (2^{cnt}-1)(去掉一个都不取)
那就做完了。不过这题全 (0) 也算一种方案,不需要 (-1) .
我怎么又没看见取模(悲)
//Author:RingweEH
const int N=55,MX=50;
const ll Mod=2008;
int n,m;
ll a[N],cnt=0;
char s[N];
void Insert( ll x )
{
for ( int i=MX; ~i; i-- )
if ( x>>i&1 )
{
if ( !a[i] ) { a[i]=x; cnt++; return; }
x^=a[i];
}
}
int main()
{
n=read(); m=read();
for ( int i=1; i<=m; i++ )
{
scanf( "%s",s ); ll x=0;
for ( int j=0; j<n; j++ )
{
x<<=1;
if ( s[j]=='O' ) x|=1;
}
Insert( x );
}
printf( "%lld
",(1ll<<cnt)%Mod );
return 0;
}
最大XOR和路径
给定一个边权为非负整数的无向连通图,求 (1) 到 (N) 的路径,使得边权异或和最大。点边可以重复经过。
(Nleq 5e4,Mleq 1e5,D_ileq 1e18) .
Solution
做法很简单:找出所有环,扔到线性基里,然后随便找一条路径作为初始值,求异或最大值即可。
考虑为什么是对的。
首先找环肯定是没有疑问的,因为重复走两遍相当于没有走,唯一能产生变数的就是环了。
然后考虑为什么随便一条路径就行。假设存在至少两条,设为 (path_1,path_2) ,那么它们本身就构成了一个大环,异或一下就能得到对方。因此只要任意一条路径+所有环就好了。
//Author:RingweEH
const int N=5e4+10;
struct Edge
{
int to,nxt; ll val;
}e[N<<2];
int head[N],tot=0,n,m;
ll path[N],a[64];
bool vis[N];
void Add( int u,int v,ll w )
{
e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[tot].val=w;
}
void Insert( ll x )
{
for ( int i=62; ~i; i-- )
if ( x>>i&1 )
{
if ( !a[i] ) { a[i]=x; return; }
x^=a[i];
}
}
void Dfs( int u,int fa,ll now )
{
vis[u]=1; path[u]=now;
for ( int i=head[u]; i; i=e[i].nxt )
{
int v=e[i].to;
if ( v==fa ) continue;
if ( !vis[v] ) Dfs( v,u,now^e[i].val );
else Insert( path[v]^now^e[i].val );
}
}
int main()
{
n=read(); m=read();
for ( int i=1; i<=m; i++ )
{
int u=read(),v=read(); ll w=read();
Add( u,v,w ); Add( v,u,w );
}
Dfs( 1,0,0 ); ll ans=path[n];
for ( int i=62; ~i; i-- )
ans=max( ans,ans^a[i] );
printf( "%lld
",ans );
return 0;
}
albus就是要第一个出场
给定一个长度为 (n) 的序列 (A) ,将所有 (A) 的子集的异或和从小到大排成序列 (B) ,求一个数在 (B) 中第一次出现的下标。
Solution
还是用这个性质:
假设线性基中有 (cnt) 个数,线性基能异或出的数的集合大小为 (2^{cnt}-1)(去掉一个都不取)
然后注意到除了这 (cnt) 个数,还有 (n-cnt) 个,而它们所能组成的异或和一定能被线性基中的数表示出来,也就相当于我们有 (2^{n-cnt}) 个异或和为 (0) 的子集。那么就是,所有能异或出的数的集合中,每个数在 (B) 序列里都出现了 (2^{n-cnt}) 次。我们只需要查询数 (x) 在不重复的序列中的排名 (rk) ,然后 (ans=rk imes2^{n-cnt}+1) 即可。
现在考虑如何求排名。从高到低枚举每一位 (a[i] eq 0) 的位置,如果 (x) 的当前位为 (1) ,那么就是比 “当前位为 (0) 的 (2^{n-cnt}) 个异或和”都要大,就加上这一部分的贡献;否则不加。(注:这里的 (cnt) 指的是到当前位位置,(a[i] eq 0) 的个数。这应该很好理解,因为如果当前这个高位为 (1) ,那么无论后面怎么取,都比高位为 (0) 的要大)
被位运算优先级坑了一发 /kk
//Author:RingweEH
const int N=1e5+10,M=30,Mod=10086;
int n,a[M+5];
ll power( ll a,ll b )
{
ll res=1;
for ( ; b; b>>=1,a=a*a%Mod )
if ( b&1 ) res=res*a%Mod;
return res;
}
void Insert( int x )
{
for ( int i=M; ~i; i-- )
if ( x>>i&1 )
{
if ( !a[i] ) { a[i]=x; return; }
x^=a[i];
}
}
ll Query_rk( int x )
{
int cnt=0; ll ans=0;
for ( int i=M; ~i; i-- )
if ( a[i] )
{
cnt++;
if ( x>>i&1 ) ans=(ans+power(2ll,n-cnt))%Mod;
}
return ans;
}
int main()
{
n=read();
for ( int i=1; i<=n; i++ )
Insert( read() );
ll Q=read(); ll ans=Query_rk(Q);
printf( "%lld
",(ans+1)%Mod );
return 0;
}
新Nim游戏
在 Nim 游戏的第一轮,允许两个玩家特殊操作:可以拿走若干个整堆,可以一堆都不拿,但是不能全部拿走。其余同 Nim。
问先手是否必胜,如果是那么给出第一轮拿的最小数量。
Solution
其实先手肯定必胜,第一次拿的时候只剩下一堆就好了。
那么问题在于如何让第一轮拿走的数量最小。
显然可以发现,先手第一轮拿完之后不能剩下异或为 (0) 的子集。而这显然是个线性基(性质 (3) ),也就是要构造和最大的一组线性基。
那么将每一堆排序,然后依次尝试加入线性基,并求出所有成功加入的数之和即可。
//Author:RingweEH
const int N=110,M=30;
int n,a[35],b[N];
bool Insert( int x )
{
for ( int i=M; ~i; i-- )
if ( x>>i&1 )
{
if ( !a[i] ) { a[i]=x; return 1; }
x^=a[i];
}
return 0;
}
int main()
{
n=read(); ll sum=0;
for ( int i=1; i<=n; i++ )
b[i]=read(),sum+=b[i];
sort( b+1,b+1+n ); ll ans=0;
for ( int i=n; i>=1; i-- )
if ( Insert(b[i]) ) ans+=b[i];
printf( "%lld
",sum-ans );
return 0;
}
元素
给定一个长度为 (n) 的序列 (A[i][0/1]) ,求一个子集,满足 (A[i][0]) 的异或和不为 (0) 的情况下,(A[i][1]) 和最大。
Solution
神笔题,直接按照 (A[i][1]) 排序,然后依次尝试插入即可。和上题差不多。
//Author:RingweEH
const int N=1010,M=60;
struct Node
{
ll num; ll val;
bool operator < ( const Node &tmp ) const { return val<tmp.val; }
}b[N];
int n;
ll a[M];
bool Insert( ll x )
{
for ( int i=M; ~i; i-- )
if ( x>>i&1 )
{
if ( !a[i] ) { a[i]=x; return 1; }
x^=a[i];
}
return 0;
}
int main()
{
n=read();
for ( int i=1; i<=n; i++ )
b[i].num=read(),b[i].val=read();
sort( b+1,b+1+n ); ll ans=0;
for ( int i=n; i>=1; i-- )
if ( Insert(b[i].num) ) ans+=b[i].val;
printf( "%lld
",ans );
return 0;
}
装备购买
(n) 个装备,每个装备 (m) 个属性,每个装备还有个价格。如果手里有的装备的每一项属性为它们分配系数(实数)后可以相加得到某件装备,则不必要买这件装备。求最多装备下的最小花费。
Solution
“能被已有的装备组合出来”这一点很像线性基,但这里不再是异或线性基了,而是实数。
回归本真的线性基,可喜可贺
其实方式和异或线性基差不多,不过是把原来的 (x=xoplus a[i]) 换成了消元(具体参考高斯消元的方式),之前 (a[i]) 记录的是每一位上留下的那个数,现在就记录一个位置,使得矩阵中第 (i) 列(也就是第 (i) 个变量)只有 (a[i]) 这一行不为 (0) (对应高斯消元中把每一行的方程消成只剩下一个变量, (a[i]) 记录的是第 (i) 个变量所在的方程)。每次找当前行不为 (0) 的列 (j) ,如果 (a[j]) 还没有值就赋值并退出,否则就用 (c[i][j]/c[a[j]][j]) 乘上 (c[a[j]][k]) 去减 (a[i][k]) 。不会高斯消元的你试试看怎么消元解多元方程就好了吧
然后要求最小花费,那就排个序即可。
精度yyds!
要开 long double
或者把 (eps) 调成 (1e-5) .
//Author:RingweEH
const int N=510;
const db eps=1e-5;
struct Vector
{
db a[N]; int val;
db &operator [] ( const int &x ) { return a[x]; }
bool operator < ( const Vector&tmp ) const { return val<tmp.val; }
}c[N];
int n,m,a[N];
int main()
{
n=read(); m=read();
for ( int i=1; i<=n; i++ )
for ( int j=1; j<=m; j++ )
scanf( "%lf
",&c[i][j] );
for ( int i=1; i<=n; i++ )
c[i].val=read();
sort( c+1,c+1+n ); int cnt=0; ll ans=0;
for ( int i=1; i<=n; i++ )
for ( int j=1; j<=m; j++ )
{
if ( fabs(c[i][j])<eps ) continue;
if ( !a[j] ) { a[j]=i; cnt++; ans+=c[i].val; break; }
db tmp=1.0*c[i][j]/c[a[j]][j];
for ( int k=j; k<=m; k++ )
c[i][k]-=tmp*c[a[j]][k];
}
printf( "%d %lld
",cnt,ans );
return 0;
}