参考资料
《算法竞赛进阶指南》
异或空间线性基
应用背景
现在给你 (n) 个数 ({a_i}),它们可以 (mathrm{xor}) 出很多数。
我们就想,是不是可以换一个数集,使得它 (mathrm{xor}) 出来的数集和 ({a_i}) (mathrm{xor}) 出来的数集是一样的。
定义
若一个数 (b) 能由整数 (a_1,a_2,ldots,a_k) (mathrm{xor}) 出,则称 (b) 可以由(a_1,a_2,ldots,a_k) 表示。
(a_1,a_2,ldots,a_k) 能表示出的所有整数构成的数集就是一个异或空间,(a_1,a_2,ldots,a_k) 是这个异或空间的一个生成子集。
从异或空间中选出一组数,若其中的某个数能由别的数经 (mathrm{xor}) 得出,则这一组数是线性相关的。否则,这一组数是线性无关的。
异或空间的一个基是异或空间的一个线性无关的生成子集。通常我们选极大的。
一些性质
- 线性基最高位互不相同。
- 线性基异或空间里每个元素的表示方案数唯一。
- 线性基的任何一个非空子集的 (mathrm{xor}) 值都不为 (0)。
证明: 倘若有这么一个非空子集 (a_1,a_2,ldots,a_j),则 (a_1 mathrm{xor} a_2 mathrm{xor}cdots mathrm{xor} a_j=0),即 (a_j = a_1 mathrm{xor} a_2 mathrm{xor} cdots mathrm{xor} a_{j-1})。这说明这个子集不是线性无关的。这个线性基不是线性无关的。证毕。
构造
若干数的线性基是一组数 (a_1,a_2,ldots,a_k),其中 (a_i) 的最高位的 (1) 在第 (i) 位。(位从 (0) 开始标号)
我们将这若干数一个一个“塞进”线性基里。
对每个数 (p) 从高位往低位扫,扫到第 (x) 位为 (1) 时,若:
- (a_x) 不存在:(a_x leftarrow p),结束扫描。
- (a_x) 存在:(p leftarrow p mathrm{xor} a_x),继续扫描。
(p) 要么进去了,要么成 (0) 被抛弃了。所以 (0) 是不能插入线性基的qwq。
代码:
for(int i=1; i<=n; i++){
for(int j=63; j>=0; j--)
if(p&(1ll<<j)){
if(!ji[j]){
ji[j] = p;
break;
}
p ^= ji[j];
}
}
查询
某数是否存在于异或空间中
从高到低扫描 (p) 的二进制位。
若 (i) 位为 (1),则令 (p leftarrow a_i)。
若中途 (p) 变为 (0),则说明 (p) 存在于异或空间中。
查询异或空间最大值(SGU275)
从高位到低扫描线性基。若 (mathrm{xor}) 上可以使结果变大,则 (mathrm{xor}) 上。
这其实是对于 (ans) 有初值也适用的。
若 (ans) 没初值,并且使用和下面的第 (k) 小一样的线性基构造法的话,全异或起来也是答案。
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
int n;
ll uu, ji[75], ans;
int main(){
cin>>n;
for(int i=1; i<=n; i++){
scanf("%lld", &uu);
for(int k=62; k>=0; k--)
if(uu&(1ll<<k)){
if(ji[k]) uu ^= ji[k];
else{
ji[k] = uu;
break;
}
}
}
for(int i=62; i>=0; i--)
if((ans^ji[i])>ans)
ans ^= ji[i];
cout<<ans<<endl;
return 0;
}
查询异或空间最小值
位权最低的那个线性基。
long long queryMin(){
for(int i=0; i<=62; i++)
if(ji[i])
return ji[i];
return 0;
}
查询异或空间第 (k) 小值
先明确一点,异或空间是允许 (a_i mathrm{xor} a_i) 存在的,因此异或空间必定含 (0),但是一般的题目里头都是不允许这样的。
因此,我们需要改造一下上述线性基。使他们达到“对于每个线性基,记它的为 (1) 的最高的那个二进制位为 (x),别的线性基的第 (x) 位都不是 (1)”这样一种境界。为什么这么做下面会讲。
例如,对于整数 ({5,12,2,7,9}),它们写下来是
最终达到的境界是
为什么要这么做呢?我们不妨把这些线性基排个升序,则显然,异或上一个线性基一定比不异或上他大,这就是这种构造法的目的。
这样,我们就可以把 (k) 二进制拆分,根据 (k) 的二进制的每一位来决定是否要异或上它对应的线性基。
再回过头考虑 (0) 的事。我们发现,如果按照上述步骤操作,那么 (0) 在异或空间里一定是第 (0) 小的。因此,如果原数集可以 (mathrm{xor}) 出 (0),就把 (k leftarrow k-1),把第 (1) 小记为第 (0) 小;否则,就拿 (k) 做。这样第 (1) 小就是选上最小的那个线性基,也是正确的。
怎样判断原数集可不可以 (mathrm{xor}) 出 (0) 呢?要是他对应的简化阶梯形矩阵有全 (0) 行,就是可以 (mathrm{xor}) 出 (0) 的。这等价于线性基的大小与原数集的大小相等。
当然,还有一种可能是 (k) 大过了异或空间的大小。当原数集可以 (mathrm{xor}) 出 (0)时,异或空间有 (2^t) 个数,(k) 过大就是 (k > 2^t)。由于 (k) 要减一,就成了 (k geq 2^t)。当原数集不可以 (mathrm{xor}) 出 (0)时,异或空间有 (2^t-1) 个数,(k) 过大就是 (k geq 2^t)。发现这两个公式统一了,因此在考虑完 (0) 后判定无解就是 (k geq 2^t)。
代码(hdu3949):
#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
using namespace std;
typedef long long ll;
int T, n, q, cnt;
ll ji[75], uu, ans;
vector<int> vec;
int main(){
cin>>T;
for(int ii=1; ii<=T; ii++){
vec.clear();
printf("Case #%d:
", ii);
cnt = 0;
memset(ji, 0, sizeof(ji));
scanf("%d", &n);
for(int i=1; i<=n; i++){
scanf("%lld", &uu);
for(int j=62; j>=0; j--)
if(uu&(1ll<<j)){
if(ji[j]) uu ^= ji[j];
else{
ji[j] = uu;
cnt++;
for(int k=j-1; k>=0; k--)
if(ji[k] && (ji[j]&(1ll<<k)))
ji[j] ^= ji[k];
for(int k=j+1; k<=62; k++)
if(ji[k] && (ji[k]&(1ll<<j)))
ji[k] ^= ji[j];
break;
}
}
}
for(int i=0; i<=62; i++)
if(ji[i])
vec.push_back(ji[i]);
scanf("%d", &q);
while(q--){
ans = 0;
scanf("%lld", &uu);
if(cnt!=n) uu--;
if(uu>=(1ll<<cnt)) ans = -1;
else
for(int i=cnt-1; i>=0; i--)
if((uu&(1ll<<i)))
ans ^= vec[i];
printf("%lld
", ans);
}
}
return 0;
}
可重异或空间
上述讨论都是不可重异或空间。事实上,如果有 (n) 个整数,他们有一组线性基 (mathcal{B}),那么记 (n) 个整数的所有子集的 (mathrm{xor}) 值组成了一个可重集 (mathcal{A}),(mathcal{B}) 的所有子集的 (mathrm{xor}) 值组成了一个可重集 (mathcal{C}),则 (mathcal{C}) 中的每个元素在 (mathcal{A}) 中出现了 (2^{n-|mathcal{B}|}) 次。
例题
bzoj2460 [BeiJing2011]元素
显而易见每个矿石至多选一个。且这些矿石的编号不能有 (mathrm{xor}) 起来为 (0) 的。想到依照魔力值排序后依次插入编号,构造出一组极大线性基。
#include <algorithm>
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
int n, ans;
ll ji[75];
struct Node{
int val;
ll num;
}nd[1005];
bool cmp(Node x, Node y){
return x.val>y.val;
}
int main(){
cin>>n;
for(int i=1; i<=n; i++)
scanf("%lld %d", &nd[i].num, &nd[i].val);
sort(nd+1, nd+1+n, cmp);
for(int i=1; i<=n; i++){
for(int j=63; j>=0; j--)
if(nd[i].num&(1ll<<j)){
if(!ji[j]){
ji[j] = nd[i].num;
ans += nd[i].val;
break;
}
nd[i].num ^= ji[j];
}
}
cout<<ans<<endl;
return 0;
}
luogu4151 [WC2011]最大XOR和路径/bzoj2115 [Wc2011] Xor
任选出从 (1) 到 (n) 的一条路径,再找出所有的环的 (mathrm{xor}) 值,对其建立线性基,求出任选路径和一些环的 (mathrm{xor}) 最大值。
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
int n, m, hea[50005], uu, vv, cnt, din;
ll ww, dis[50005], cir[200005], ji[75], ans;
bool vis[50005];
struct Edge{
int too, nxt;
ll val;
}edge[200005];
void add_edge(int fro, int too, ll val){
edge[++cnt].nxt = hea[fro];
edge[cnt].too = too;
edge[cnt].val = val;
hea[fro] = cnt;
}
void dfs(int x, int f){
vis[x] = true;
for(int i=hea[x]; i; i=edge[i].nxt){
int t=edge[i].too;
if(t==f) continue;
if(!vis[t]){
dis[t] = dis[x] ^ edge[i].val;
dfs(t, x);
}
else cir[++din] = dis[x] ^ dis[t] ^ edge[i].val;
}
}
int main(){
cin>>n>>m;
for(int i=1; i<=m; i++){
scanf("%d %d %lld", &uu, &vv, &ww);
add_edge(uu, vv, ww);
add_edge(vv, uu, ww);
}
dfs(1, 0);
for(int i=1; i<=din; i++)
for(int j=62; j>=0; j--)
if((cir[i]>>j)&1){
if(ji[j]) cir[i] ^= ji[j];
else{
ji[j] = cir[i];
break;
}
}
ans = dis[n];
for(int i=62; i>=0; i--)
if((ans^ji[i])>ans)
ans ^= ji[i];
cout<<ans<<endl;
return 0;
}
向量空间线性基
其实也很简单
loj2108/luogu3265/bzoj4004 「JLOI2015」装备购买
#include <algorithm>
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
typedef long double ld;
int n, m, ans, uu, cnt;
const ld eps=1e-6;
ld ji[505][505];
bool vis[505];
struct Node{
int val;
ld num[505];
}nd[505];
bool cmp(Node x, Node y){
return x.val<y.val;
}
void qwqqwqqwq(){
for(int i=1; i<=n; i++)
for(int j=m; j>=1; j--)
if(fabs(nd[i].num[j])>eps){
if(!vis[j]){
vis[j] = true;
ans += nd[i].val;
cnt++;
for(int k=j; k>=1; k--)
ji[j][k] = nd[i].num[k];
break;
}
ld tmp=nd[i].num[j]/ji[j][j];
for(int k=j; k>=1; k--)
nd[i].num[k] -= tmp * ji[j][k];
}
}
int main(){
cin>>n>>m;
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
scanf("%d", &uu), nd[i].num[j] = uu;
for(int i=1; i<=n; i++)
scanf("%d", &nd[i].val);
sort(nd+1, nd+1+n, cmp);
qwqqwqqwq();
cout<<cnt<<" "<<ans<<endl;
return 0;
}