先说明一点,ai 表示一个标量,而加粗的 ai 表示一个向量,以便于区分。
概述
基(basis)是线性代数中的一个概念,它是描述、刻画向量空间的基本工具。而在现行的 OI 题目中,通常在利用基在异或空间中的一些特殊性质来解决题目,而这一类题目所涉及的知识点被称作「线性基」。
预备知识
这里有一些线性代数的基本知识,以便更好的理解基的概念。
向量空间(vector space)
定义 (F,V,+,⋅) 为向量空间(vector space),其中 F 为域,V 为集合,V 中元素称为向量,+ 为向量加法,⋅ 为标量乘法,且运算满足 8 条公理(见维基百科)。
线性无关(linearly independent)
对于向量空间中 V 上 n 个元素的向量组 (v1,…,vn),若存在不全为 0 的数 ai∈F,满足
a1v1+a2v2+…+anvn=0则称这 n 个向量线性相关(linearly dependent),否则称为线性无关(linearly independent)。
线性组合(linear combination)
对于向量空间中 V 上 n 个元素的向量组 (v1,…,vn),其线性组合(linear combination)是如下形式的向量
a1v1+a2v2+…+anvn其中 a1,…,an∈F。
一组向量线性无关 ⇔ 没有向量可用有限个其他向量的线性组合所表示
张成(span)
对于向量空间中 V 上 n 个元素的向量组 (v1,…,vn),其所有线性组合所构成的集合称为 (v1,…,vn) 的张成(span),记为 span(v1,…,vn)。
基(basis)
若向量空间 V 中向量组 B 既是线性无关的又可以张成 V,则称其为 V 的基(basis)。
B 中的元素称为基向量。如果基中元素个数有限,就称向量空间为有限维向量空间,将元素的个数称作向量空间的维数。
性质
设 B 是向量空间 V 的基。则 B 具有以下性质:
- V 是 B 的极小生成集,就是说只有 B 能张成 V,而它的任何真子集都不张成全部的向量空间。
- B 是 V 中线性无关向量的极大集合,就是说 B 在 V 中是线性无关集合,而且 V 中没有其他线性无关集合包含它作为真子集。
- V 中所有的向量都可以按唯一的方式表达为 B 中向量的线性组合。
第三点尤其重要,感性的理解,基就是向量空间中的一个子集,它可以通过唯一的线性组合,来张成向量空间中所有的向量,这样就可以大大的缩小我们向量空间的大小。
线性相关性引理(Linear Dependent Lemma)
如果 (v1,…,vn) 在 V 中是线性相关的,并且 v1≠0,则有至少一个 j∈{2,…,m} 使得下列成立:
- vj∈span(v1,…,vj−1);
- 如果从 (v1,…,vn) 去掉第 j 项,则剩余向量组的张成仍然等于 span(v1,…,vn)。
证明:设 (v1,…,vn) 在 V 中是线性相关的,并且 v1≠0,则有不全为 0 的 a1,…,an∈F,使得
a1v1+…+amvm=0a2,a3,…,an 不会全为 0(因为 v1≠0)。设 j 是 {2,…,m} 中使得 aj≠0 的最大者,那么
vj=−aja1v1−…−ajaj−1vj−1这就有 (1) 成立。
为了证明 (2),设 u∈span(v1,…,vn),则存在 c1,…,cn∈F,使得
u=c1v1+…+cnvn在上面的等式中,可以用之前的等式右边来代替 vj。这样 u 包含于从 (v0,…,vn) 去掉第 j 项的张成,因而 (2) 成立。
OI 中的线性基
异或运算下的基
求法
对于数 a0,a1,…,an,将 ai 的二进制表示 (bm…b0)2 看作一个向量 ai=(bm,…,b0),为了叙述上的方便,下文称向量 ai的第 j 位为 bj。
向量组 a1,…,an 可以张成一个向量集合 span(a1,…,an),加上我们的异或运算和乘法运算(显然满足 8 条公理),即可形成一个向量空间 V=({0,1},span(a1,…,an),⊕,⋅)。
我们考虑求出向量空间 V 的一个基 B,从 B=(a1,…,an) 开始。
第 1 步:如果 a1=0,则从 B 中去掉 a1,否则保持 B 不变。
第 j 步:若 aj∈span(a1,…,aj−1),则从 B 中去掉 aj,否则保持 B 不变。
经过 n 步后终止程序,得到一个向量组 B。由于每一次去掉的向包含于前面诸向量的张成,到最后这个组 B 仍然可以张成 V。而且这一程序确保了 B 中的任何向量都不包含与它前面诸向量的张成,根据线性相关性引理可知 B 是线性无关的。于是 B 是 V的一个基。
利用高斯消元来判断向量能否被前面的向量张成,就可以写出下面的程序:
void cal() {
for (int i = 0; i < n; ++i)
for (int j = MAX_BASE; j >= 0; --j)
if (a[i] >> j & 1) {
if (b[j]) a[i] ^= b[j];
else {
b[j] = a[i];
for (int k = j - 1; k >= 0; --k) if (b[k] && (b[j] >> k & 1)) b[j] ^= b[k];
for (int k = j + 1; k <= MAX_BASE; ++k) if (b[k] >> j & 1) b[k] ^= b[j];
break;
}
}
这个程序实现的非常精妙,我们每次维护一个对角矩阵。执行到第 i 步的时候,我们从高到低考虑数 ai 为 1 的二进制位 j,如果 j这一行的对角线已经为 1 了,那么我们不能加入,同时为了保持上三角性质,需要将第 j 行的行向量异或到 ai;如果 j 这一行的对角线为 0,那么我们就可以将 ai 添加到这一行,同时为了维护一个对角矩阵,要先用下面的行消自己,再用自己消上面的行。
如果一个向量 ai 能被 a1,…,ai−1 张成,它不应添加进 B,在高斯消元的过程中它必然是已经存在的行向量的线性组合,所以这个方程实际上是多余的,它最后一定会被异或为一个 0。反之如果向量 ai 不能被 a1,…,ai−1 张成,那么它一定能找到某一个行添加进去。
我们来模拟下这个过程,n=5,a={7,1,4,3,5}。一开始矩阵是这样的:
⎣⎡000000000⎦⎤加入 7=(111)2,矩阵变为:
⎣⎡100100100⎦⎤加入 1=(001)2,添加到最后一行,同时为了维护对角矩阵,消去第一行的最低位,矩阵变为:
⎣⎡100100001⎦⎤加入 4=(100)2,由于第一行已经有数了,它被异或为 (010)2,加入第二行,同时为了维护对角矩阵,消去第一行的第二位,矩阵变为:
⎣⎡100010001⎦⎤剩下的数都加不上了。
这样所有被选上的 ai 构成一个向量空间 V 的一个基 B。同时我们知道高斯消元最后得到的矩阵 b 是 B 中的向量构成的矩阵进行若干初等行变换得到的矩阵,而任意初等行变换不会影响向量之间的线性无关性,且任意初等行变换过后,这些向量仍然能够张成原有的向量空间(不难证明)。所以,所有非 0 的 bi 仍然构成向量空间 V 的一个基。
大家所称的「线性基」一般都指这个方式得到的基,因为这个基具有一个独特的性质,可以应用到 OI 题目中。所以我们一般谈论的线性基,特指高斯消元解出的对角矩阵的非零行构成的向量组。
性质
对于最后得到的矩阵,如果第 i 的主对角线上为 1,此时我们称第 i 位存在于线性基中。对于存在于线性基的二进制位,有一个重要的性质:
对于任意存在于线性基的二进制位 i,至多只有一个 bj 满足第 i 位为 1。
证明:高斯消元的过程中,我们维护了一个对角矩阵,如果二进制位 i 存在于一个向量 bj 上,那么 bj 它一定消去了别的向量第 i位上的 1,故二进制位 i 只存在于 bj 上。
自然,对于不在线性基中的二进制位 i,那么第 i 行主对角线下方全为 0,而主对角线上方就可能有若干个 1。
注意
上述高斯消元过程是消成了一个对角矩阵,如果消成上三角矩阵,虽然不具备这个性质,但是仍然能知道那些二进制位 i 存在于线性基中,有时为了代码的简便,只消成上三角矩阵。下文不做区分,请自行判断。
下面例题中用 V 代指将题目中所有数的张成与异或运算和乘法运算构成的向量空间。
例一(SGU 275)
给定 n(1≤n≤100000) 个数 a1,a2,…,an,请问这些数能够组成的最大异或和是什么?
分析
我们求出向量空间 V 的一组线性基。则答案就是将线性基中所有向量异或起来得到的向量所对应的数。
考虑用归纳法证明。因为最高的二进制位只存在于最大的基向量上(用向量所代表的二进制数来比大小),所以最大的基向量肯定要选。接着假设前 i 大的都需要选,考虑第 i+1 大的基向量选不选。显然 i+1 大的基向量能对异或和贡献它的最高的二进制位 j,因为二进制位 j 在之前的异或和中必然为 0(根据性质,j 只存在于第 i+1 大的基向量中)。如果不选,之后所有数对答案的贡献都只能在小于这个二进制位的地方做贡献,总是比选 i+1 得到的答案小,所以这个数必须得选。
例二(HDOJ 3949)
给定 n(n≤10000) 个数 a1,a2,…,an,以及 Q(Q≤10000) 个询问,每次询问这些数(至少一个,不能不选)能够组成的异或和中第 k 小的数是什么(去掉重复的异或和)。
分析
我们求出向量空间 V 的一组线性基 B。因为向量空间中的任意向量 u 均可以被表示称 B 中向量的唯一线性组合,所以 B 中的任意非空子集都可以构成一个向量且互不重复。所以这些数能够组成的异或和的个数为 2∣B∣−1,特别的,如果 ∣B∣<n,则必然存在一个向量组满足线性相关性,这个向量组也就能通过线性组合,异或得到 0,则异或和的个数为 2∣B∣。
假设线性基中有 m 个基向量,从小到大依次为 (v0,…,vm−1),则第 k=(bx…b0)2 小的数就是:
⊕0≤i≤xbi⋅vi不难用二进制的思想证明。
// Created by Sengxian on 2016/12/5.
// Copyright (c) 2016年 Sengxian. All rights reserved.
// HDOJ 3949 线性基
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll readLL() {
static ll n;
static int ch;
n = 0, ch = getchar();
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) n = n * 10 + ch - '0', ch = getchar();
return n;
}
const int MAX_N = 100000 + 3, MAX_BASE = 60;
int n, zero = false;
ll a[MAX_N], b[MAX_BASE + 3];
vector<ll> mmap;
void prepare() {
int cnt = 0;
memset(b, 0, sizeof b);
for (int i = 0; i < n; ++i)
for (int j = MAX_BASE; j >= 0; --j)
if (a[i] >> j & 1) {
if (b[j]) a[i] ^= b[j];
else {
b[j] = a[i], cnt++;
for (int k = j - 1; k >= 0; --k) if (b[k] && ((b[j] >> k) & 1)) b[j] ^= b[k];
for (int k = j + 1; k <= MAX_BASE; ++k) if ((b[k] >> j) & 1) b[k] ^= b[j];
break;
}
}
zero = cnt != n;
mmap.clear();
for (int i = 0; i <= MAX_BASE; ++i)
if (b[i]) mmap.push_back(b[i]);
}
ll query(ll k) {
if (zero) k--;
if (k >= (1LL << (int)mmap.size())) return -1;
ll ans = 0;
for (int i = 0; i < (int)mmap.size(); ++i) if ((k >> i) & 1)
ans ^= mmap[i];
return ans;
}
int main() {
#ifdef DEBUG
freopen("test.in", "r", stdin);
#endif
int caseNum = readLL();
for (int t = 1; t <= caseNum; ++t) {
n = readLL();
for (int i = 0; i < n; ++i) a[i] = readLL();
prepare();
printf("Case #%d:\n", t);
int q = readLL();
for (int i = 0; i < q; ++i) printf("%lld\n", query(readLL()));
}
return 0;
}
例三(BZOJ 2115)
给定一个 n(n≤50000) 个点 m(m≤10000) 条边的无向图,每条边上有一个权值。请你求一条从 1 到 n 的路径,使得路径上的边的异或和最大。
分析
任意一条 1 到 n 的路径的异或和,都可以由任意一条 1 到 n 路径的异或和与图中的一些环的异或和来组合得到。
为什么?假如我们已经有一条 1 到 n 的路径,考虑在出发之前,先走到图中任意一个环上面,走一遍这个环,然后原路返回,这样我们既得到了这个环的异或值(走到环的路径被走过了 2 次,抵消了),也返回了点 1。我们可以对任意的环这样做,从而获得这个环的异或值。有了这个性质,不难验证上述结论是正确的。
现在的解题思路就非常明确了,首先找出所有的环(利用 DFS 树中的返祖边来找环),然后找一条任意的 1 到 n 的路径,其异或值为 s。则我们就需要选择若干个环,使得这些这些环上的异或值与 s 异或起来最大。这就转化为线性基的问题了。
求出所有环的异或值的线性基,由于线性基的良好性质,只需要从大到小考虑选择每个线性基向量能否使得异或值更大即可,容易用贪心证明正确性。
证明:从高到低考虑每个二进制位,设当前的答案为 s,考虑到第 k 位,线性基向量中代表二进制位 k 的向量为 v。那么对于第 k位,一共有三种情况,我们分别考虑我们的选择原则是不是正确的。
- s 中第 k 位是 1,v 中第 k 位是 1,实际上不能选。根据我们的选择原则,此时异或起来答案一定会变小,不选。正确。
- s 中第 k 位是 0,v 中第 k 位是 1,实际上要选。根据我们的选择原则,此时异或起来答案一定会变大,选。正确。
- v 中第 k 位是 0,那么 v 必定是零向量,选不选无所谓。正确。
所以在每一种情况中,我们的选择原则都是正确的,所以这个贪心也是正确的。
// Created by Sengxian on 2016/12/05.
// Copyright (c) 2016年 Sengxian. All rights reserved.
// BZOJ 2115 线性基
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll readLL() {
static ll n;
static int ch;
n = 0, ch = getchar();
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) n = n * 10 + ch - '0', ch = getchar();
return n;
}
const int MAX_N = 50000 + 3, MAX_M = 100000 + 3, MAX_BASE = 60;
struct edge {
edge *next, *rev;
int to;
ll cost;
edge(edge *next = NULL, int to = 0, ll cost = 0): next(next), to(to), cost(cost) {}
} pool[MAX_M * 2], *pit = pool, *first[MAX_N];
int n, m, cnt = 0;
ll d[MAX_N], a[MAX_N + MAX_M * 2], b[MAX_BASE + 3];
void dfs(int u, edge *fa) {
static bool vis[MAX_N];
vis[u] = true;
for (edge *e = first[u]; e; e = e->next) if (e->rev != fa) {
if (!vis[e->to]) {
d[e->to] = d[u] ^ e->cost;
dfs(e->to, e);
} else a[cnt++] = d[u] ^ d[e->to] ^ e->cost;
}
}
void prepare() {
for (int i = 0; i < cnt; ++i)
for (int j = MAX_BASE; j >= 0; --j)
if (a[i] >> j & 1) {
if (b[j]) a[i] ^= b[j];
else {
b[j] = a[i];
for (int k = j - 1; k >= 0; --k) if (b[k] && (b[j] >> k & 1)) b[j] ^= b[k];
for (int k = j + 1; k <= MAX_BASE; ++k) if (b[k] >> j & 1) b[k] ^= b[j];
break;
}
}
}
int main() {
n = readLL(), m = readLL();
for (int i = 0; i < m; ++i) {
int u = readLL() - 1, v = readLL() - 1;
ll w = readLL();
first[u] = new (pit++) edge(first[u], v, w);
first[v] = new (pit++) edge(first[v], u, w);
first[u]->rev = first[v], first[v]->rev = first[u];
}
dfs(0, NULL);
prepare();
ll ans = d[n - 1];
for (int i = MAX_BASE; i >= 0; --i)
if (ans < (ans ^ b[i])) ans ^= b[i];
printf("%lld\n", ans);
return 0;
}
例四(BZOJ 2844)
给定 n(n≤10000) 个数 a1,a2,…,an,以及一个数 Q。将 a1,a2,…,an 的所有子集(可以为空)的异或值从小到大排序得到序列 B,请问 Q 在 B 中第一次出现的下标是多少?保证 Q 在 B 中出现。
分析
首先求出 V 的线性基 B。
如果去除序列 B 中重复的数,使用线性基,根据 Q 的二进制位便可以确定 Q 的排名(使用类似例二的方法)。可是如果不去重,怎么才能知道每个数出现多少次呢?
结论:每个数都出现一样的次数,且这个次数为 2n−∣B∣。
证明:我们考虑在 B 中出现的任意一个数 x。所有不在线性基中的数的个数为 n−∣B∣,我们任意选择它的一个子集 S,对于 S中的每个数 v,有唯一的方式表达为 B 中向量的线性组合。我们对于每个 v,将这个线性组合中的向量都选上(一个向量选多次不要紧),两个相同的数异或起来得到 0,所以对于每个数 x,我们都能找到 2n−∣B∣ 种不同的选择方案,使得异或值为 x。又因为对于每个子集 S,为了使得最终异或值为 x,选择线性基中的向量的方案是唯一的,所以上界也是 2n−∣B∣。这就完成了证明。
这样就只需要一个快速幂就能快速计算答案了。
// Created by Sengxian on 2016/12/07.
// Copyright (c) 2016年 Sengxian. All rights reserved.
// BZOJ 2844 线性基
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int readInt() {
static int n, ch;
n = 0, ch = getchar();
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) n = n * 10 + ch - '0', ch = getchar();
return n;
}
const int MAX_N = 100000 + 3, MAX_BASE = 30, MOD = 10086;
int n, a[MAX_N], b[MAX_BASE + 1], Q;
inline int mod_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 = readInt();
for (int i = 0; i < n; ++i) a[i] = readInt();
Q = readInt();
int cnt = 0, rnk = 0;
for (int i = 0; i < n; ++i)
for (int j = MAX_BASE; j >= 0; --j)
if (a[i] >> j & 1) {
if (b[j]) a[i] ^= b[j];
else {
b[j] = a[i];
cnt++;
break;
}
}
vector<int> vec;
for (int i = 0; i <= MAX_BASE; ++i) if (b[i]) vec.push_back(i);
for (int i = 0; i < (int)vec.size(); ++i) if (Q >> vec[i] & 1)
rnk += 1 << i;
printf("%d\n", (rnk % MOD * mod_pow(2, n - cnt) % MOD + 1) % MOD);
return 0;
}
例五
魔法之龙玛里苟斯最近在为加基森拍卖师的削弱而感到伤心,于是他想了一道数学题:S 是一个可重集合,S={a1,a2,…,an}。等概率随机取 S 的一个子集 A={ai1,…,aim},计算出 A 中所有元素的异或值 x, 求 xk(1≤k≤5) 的期望。
保证答案不超过 264。
分析
这种题需要求异或值的和,通常的方法是求出线性基之后,考虑每一位的贡献。
我们对每一个 k 分别考虑。
k=1 时,由于贡献是线性的,所以我们可以对每一个二进制位分别考虑。我们考虑二进制位 i,如果在某一个 aj 中,存在二进制位 i 为 1,那么子集的异或和中二进制位 i 为 1 的概率为 21。如果不存在这样的 aj,概率为 0。证明很容易,因为子集中有奇数个或者偶数个 aj 二进制位 i 为 1 的概率是一样的,而只有奇数个 aj 二进制位 i 为 1 才能满足异或和中二进制位 i 为 1。
k=2 时,我们求的是期望的平方,即每个异或和的贡献为 (bmbm−1…b0)2⋅(bmbm−1…b0)2,写成和式就是
i∑j∑bjbi⋅2i+j我们需要枚举两个二进制位,现在每个数变成了 (0/1,0/1) 二元组,仅当异或后得到 (1,1),才会产生 2i+j 的贡献。根据 k=1的情况不难发现,有 41 的概率得到 (1,1)。需要特判的是,如果所有数都是 (1,1) 或者 (0,0) 且至少有一个 (1,1),那么概率为 21。如果所有数都是 (0,0),那么概率为 0。
k≥3 时,由于答案不超过 264,所以每个数不超过 222,这些数的线性基不会超过 22 个,所以我们可以考虑求出线性基,然后暴力枚举线性基的子集即可。
虽然答案不会溢出,但是中间过程是有可能是溢出的,为了防止溢出,在 k≠1 时,若除数为 2m,那么我们在乘的过程中,将答案记录为 y=⌊2my⌋⋅2m+ymod2m 的形式,这样两个项都不会超过 264,可以计算了。
现在我们考虑输出小数的问题,当 k=1 时显然小数位要么是 0 要么是 0.5;而 k≠1 时这个结论仍然成立(虚心求证明),所以特判一下就好了。
// Created by Sengxian on 2016/12/10.
// Copyright (c) 2016年 Sengxian. All rights reserved.
// BZOJ 3811 线性基
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ll;
inline ll readLL() {
static ll n;
static int ch;
n = 0, ch = getchar();
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) n = n * 10 + ch - '0', ch = getchar();
return n;
}
const int MAX_N = 100000 + 3, MAX_BASE = 23;
int n, K;
ll a[MAX_N], b[MAX_N];
void solve1() {
ll res = 0;
for (int i = 0; i < n; ++i) res |= a[i];
printf("%llu", res / 2);
if (res & 1) puts(".5");
else puts("");
}
void solve2() {
ll ans = 0, res = 0;
for (int i = 0; i < 32; ++i)
for (int j = 0; j < 32; ++j) {
bool flag = false;
for (int k = 0; k < n; ++k) if (a[k] >> i & 1) { flag = true; break; }
if (!flag) continue;
flag = false;
for (int k = 0; k < n; ++k) if (a[k] >> j & 1) { flag = true; break; }
if (!flag) continue;
flag = false;
for (int k = 0; k < n; ++k) if ((a[k] >> i & 1) != (a[k] >> j & 1)) { flag = true; break; }
if (i + j - 1 - flag < 0) res++;
else {
if (!flag) ans += 1LL << (i + j - 1); // 1 / 2
else ans += 1LL << (i + j - 1 - 1); // 1 / 4
}
}
ans += res >> 1, res &= 1;
printf("%llu", ans);
if (res) puts(".5");
else puts("");
}
void solve3() {
vector<int> vec;
for (int i = 0; i < n; ++i)
for (int j = MAX_BASE; j >= 0; --j)
if (a[i] >> j & 1) {
if (b[j]) a[i] ^= b[j];
else {
b[j] = a[i];
vec.push_back(a[i]);
break;
}
}
int all = vec.size();
ll ans = 0, res = 0;
for (int i = (1 << all) - 1; i >= 0; --i) {
int val = 0;
for (int j = 0; j < (int)vec.size(); ++j) if (i >> j & 1) val ^= vec[j];
ll a = 0, b = 1;
for (int j = 0; j < K; ++j) {
a *= val, b *= val;
a += b >> all, b &= (1 << all) - 1;
}
ans += a, res += b;
ans += res >> all, res &= (1 << all) - 1;
}
printf("%llu", ans);
if (res) puts(".5");
else puts("");
}
int main() {
n = readLL(), K = readLL();
for (int i = 0; i < n; ++i) a[i] = readLL();
if (K == 1) solve1();
else if (K == 2) solve2();
else solve3();
return 0;
}
总结
线性基的题型相对比较固定,看到下面的类型基本上都是线性基了:
- 最大异或和
- 第 k 大异或和/异或和是第几大
- 求所有异或值的和
线性基中的题目中还用到一个技巧:
- 任意一条 1 到 n 的路径的异或和,都可以由任意一条 1 到 n 路径的异或和与图中的一些环的异或和来组合得到。
这便是线性基的全部东西了。