线性基略解
参考资料
《算法竞赛进阶指南》
异或空间线性基
应用背景
现在给你 \(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:\n", 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\n", 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;
}