线性基
简介
定义
对于一组数
a
1
,
…
,
a
n
a_1,\ldots,a_n
a1,…,an,相应的线性基为
P
1
,
…
,
P
m
P_1,\ldots,P_m
P1,…,Pm ,其中
P
i
P_i
Pi 是最高位的1出现在第 i 位的数 。
线性基是一种特殊的基,它通常会在异或运算中出现,它的意义是:通过原集合
S
S
S的某一个最小子集
S
1
S1
S1,使得
S
1
S1
S1内元素相互异或得到的值域与原集合S相互异或得到的值域相同。
性质
1.线性基能相互异或得到原集合的所有相互异或得到的值。
2.线性基是满足性质1的最小的集合
3.线性基没有异或和为0的子集。
证明:
设线性基
S
S
S = {
a
1
,
a
2
,
…
,
a
n
a_1,a_2,\ldots,a_n
a1,a2,…,an};
若有子集
a
1
⊕
a
2
⊕
…
⊕
a
t
a_{1} \oplus a_{2} \oplus \ldots \oplus a_{t}
a1⊕a2⊕…⊕at =
0
0
0,则
a
1
a_{1}
a1=
a
2
⊕
a
3
⊕
…
⊕
a
t
a_{2} \oplus a_{3} \oplus \ldots \oplus a_{t}
a2⊕a3⊕…⊕at,则舍弃
a
1
a_{1}
a1后一定能通过剩余的元素异或出
a
1
a_{1}
a1。设
Y
=
a
1
⊕
X
Y=a_{1} \oplus X
Y=a1⊕X,因为
a
1
,
a
2
,
…
,
a
n
{a_1,a_2,\ldots,a_n}
a1,a2,…,an是一组线性基,
X
X
X 一定能由
a
2
,
…
,
a
n
a_2, \ldots,a_n
a2,…,an中相互异或得来。
Y
Y
Y=
a
1
⊕
X
a_1 \oplus X
a1⊕X =
a
2
⊕
a
3
⊕
…
⊕
a
t
⊕
X
a_2 \oplus a_3 \oplus \ldots \oplus a_t \oplus X
a2⊕a3⊕…⊕at⊕X,
a
2
,
…
,
a
t
⊕
Y
a_2,\ldots,a_t \oplus Y
a2,…,at⊕Y也能得到
Y
Y
Y,所以
a
1
a_1
a1于线性基无用,与线性基是最小子集的定义矛盾。
所以,线性基没有异或和为0的子集。
预备知识
向量空间(vector space):
定义
(
F
,
V
,
+
,
⋅
(F, V, +, \cdot
(F,V,+,⋅)为向量空间(vector space),其中
F
F
F 为域,
V
V
V 为集合,
V
V
V 中元素称为向量,+ 为向量加法,
⋅
\cdot
⋅ 为标量乘法,且运算满足 8 条公理:
设 F F F是一个域。一个 F F F上的向量空间是一个集合 V V V的两个运算:
向量加法: V + V → V V + V → V V+V→V, 记作 v + w v + w v+w, ∃ v , w ∈ V ∃ v, w∈V ∃v,w∈V
标量乘法: F × V → V F × V → V F×V→V, 记作 a ⋅ v , ∃ a ∈ F , v ∈ V a·v, ∃a∈F, v∈V a⋅v,∃a∈F,v∈V
符合下列公理 ( ∀ a , b ∈ F 及 u , v , w ∈ V ) (∀ a, b ∈ F 及 u, v, w ∈ V) (∀a,b∈F及u,v,w∈V):
1.向量加法结合律: u + ( v + w ) = ( u + v ) + w u + (v + w) = (u + v) + w u+(v+w)=(u+v)+w;
2.向量加法交换律: v + w = w + v v + w = w + v v+w=w+v;
3.向量加法的单位元: V V V 里有一个叫做零向量的 0 0 0, ∀ v ∈ V , v + 0 = v ∀ v \in V , v + 0 = v ∀v∈V,v+0=v;
4.向量加法的逆元素: ∀ v ∈ V , ∃ w ∈ V ∀v \in V, ∃w \in V ∀v∈V,∃w∈V,使得 v + w = 0 v + w = 0 v+w=0;
5.标量乘法分配于向量加法上: a ( v + w ) = a v + a w a(v + w) = a v + a w a(v+w)=av+aw;
6.标量乘法分配于域加法上: ( a + b ) v = a v + b v (a + b)v = a v + b v (a+b)v=av+bv;
7.标量乘法一致于标量的域乘法: a ( b v ) = ( a b ) v a(b v) = (ab)v a(bv)=(ab)v;
8.标量乘法有单位元: 1 v = v 1 v = v 1v=v, 这里 1 是指域 F F F 的乘法单位元。
线性无关(linearly independent)
对于向量空间中
V
V
V 上
n
n
n 个元素的向量组
(
v
1
,
…
,
v
n
)
(\mathbf{v}_1, \ldots, \mathbf{v}_n)
(v1,…,vn),其线性组合(linear combination)是如下形式的向量
a 1 v 1 + a 2 v 2 + … + a n v n , 其 中 a 1 , … , a n ∈ F a_{1}\mathbf{v}_{1} + a_{2}\mathbf {v} _{2}+\ldots +a_{n}\mathbf {v} _{n},其中 a_1, \ldots, a_n \in F a1v1+a2v2+…+anvn,其中a1,…,an∈F。
若存在不全为 0 的数
a
i
∈
F
a_i \in F
ai∈F,满足
a
1
v
1
+
a
2
v
2
+
…
+
a
n
v
n
=
0
a_{1}\mathbf {v} _{1}+a_{2}\mathbf {v} _{2}+\ldots +a_{n}\mathbf {v} _{n} = 0
a1v1+a2v2+…+anvn=0
则称这
n
n
n 个向量线性相关(linearly dependent),否则称为线性无关(linearly independent)。
一组向量线性无关 ⇔ \Leftrightarrow ⇔ 没有向量可用有限个其他向量的线性组合所表示。
基
在线性代数中,基(也称为基底)是描述、刻画向量空间的基本工具。向量空间的基是它的一个特殊的子集,基的元素称为基向量。向量空间中任意一个元素,都可以唯一地表示成基向量的线性组合。如果基中元素个数有限,就称向量空间为有限维向量空间,将元素的个数称作向量空间的维数。
显然,基一定线性无关。
构建方法
对于每一个数,从最高位到最低位扫,我们找出最高位的1出现在第 i 位的数, 如果此时 P i P_i Pi 为零,就将这个数加入线性基,否则异或 P i P_i Pi 继续找。然后我们就可以在 0 到 k 位上处理好每一位的线性基。这样得到的线性基保证每一位都能有对应的最大值。
从高到低考虑每个二进制位,设当前的答案为 a n s ans ans,考虑到第 k 位,线性基向量中代表二进制位 k 的向量为 v \mathbf{v} v。那么对于第 k 位,一共有三种情况,我们分别考虑我们的选择原则是不是正确的。
1. a n s ans ans 中第 k 位是 1, v \mathbf{v} v 中第 k 位是 1,实际上不能选。根据我们的选择原则,此时异或起来答案一定会变小,不选。正确。
2. a n s ans ans 中第 k 位是 0, v \mathbf{v} v 中第 k 位是1,实际上要选。根据我们的选择原则,此时异或起来答案一定会变大,选。正确。
3. v \mathbf{v} v 中第 k 位是 0,那么 v \mathbf{v} v 必定是零向量,选不选无所谓。正确。
应用
洛谷P3812(模板题)
题目描述
给定n个整数(数字可能重复),求在这些数中选取任意个,使得他们的异或和最大。
题解
求出相应的线性基。
P i P_{i} Pi | 10111 | 10111 | 10111 |
---|---|---|---|
P i − 1 P_{i-1} Pi−1 | 01111 | 00000 | 00111 |
异或值 | 11000 | 10111 | 10000 |
第一种情况显然比第三种情况大;虽然第二种情况后三位全为1,但仍没有第一种情况(后三位全为0)大。因为线性基在构建时,相应的第i个数已满足该数二进制最高位1为第i位,1异或0仍为1,从而从高位到低位异或即为最大异或值。
也可以看看下面的证明(copy大佬的)
从高到低考虑每个二进制位,设当前的答案为 a n s ans ans,考虑到第 k 位,线性基向量中代表二进制位 k 的向量为 v \mathbf{v} v。那么对于第 k 位,一共有三种情况,我们分别考虑我们的选择原则是不是正确的。
1. a n s ans ans 中第 k 位是 1, v \mathbf{v} v 中第 k 位是 1,实际上不能选。根据我们的选择原则,此时异或起来答案一定会变小,不选。正确。
2. a n s ans ans 中第 k 位是 0, v \mathbf{v} v 中第 k 位是1,实际上要选。根据我们的选择原则,此时异或起来答案一定会变大,选。正确。
3. v \mathbf{v} v 中第 k 位是 0,那么 v \mathbf{v} v 必定是零向量,选不选无所谓。正确。
代码
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int maxn = 55;
LL a, p[maxn], n, ans;
inline LL read()
{
LL n = 0;
char ch;
while (!isdigit(ch = getchar())) ;
while (isdigit(ch))
n = n*10 + ch-'0', ch = getchar();
return n;
}
int main()
{
n = read();
for (int i = 1; i <= n; i++)
{
a = read();
for (int j = 50; j >= 0; j--)
if (a >> j & 1)
{
if (!p[j])
{
p[j] = a;
break;
}
a ^= p[j];
}
}
for (int j = 50; j >= 0; j--)
ans = max(ans, ans^p[j]);
printf("%lld", ans);
return 0;
}
hdu3949(第K大异或值+高斯消元)
题目描述
输出一组数第K大的异或值(无重复),如果该组数所能表示的异或值小于K,输出-1
题解
我们求出向量空间
V
V
V 的一组线性基
p
\mathfrak{p}
p,不能简单的直接赋值,需要用到高斯消元,使得只有
p
i
\mathfrak{p}_i
pi的第i位为1,其它的都不为1,。因为
p
\mathfrak{p}
p 中有且仅有一组线性组合能表示成向量空间中的任意向量
u
\mathbf{u}
u ,所以
p
\mathfrak{p}
p 中的任意非空子集都可以构成一个向量且互不重复。这些数能够组成的异或和的个数为
2
∣
p
∣
2^{\vert \mathfrak{p}\vert}
2∣p∣ - 1。如果
∣
p
∣
<
n
\vert\mathfrak{p}\vert < n
∣p∣<n,则必然存在一个向量组满足线性相关性,这个向量组也就能通过线性组合,异或得到 0,则异或和的个数为
2
∣
p
∣
2^{\vert \mathfrak{p}\vert}
2∣p∣ 。
假设线性基中有
m
m
m 个基向量,从大到小依次为
(
v
m
−
1
,
…
,
v
0
)
(\mathbf{v}_{m-1}, \ldots, \mathbf{v}_{0})
(vm−1,…,v0),则第 k =
(
b
x
…
b
0
)
2
(b_x\ldots b_0)_2
(bx…b0)2小的数就是:
⊕
0
≤
i
≤
x
b
i
⋅
v
i
\oplus_{0\le i\le x}b_i \cdot v_i
⊕0≤i≤xbi⋅vi,由线性基的性质易知结果的正确性。
代码
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int maxn = 10000+10;
int n, q, t;
LL a[maxn], b[66];
int main()
{
scanf("%d", &t);
for (int i = 1; i <= t; i++)
{
memset(b, 0, sizeof(b));
printf("Case #%d:\n", i);
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%lld", a+i);
for (int i = 1; i <= n; i++)
{
for (int j = 62; j >= 0; j--)
if (a[i] >> j &1)
{
if (!b[j])
{
b[j] = a[i];
//高斯消元,使得只有b[i]的第i为1,其它的都不为1
for (int k = j-1; k >= 0; k--)
if (b[j] >> k & 1)
b[j] ^= b[k];
for (int k = j+1; k <= 62; k++)
if (b[k] >> j & 1)
b[k] ^= b[j];
break;
}
a[i] ^= b[j];
}
}
vector<LL> vec;
for (int i = 0; i <= 62; i++)
if (b[i])
vec.push_back(b[i]);
int cnt = vec.size();
scanf("%d", &q);
while (q--)
{
LL k, ans = 0;
scanf("%lld", &k);
k -= cnt < n;//表示存在0
if (cnt < n && k == 0)
{
puts("0");
continue;
}
if (k >= (1LL << cnt))
{
puts("-1");
continue;
}
for (int i = 0; i < cnt; i++)
if (k >> i & 1)
ans ^= vec[i];
printf("%lld\n", ans);
}
}
return 0;
}
BZOJ 2115(图中最小异或路径)
题目描述
给定一个n节点m条边的无向图。求出图中最小异或路径(存在重边和自环)。
题解
任意一条包含环从1到n包含环的路径的异或和,都可以用任意一条不包含环从1到n的路径的异或和加上图中某些的环的异或和得到。先从起始点1考虑,假设我们直接走图中任意一个环,然后原路返回,由于从起始点到环的路径走过两遍,其异或和不影响相应环的异或和(异或两次结果为0)。可以利用DFS找出所有环,求出其相应的以异或和。
推广到图中的最大路径异或和,就可以用图中1到n路径的最大异或和,异或上图中能使结果更大的环。
代码
#include <bits/stdc++.h>
#define LL long long
const int maxn = 5e4+10;
int n, m, head[maxn*4], tot, t;
LL clique[maxn*4], d[maxn], p[66];
bool vis[maxn];
struct node
{
int v, next;
LL w;
};
node e[4*maxn];
void add(int u, int v, LL w)
{
e[++t] = node{ v, head[u], w };
head[u] = t;
}
void dfs(int u)
{
vis[u] = 1;
for (int i = head[u]; ~i; i = e[i].next)
{
int v = e[i].v;
if (vis[v])
clique[++tot] = d[v] ^ e[i].w ^ d[u];
else
d[v] = e[i].w ^ d[u], dfs(v);
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(head, -1, sizeof(head));
for (int i = 1; i <= m; i++)
{
int s, t;
LL d;
scanf("%d%d%lld", &s, &t, &d);
add(s, t, d);
add(t, s, d);
}
dfs(1); // 找到所有的环
// 根据环的异或和建一个线性基
for (int i = 1; i <= tot; i++)
for (int j = 62; j >= 0; j--)
if (clique[i] >> j & 1)
{
if (!p[j])
{
p[j] = clique[i];
break;
}
clique[i] ^= p[j];
}
for (int i = 62; i >= 0; i--)
if (d[n] < (d[n] ^ p[i]))
d[n] ^= p[i];
printf("%lld", d[n]);
return 0;
}
BZOJ2844(升序异或和集合中的最小下标)
题目描述
给定一个数Q,求Q在一组数升序异或集合中的最小下标。
题解
在集合中每个数都出现一样的次数,且由hdu3949的结论(
p
\mathfrak{p}
p 中的任意非空子集都可以构成一个向量且互不重复。这些数能够组成的异或和的个数为
2
∣
p
∣
2^{\vert \mathfrak{p}\vert}
2∣p∣ - 1。如果
∣
p
∣
<
n
\vert\mathfrak{p}\vert < n
∣p∣<n,则必然存在一个向量组满足线性相关性,这个向量组也就能通过线性组合,异或得到 0,则异或和的个数为
2
∣
p
∣
2^{\vert \mathfrak{p}\vert}
2∣p∣ ),易知这个次数为
2
n
−
∣
p
∣
2^{n - \vert \mathfrak{p}\vert}
2n−∣p∣。
所有的数不在线性基中的个数为 n -
∣
p
∣
\vert \mathfrak{p}\vert
∣p∣,我们任选一个数 v,
p
\mathfrak {p}
p 中向量有唯一的线性组合表示 v 。我们对于每个 v,将这个线性组合中的向量都选上(两个相同的数异或起来得到 0),所以对于每个数 v,我们都能找到
2
n
−
∣
p
∣
2^{n - \vert \mathfrak{p}\vert}
2n−∣p∣种不同的选择方案,使得异或值为 v。
代码
#include <bits/stdc++.h>
using namespace std;
const int mod = 10086;
int n, q, a[40], cnt, ans;
inline int read()
{
int n = 0, ch;
while (!isdigit(ch = getchar()));
while (isdigit(ch))
n = n*10 + ch-'0', ch = getchar();
return n;
}
int pow_(int a, int b)
{
int res = 1;
while (b)
{
if (b&1)
(res *= a) %= mod;
(a *= a) %= mod;
b >>= 1;
}
return res;
}
int main()
{
n = read();
for (int i = 1; i <= n; i++)
{
int t = read();
for (int j = 30; j >= 0; j--)
if (t >> j & 1)
{
if (!a[j])
{
a[j] = t;
cnt++;
break;
}
t ^= a[j];
}
}
vector<int> vec;
q = read();
for (int i = 0; i <= 30; i++)
if (a[i])
vec.push_back(i);
for (int i = 0; i < vec.size(); i++)
if (q >> vec[i] & 1)
ans += 1 << i;
printf("%d\n", (ans % mod * pow_(2, n-cnt) % mod + 1) % mod);
return 0;
}
CF1100F(线性基+离线)
题目描述
有n个汉堡店,每个店里最贵的汉堡是
c
i
c_i
ci元,
q
q
q次询问,从
l
l
l到
r
r
r,所能花费的最多钱数(结束与开始时的金额差),手里有d元,要花费c元,每次支付后,他还有d
⊕
\oplus
⊕ c元。
题解
以
r
r
r为下标,将询问离线插入
Q
[
r
]
Q[r]
Q[r] 数组中。而这时数组元素有个变量定义为
b
b
b ,表示这个元素目前所处的位置。显然当询问到位置
l
l
l时,满足
l
≤
b
l\le b
l≤b,才考虑该位置。
遍历
1
≤
i
≤
n
1 \le i \le n
1≤i≤n ,在第
i
i
i 个汉堡店时,在构造线性基
t
\mathfrak{t }
t 时,将
c
i
c_i
ci,
i
i
i 两个变量赋值到结构体变量
t
e
m
p
temp
temp 的元素
a
a
a 和
b
b
b 中,从高位到低位判断
t
e
m
p
.
a
temp.a
temp.a 的值,需要注意的是,考虑
t
j
t_j
tj 能异或尽量多的汉堡店,故在所有满足需求的线性基中要使
t
[
j
]
.
b
t[j].b
t[j].b 尽可能的大,使
l
≤
t
[
j
]
.
b
l\le t[j].b
l≤t[j].b成立,。当满足
t
j
t_j
tj 存在且
t
[
j
]
.
b
<
t
e
m
p
.
b
t[j].b \lt temp.b
t[j].b<temp.b,就交换位置。遍历
Q
[
i
]
Q[i]
Q[i]数组,如果
Q
[
i
]
[
k
]
.
a
≤
t
[
j
]
.
b
Q[i][k].a \le t[j].b
Q[i][k].a≤t[j].b ,说明
t
j
t_j
tj 满足第
t
m
p
.
b
tmp.b
tmp.b 组的询问,选出最大的
a
n
s
ans
ans 即可。
代码
#include <bits/stdc++.h>
#define LL long long
using namespace std;
inline int read()
{
int n = 0;
char ch;
while (!isdigit(ch = getchar())) ;
while (isdigit(ch))
n = n*10 + ch-'0', ch = getchar();
return n;
}
const int maxn = 5e5 + 10;
const int maxm = 25; // 实际20
int n, c[maxn], q, a, b, ans[maxn];
struct node
{
int a, b;
} t[maxm];
vector<node> Q[maxn];
int main()
{
n = read();
for (int i = 1; i <= n; i++)
c[i] = read();
q = read();
for (int i = 1; i <= q; i++)
{
a = read();
b = read();
Q[b].push_back(node { a, i });
}
for (int i = 1; i <= n; i++)
{
node temp = node { c[i], i };
for (int j = 20; j >= 0; j--)
if (temp.a >> j & 1)
{
if (!t[j].a)
{
t[j] = temp;
break;
}
else if (t[j].b < temp.b)
swap(t[j], temp);
temp.a ^= t[j].a;
}
for (int k = 0; k < Q[i].size(); k++)
{
node tmp = Q[i][k];
// tmp.b为当前询问第几组数据
for (int j = 20; j >= 0; j--)
if (t[j].b >= tmp.a)
ans[tmp.b] = max(ans[tmp.b], ans[tmp.b] ^ t[j].a);
}
}
for (int i = 1; i <= q; i++)
printf("%d\n", ans[i]);
return 0;
}
2019牛客多校第四场B(线段树维护线性基求交)
题目描述
给定n个序列,每个序列sz个数,求
[
l
,
…
,
r
]
[l, \ldots, r]
[l,…,r] 的序列能否表示x 。
题解
建立线段树,区间查询线性基交集。
考虑子节点的线性基
B
1
,
B
2
B_1, B_2
B1,B2 ,显然父节点为
B
1
⋂
B
2
B_1 \bigcap B_2
B1⋂B2 ,从低位到高位遍历
B
2
B_2
B2 的线性基,如果
B
2
B_2
B2 的第
i
i
i 位对应的数在线性基
B
1
B_1
B1 中,就将相应的异或结果赋值到父节点对应的线性基中。
代码
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int maxn = 5e4+10;
const int maxm = 35;
struct node
{
int a[maxm];
};
node tree[maxn << 2];
int n, m, a, b, x;
int size[maxn], v[maxn][maxm];
//成功插入线性基返回1,否则返回0
int insert(int p, int x, int flag)
{
for (int i = 31; i >= 0; i--)
if (x >> i & 1)
{
if (!tree[p].a[i])
{
if (flag)
tree[p].a[i] = x;
return 1;
}
x ^= tree[p].a[i];
}
return 0;
}
void merge(int p, node B1, node B2)
{
node temp;
for (int i = 0; i < 32; i++)
temp.a[i] = B1.a[i], tree[p].a[i] = 0;
for (int i = 0; i < 32; i++)
if (B2.a[i])
{
int t1 = B2.a[i]; // B2的第i位
int t2 = 0; // B1和B2异或交集
for (int j = i; j >= 0; j--)
if (t1 >> j & 1)
{
if (!temp.a[j])
{
temp.a[j] = t1;
B1.a[j] = t2;
break;
}
t1 ^= temp.a[j];
t2 ^= B1.a[j];
// t1在B1中
if (!t1)
{
tree[p].a[i] = t2;
break;
}
}
}
}
void build(int p, int l, int r)
{
if (l == r)
{
for (int i = 0; i < size[l]; i++)
insert(p, v[l][i], 1);
return ;
}
int mid = (l+r) >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid+1, r);
merge(p, tree[p<<1], tree[p<<1|1]);
}
int query(int p, int l, int r)
{
if (a <= l && b >= r)
return !insert(p, x, 0);
int mid = (l+r) >> 1;
if (a <= mid && !query(p<<1, l, mid))
return 0;
if (b > mid && !query(p<<1|1, mid+1, r))
return 0;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
{
scanf("%d", size+i);
for (int j = 0; j < size[i]; j++)
scanf("%d", &v[i][j]);
}
build(1, 1, n);
while (m--)
{
scanf("%d%d%d", &a, &b, &x);
printf("%s\n", query(1, 1, n) ? "YES" : "NO");
}
}