线性基简介
线性基是一种擅长处理异或问题的数据结构.设值域为 \([1,N]\) ,就可以用一个长度为 $⌈\log_2{N}⌉ $ 的数组来描述一个线性基。特别地,线性基第\(i\) 位上的数二进制下最高位也为第\(i\)位。
一个线性基满足,对于它所表示的所有数的集合\(S\),\(S\)中任意多个数异或所得的结果均能表示为线性基中的元素互相异或的结果,即意,线性基能使用异或运算来表示原数集使用异或运算能表示的所有数。运用这个性质,我们可以极大地缩小异或操作所需的查询次数。
插入和判断
我们考虑插入的操作,令插入的数为\(x\),考虑\(x\)的二进制最高位\(i\),
-
若线性基的第\(i\)位为\(0\),则直接在该位插入\(x\),退出;
-
若线性基的第\(i\)位已经有值\(a_i\) ,则\(x = x ⊕ a_i\),重复以上操作直到\(x=0\)。
如果退出时\(x=0\),则此时线性基已经可以表示原先的\(x\)了;
反之,则说明为了表示\(x\),往线性基中加入了一个新元素。
很容易证明这样复杂度为\(\log_2{N}\),也可以用这种方法判断能否通过原数列异或得到一个数\(x\)。
inline void Insert (register lld x) {
for (register int i = 63; i >= 0; --i)
if (x & (1ll << i))
if (!a[i]) {
a[i] = x;
break;
}
else x ^= a[i];
}
inline bool Check (register lld x) {
for (register int i = 63; i >= 0; --i)
if (x & (1ll << i))
if (!a[i]) return false;
else x ^= a[i];
return true;
}
查询异或最值
查询最小值相对比较简单。考虑插入的过程,因为每一次跳转操作,\(x\)的二进制最高位必定单调降低,所以不可能插入两个二进制最高位相同的数。而此时,线性基中最小值异或上其他数,必定会增大。所以,直接输出线性基中的最小值即可。
考虑异或最大值,从高到低遍历线性基,考虑到第\(i\)位时,如果当前的答案\(x\)第\(i\)位为\(0\),就将\(x\)异或上 $ a_i $ ;否则不做任何操作。显然,每次操作后答案不会变劣,最终的\(x\)即为答案。
同样,我们考虑对于一个数\(x\),它与原数列中的数异或的最值如何获得。用与序列异或最大值类似的贪心即可解决。
inline lld Query_max () {
register lld res = 0;
for (register int i = 63; i >= 0; i --)
res = max (res, res ^ a[i]);
return res;
}
inline lld Query_min () {
if (Check (0)) return 0;
for (register int i = 0; i <= 63; i ++)
if (a[i]) return a[i];
}
查询k小值
我们考虑进一步简化线性基。显然,一个线性基肯定可以表示为若干个形如\(2^i\) 的数。从高到低处理线性基每一位,对于每一位向后扫,如果当前数第\(i\)位为\(0\),且线性基第\(i\)位不为\(0\),则将当前数异或上\(a_i\)。这一操作可以在\(O(\log_2^2{n})\)的时间内解决。
经过这一步操作后,设线性基内共有\(cnt\)个数,则它们共可以表示出\(2^cnt\)个数。当然,对于\(0\)必须特殊考虑。如果插入的总数\(n\)与\(cnt\)相等,就无法表示\(0\)了。
同样,考虑最小值时,也必须要考虑到\(0\)的情况。事实上,如果插入时出现了未被加入的元素,就肯定可以表示出\(0\)。
随后,我们考虑将\(k\)二进制拆分,用与快速幂类似的方法就可以求出第\(k\)大值。
学过线性代数的同学应该可以看出,这个过程就是对一个矩阵求解异或意义下的秩的过程。因此,\(cnt \leqslant \lceil \log_2{N} \rceil\)一定成立。而最终,线性基中保存的也是异或意义下的一组极小线性无关组。
lld tmp[70];
inline lld Query (lld k) {
register lld res = 0;
register int cnt = 0;
k -= Check (0);
if (!k) return 0;
for (register int i = 0; i <= 63; i ++) {
for (register int j = i - 1; j; j --)
if (a[i] & (1ll << j)) a[i] ^= a[j];
if (a[i]) tmp[cnt ++] = a[i];
}
if (k >= (1ll << cnt)) return -1;
for (register int i = 0; i < cnt; i ++)
if (k & (1ll << i)) res ^= tmp[i];
return res;
}
[TJOI2008] 彩灯
题意
求线性基中有多少个线性无关的向量,输出 \(2^{num}\)。
Solution
线性基模板题。
Code
点击查看代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 150;
typedef long long lld;
inline lld read() {
register lld w = 0, f = 1;
register char c = getchar();
while (c > '9' || c < '0') {
if (c == '-') f = -1;
c = getchar();
}
while (c >= '0' && c <= '9') {
w = w * 10 + c - '0';
c = getchar();
}
return w * f;
}
int n, m, num;
lld a[N];
char s[N];
inline void Insert (register lld x) {
for (register int i = 52; i >= 0; -- i)
if (x & (1ll << i))
if (!a[i]) {
a[i] = x;
++ num;
break;
}
else x ^= a[i];
}
int main() {
n = read(), m = read();
for (register int i = 1; i <= m; ++ i) {
scanf ("%s ", s + 1);
register lld x = 0;
for (register int j = 1; j <= n; ++ j)
if (s[j] == 'O') x = (x << 1) + 1;
else x = x << 1;
Insert (x);
}
register lld ans = 1;
for (register int i = 1; i <= num; ++ i) ans = 2ll * ans % 2008;
printf ("%lld\n", ans);
return 0;
}
[BJWC2011] 元素
题意
给定若干个元素,每个元素有一个权值和一个贡献,要求选出若干个元素构成集合\(S\),使得\(S\)的任一子集的异或和不为\(0\),求最大的贡献。
Solution
我们可以按照贡献排序,然后贪心的尝试将每个元素加入线性基,最终线性基内所有元素的贡献之和即为答案。
Code
点击查看代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1050;
typedef long long lld;
inline lld read() {
register lld w = 0, f = 1;
register char c = getchar();
while (c > '9' || c < '0') {
if (c == '-') f = -1;
c = getchar();
}
while (c >= '0' && c <= '9') {
w = w * 10 + c - '0';
c = getchar();
}
return w * f;
}
int n;
lld ans;
lld a[N], val[N];
inline void Insert (register lld x, register lld w) {
for (register int i = 63; i >= 0; -- i)
if (x & (1ll << i))
if (!a[i]) {
a[i] = x;
return ;
} else x ^= a[i];
}
inline bool Query (register lld x) {
for (register int i = 63; i >= 0; i --)
if (x & (1ll << i))
if (!a[i]) return false;
else x ^= a[i];
return true;
}
struct Node {
lld x, w;
} p[N];
inline bool operator < (register Node x, register Node y) {
return x.w > y.w;
}
int main() {
n = read();
for (register int i = 1; i <= n; ++i) p[i].x = read(), p[i].w = read();
sort (p + 1, p + 1 + n);
for (register int i = 1; i <= n; ++ i) {
register lld x = p[i].x, w = p[i].w;
if (!Query (x)) Insert (x, w), ans += w;
}
printf ("%lld\n", ans);
return 0;
}
[CQOI2013] 新Nim游戏
题意
类似 Nim 游戏,不过游戏的第一轮变成了可以任取任意堆石子,可以不取但是不能取完,两个人各取一次后按照正常的 Nim 游戏进行。
问先手最少可以取走多少个石子后,让先手必胜。
Solution
考虑后手必胜的状态,也就是当剩余石子的某个子集满足异或和为\(0\)的状态。
那么先手必须使剩下的石子不能满足上述条件,也就是选取若干石子,使得他们的的某个子集不满足异或和为\(0\)的状态,显然可以扔到线性基里判断。
对于取走数量最少,也就是线性基内的数量最多,可以参照上一题。
Code
点击查看代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long lld;
const int N = 150;
inline int read () {
register int x = 0, w = 1;
register char ch = getchar ();
for (; ch < '0' || ch > '9'; ch = getchar ()) if (ch == '-') w = -1;
for (; ch >= '0' && ch <= '9'; ch = getchar ()) x = x * 10 + ch - '0';
return x * w;
}
int n;
int a[N], w[N];
inline void Insert (register int x) {
for (register int i = 31; i >= 0; --i)
if (x & (1 << i))
if (!a[i]) {
a[i] = x;
break;
} else x ^= a[i];
}
inline bool Query (register lld x) {
for (register int i = 63; i >= 0; i --)
if (x & (1ll << i))
if (!a[i]) return false;
else x ^= a[i];
return true;
}
inline bool cmp (register int x, register int y) {
return x > y;
}
int main () {
n = read ();
for (register int i = 1; i <= n; ++ i) w[i] = read();
sort (w + 1, w + 1 + n, cmp);
register lld ans = 0;
for (register int i = 1; i <= n; i ++)
if (Query (w[i])) ans += w[i];
else Insert (w[i]);
printf ("%lld\n", ans);
return 0;
}
[WC2011] 最大XOR和路径
题意
考虑一个边权为非负整数的无向连通图,节点编号为\(1\)到\(N\),试求出一条从\(1\)号节点到\(N\)号节点的路径,使得路径上经过的边的权值的 XOR 和最大。
Solution
显然我们没法枚举\(1~N\)的每一条路,但我们可以将路径拆成两部分,第一部分是环,第二部分是链。
假设我们选择了一条从\(1~N\)的链,当然,它不一定是最优秀的。但是别忘了,我们可以选择一些环来增广这条链。举个例子:
假设\(k\)是连接这条链和某个环的某条路径,那么显然,链和环都将走过一遍,而这条路径\(k\)会被走过两遍(从链到环一遍,从环到链一遍)。根据我们上面的推论,\(k\)对答案的贡献是\(0\)。于是我们发现,我们根本就没有必要算出这条路径\(k\)(反正贡献是\(0\))
于是我们枚举所有环,将环上异或和扔进线性基,然后用这条链作为初值,求线性基与这条链的最大异或和。
Code
点击查看代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long lld;
const int N = 5e5 + 50;
inline int read () {
register int x = 0, w = 1;
register char ch = getchar ();
for (; ch < '0' || ch > '9'; ch = getchar ()) if (ch == '-') w = -1;
for (; ch >= '0' && ch <= '9'; ch = getchar ()) x = x * 10 + ch - '0';
return x * w;
}
int n, m;
lld a[64];
inline void Insert (register lld x) {
for (register int i = 63; i >= 0; --i)
if (x & (1ll << i))
if (!a[i]) {
a[i] = x;
break;
} else x ^= a[i];
}
inline lld Query (register lld x) {
register lld res = x;
for (register int i = 63; i >= 0; i --)
if ((res ^ a[i]) > res) res ^= a[i];
return res;
}
int cnt;
int head[N], to[N << 2], nxt[N << 2];
lld val[N << 2];
inline void Add (register int u, register int v, register lld w) {
to[++ cnt] = v;
nxt[cnt] = head[u];
val[cnt] = w;
head[u] = cnt;
}
lld det[N];
bool vis[N];
inline void DFS (register int nw, register lld res) {
det[nw] = res, vis[nw] = 1;
for (register int i = head[nw]; i; i = nxt[i]) {
register int y = to[i];
if (!vis[y]) DFS (y, res ^ val[i]);
else Insert (res ^ val[i] ^ det[y]);
}
}
int main () {
n = read(), m = read();
for (register int i = 1; i <= m; ++ i) {
register int u = read(), v = read();
register lld w;
scanf ("%lld", &w);
Add (u, v, w);
Add (v, u, w);
}
DFS (1, 0);
printf ("%lld\n", Query (det[n]));
return 0;
}
后记
最近牛客+Codeforces+Atcoder的比赛实在是有点多,加上最近感觉一直睡不醒,所以说补题效率一直很低,也就没抽出时间来写具体每道题的算法笔记了,只能合并成一篇学习笔记了。
主观来说也是最近有点松懈,趁牛客第四场没什么题可补的情况之下,把之前的线性基的东西复习一下,顺便写成一篇算法笔记,方便以后翻阅复习。
标签:int,register,笔记,学习,异或,线性,include,lld From: https://www.cnblogs.com/abigjiong/p/17589491.html