原文链接:https://oierbbs.fun/blog/399/3
作者: @"Suzt_ilymtics"#38
推荐时间:2021年8月13日
概念
线性基是向量空间的一组基,通常可以解决有关异或的一些题目。-Oi-wiki
线性基就是一个有着特殊性质的集合,在处理某些情况下的异或问题有着意想不到的效果。
假设我们用 p
数组来存线性基。
线性基可以由给出的一组元素相互异或而来。
而 p_i
中的元素表示该元素二进制下 1
的最高位是第 i
位。
性质
- 线性基具有普通集合所具有的性质,即确定性、互异性、无序性。
- 线性基中每个元素的最高位不同。(根据
p
数组定义比较显然。) - 线性基中没有异或和为
0
的子集。(每个元素的最高位不同,异或起来最高位一定不会为0
。) - 线性基中任意多元素的异或和的值域等于原集合中任意多元素的异或和的值域。
- 线性基在满足上一个条件的情况下,所包含元素个数是最少的。
- 线性基中不同的元素异或出来的值是不同的。
构造
一般情况下都是在二进制下进行。
假设插入一个元素为 x
。
- 从高位向低位判断,直到遇到该元素某位上为
1
,设该位为i
。 - 然后判断
p_i
是否有值,如果没有把x
存到p_i
中,否则将x
与p_i
异或然后重复上面的操作。(将x
与p_i
异或后x
的最高位上的1
就没了,至于变成了啥也无所谓了)
把所有元素都插入后,我们就得到了这组元素的线性基。
这样构造的线性基满足它该有的所有性质,p_i
数组也符合定义。
Code:
void Insert(int k) {
for(int i = Max; i >= 0; --i) { // Max 表示二进制最高位
if(!(k & (1ll << i))) continue;
if(!p[i]) { p[i] = k; return ; }
k ^= p[i];
}
}
线性基的插入和下面的几个操作复杂度都是 O(\log n)
的。
操作
求最大值
给你一堆元素,挑几个异或起来,是他们的值最大,输出最大值。
我们先用这组元素构造出线性基。然后贪心的进行选择,选高位的肯定比低位的要更优。
所以我们从高位向低位遍历,如果异或上 p_i
更优就异或。最后的结果就是要求的最大值。
为什么能直接用?线性基的元素都是通过已知的元素异或而来,肯定是正确的。
Code:
int Query() {
int res = 0;
for(int i = Max; i >= 0; --i) res = max(res, res^p[i]);
return res;
求一个数是否能被表示出来
把这个数扔到线性基里跑一边就行。
每次挑这个数的最高位进行异或,都能把最高位消掉。
如果最后这个数为 0
,说明该数可以被表示出。
线性基合并
把一个线性基的每个元素插入另外一个线性基即可。
查找严格次大值
先找到最大值,然后从低位向高位枚举,然后找到两者都为一的异或上, 然后退出即可。
广义线性基
之前我们只是把线性基简单化了,线性基最初是应用到向量里的。
这道题思路和[P4570 [BJWC2011]元素](https://www.luogu.com.cn/prob...挺像的。
只不过这道题把整数换成了向量。向量中的每个元素就相当于原来的每个二进制位。
我们可以把 n
个装备看作 n
个 m
维的向量。
如 k = ( a_1, a_2, a_3, ... , a_m )
大体思路是一样的,结合高斯消元来构造。
emm看代码会更直观一点,感觉我口胡的并不是很清楚。
/*
Work by: Suzt_ilymics
Knowledge: ??
Time: O(??)
*/
#include
#include
#include
#include
#include
#include
#include
#define LL long long
#define orz cout<<"lkp AK IOI!"<= 1; --i) { // 从左向右依次遍历每个向量中的元素,这里的遍历顺序已不再重要
if(abs(k.a[i]) < lim) continue; // 如果插入向量的当前元素无值就跳过
if(abs(p[i].a[i]) < lim) { p[i] = k; return true; } // 如果线性基中“最高位”向量中没有值,就插入。
double t = k.a[i] / p[i].a[i]; // 算出系数
k = k - p[i] * t; // 进行消元
}
return false;
}
}
int main()
{
n = read(), m = read();
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m; ++j)
scanf("%lf", &ep[i].a[j]); // 读入 n 个 向量
for(int i = 1; i <= n; ++i) ep[i].cost = read(); // 读入每个向量的花费,类比元素那道题
sort(ep + 1, ep + n + 1); // 按照花费从小到大排序贪心
for(int i = 1; i <= n; ++i) {
if(Lb::Insert(ep[i])) { // 判断能否插入
++ cnt; // 计数
sum += ep[i].cost; // 记录花费
}
}
printf("%d %d", cnt, sum);
return 0;
}
例题
P3812【模板】线性基
板子题,求最大值。
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include
#include
#include
#include
#include
#include
#define int long long
#define orz cout<<"lkp AK IOI!"<= 0; --i) {
if(!(k & (1ll << i))) continue;
if(!p[i]) { p[i] = k; return ; }
k ^= p[i];
}
}
int Maxxor() {
int res = 0;
for(int i = bit; i >= 0; --i) res = max(res, (res ^ p[i]));
return res;
}
signed main()
{
n = read();
while(n--) Insert(read());
printf("%lld", Maxxor());
return 0;
}
P3857 [TJOI2008]彩灯
搞出线性基,统计线性基中有几个元素,假设有 x
个,输出 2^ x \bmod 2008
即可。
因为假设我们有 p_i
那么选与不选一定会决定第 i
位的是否为 1
,那么有几个 p_i
我们就能决定几位,答案也就是 2^ x
。
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include
#include
#include
#include
#include
#include
#define int long long
#define orz cout<<"lkp AK IOI!"<= 0; --i) {
if(!(k & (1ll << i))) continue;
if(!p[i]) { p[i] = k; return ;}
k ^= p[i];
}
}
signed main()
{
n = read(), m = read();
for(int i = 1; i <= m; ++i) {
scanf("%s", s + 1); int x = 0;
for(int j = 1; j <= n; ++j) if(s[j] == 'O') x |= (1ll << (j - 1));
Insert(x);
}
for(int i = Max; i >= 0; --i) if(p[i]) cnt++;
printf("%lld\n", (1ll << cnt) % mod);
return 0;
}
P4570 [BJWC2011]元素
首先我们知道
如果a \oplus b = c
,那么a \oplus c = b, b \oplus c = a
。
由前面推后面可以看做两边同时异或 a
或者 b
。
假设 b_1 \oplus b_2 \oplus b_3 ... \oplus b_i = x
,x
比任何一个值都要大。
那么 x \oplus b_ 1 \oplus b_ 2 \oplus b_ 3 ... \oplus b_ {i-1} = b_ i
。
为了获得更大价值,我们可以按照价值从大到小排序,然后贪心的选择。
每次向线性基插入它的 number
,如果可以插入就选择它。
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include
#include
#include
#include
#include
#include
#define LL long long
#define orz cout<<"lkp AK IOI!"< b.val; }
}a[MAXN];
LL n, ans = 0;
LL read(){
LL s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
return f ? -s : s;
}
namespace Lb {
const LL Max = 60;
LL p[61];
bool Insert(LL k) {
for(LL i = Max; i >= 0; --i) {
if(!(k & (1ll << i))) continue;
if(!p[i]) { p[i] = k; return true; }
k ^= p[i];
}
return false;
}
}
int main()
{
n = read();
for(int i = 1; i <= n; ++i) a[i].k = read(), a[i].val = read();
sort(a + 1, a + n + 1);
for(int i = 1; i <= n; ++i) if(Lb::Insert(a[i].k)) ans += a[i].val;
printf("%lld", ans);
return 0;
}
P4151 [WC2011]最大XOR和路径
乍一看没有思路。一开始以为可以像上面那个题一样贪心的选择。
因为可以重复经过,所以路径就太多了。
假设我们走到了 n
号点,我们此时的异或和为 dis_n
。
我们想要知道有没有更优的选择,我们假设往回走某条我们没有经过的路径,如果我们不能绕回来,也就是说没有环,那么在返回的途中还会再走一遍。众所周知 x \oplus x = 0
。所以就相当于我们啥也没干。
如果在走的途中绕了一个环,我们从环的起点出发又回到了环的起点,环上的路径都只被走了一次,说明什么?我们与环上的所有值异或了。
通过上面讨论不难发现,一般路径上的值不能选,环上的值可选可不选,那么我们把一个环当做一个元素构建线性基,挑几个能让我们的 dis_n
变得更优的与之异或。
dis_n
怎么求?随便找一条路径。
如果有更优的路径,那么它一定会与这条路径形成一个环,异或这个环,就会变成选择另一条路径。
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include
#include
#include
#include
#include
#include
#define LL long long
#define int long long
#define orz cout<<"lkp AK IOI!"<= 0; --i) {
if(!(k & (1ll << i))) continue;
if(!p[i]) { p[i] = k; return ; }
k ^= p[i];
}
}
int Query(int res) {
for(int i = Max; i >= 0; --i) res = max(res, res ^ p[i]);
return res;
}
void dfs(int u, int sum) {
dis[u] = sum, vis[u] = true;
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(!vis[v]) dfs(v, dis[u] ^ e[i].w);
else Insert(dis[u] ^ dis[v] ^ e[i].w);
}
}
signed main()
{
n = read(), m = read();
for(int i = 1, u, v, w; i <= m; ++i) {
u = read(), v = read(), w = read();
add_edge(u, v, w), add_edge(v, u, w);
}
dfs(1, 0);
printf("%lld\n", Query(dis[n]));
return 0;
}
P3292 [SCOI2016]幸运数字
把每个点看做一个线性基然后树剖即可。
询问时把每段区间的线性基合并求最大值即可。
总复杂度 O(n \log ^ 4n)
。
代码写的比较丑,需要吸氧才能过。
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include
#include
#include
#include
#include
#include
#define LL long long
#define orz cout<<"lkp AK IOI!"<= 0; --i) {
if(!(k & (1ll << i))) continue;
if(!p[i]) { p[i] = k; return ; }
k ^= p[i];
}
}
LL Query() {
LL res = 0;
for(int i = 60; i >= 0; --i) res = max(res, res ^ p[i]);
return res;
}
}tree[MAXN << 2];
void Push_up(int i) {
tree[i] = tree[lson];
for(int j = 60; j >= 0; --j) if(tree[rson].p[j]) tree[i].Insert(tree[rson].p[j]);
}
void hb(Tree *x, Tree y) { for(int i = 60; i >= 0; --i) if(y.p[i]) x->Insert(y.p[i]); }
void Build(int i, int l, int r) {
if(l == r) {
tree[i].Insert(a[pre[l]]);
return ;
}
int mid = (l + r) >> 1;
Build(lson, l, mid), Build(rson, mid + 1, r);
Push_up(i);
}
Tree Query(int i, int l, int r, int L, int R) {
if(L <= l && r <= R) return tree[i];
Tree ans;
int mid = (l + r) >> 1;
if(mid >= L) ans = Query(lson, l, mid, L, R);
if(mid < R) hb(&ans, Query(rson, mid + 1, r, L, R));
return ans;
}
}
namespace Cut {
void dfs(int u, int fa) {
dep[u] = dep[fa] + 1, fath[u] = fa, siz[u] = 1;
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(v == fa) continue;
dfs(v, u);
siz[u] += siz[v];
if(siz[son[u]] < siz[v]) son[u] = v;
}
}
void dfs2(int u, int tp) {
top[u] = tp, dfn[u] = ++ cnt, pre[cnt] = u;
if(son[u]) dfs2(son[u], tp);
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(v == fath[u] || v == son[u]) continue;
dfs2(v, v);
}
}
Seg::Tree Query(int x, int y) {
Seg::Tree ans;
while(top[x] != top[y]) {
if(dep[top[x]] < dep[top[y]]) swap(x, y);
Seg::hb(&ans, Seg::Query(1, 1, n, dfn[top[x]], dfn[x]));
x = fath[top[x]];
}
if(dep[x] > dep[y]) swap(x, y);
Seg::hb(&ans, Seg::Query(1, 1, n, dfn[x], dfn[y]));
return ans;
}
}
signed main()
{
n = read(), Q = read();
for(int i = 1; i <= n; ++i) a[i] = read();
for(int i = 1, u, v; i < n; ++i) u = read(), v = read(), add_edge(u, v), add_edge(v, u);
Cut::dfs(1, 0), Cut::dfs2(1, 1), Seg::Build(1, 1, n);
for(int i = 1, u, v; i <= Q; ++i) {
u = read(), v = read();
Seg::Tree ans = Cut::Query(u, v);
printf("%lld\n", ans.Query());
}
return 0;
}