线性基小计
引入
很早就听说这个高大上的算法了,好像很厉害的样子。
但是 blog 里说的是异或线性基 不是线性代数那个那个看不懂
线性基用来干什么?
- 查询一个数能不能被一堆数异或出来
- 查询一堆数异或的最大值
- 查询异或第 k 大 / 小 (本质不同)
概念
线性基有 3 个性质。
- 原序列任意数以及任意一堆数的异或可以通过线性基异或出来
- 线性基任意数异或起来不会产生 \(0\)
- 满足上述条件时,使得线性基内数最少。
构造
怎么构造一个满足上述条件的线性基呢?
这要扯到线性代数方面,我不好说。
不如直接感性理解。
怎么使得线性基数的个数最小?
我们假设现在已经插入了 \(a_1,a_2,a_3,a_4\) 现在插入 \(a_5\)
怎么办呢?
假设 \(a_5\) 很大,不好插入。
根据异或的性质,实际上,插入 \(a_5\oplus a_1\) 或者 \(a_5\oplus a_2 \oplus a_3\) 是等价 \(a_5\) 的。
明显的,这些数插入都会产生相同的线性基。
那么,我们是不是可以在原有 \(a_1,a_2,a_3,a_4\) 的前提下,构造出一个合法的,最小的 \(a_5\) 插入呢?
线性基的插入
根据上面的思想,我们可以以这样思考:
令最终线性基为 \(d\) ,其中 \(d_i\) 是第 \(i\) 位线性基内存的数,满足最高位是第 \(i\) 位。
明显的,这个线性基的大小不会超过 \(\log v\)
我们思考怎么插入一个数:
不妨令 \(a_1,a_2,a_3,a_4\) 已经构造出一个 \(d\),现在插入 \(a_5\)
从大到小枚举 \(i\) 。如果 \(a_5\) 第 \(i\) 位为 \(1\),分类讨论:
- \(d_i\) 为空。这时直接插入,退出。
- 否则令 \(a_5=a_5\oplus d_i\) 继续遍历。
第一个操作好理解,第二个呢?
根据我们刚才的分析,现在的 \(i\) 肯定是 \(a_5\) 的最高位。
这一位无法插入,我们考虑插入一个和 \(a_5\) 等价的,也许是 \(a_5\oplus a_3\) ,然后把这一位删除,考虑下一位。
不妨考虑这样有什么后果。
- 找到下一位为空的 \(d_i\)
- 找不到为空的 \(d_i\)
第一种情况直接就结束了,我们考虑情况 \(2\) 。
找不到为空的 \(d_i\) ,可以证明,现在 \(a_5=0\) 因为我们已经从小到大把 \(a_5\) 所有位数都给删掉了。
这说明,我们可以在 \(a_1,a_2,a_3,a_4\) 中构造出一种异或,使得它们的异或和为 \(a_5\) 。
这时候,\(a_5\) 不需要插入。
这样,我们构造出了满足 \(1,2\) 的线性基。
因为线性基内的数总是小于原数组的数,所以 \(3\) 也满足。
因此,构造完毕。
异或最值
Max
很简单,因为每一位线性基最高位是确定的,从后往前贪心即可。
Min
根据每一位是确定的,从前往后找到第一个有数的基即可。
Kth
不会bx
前缀线性基
link
考虑两种写法。
无脑分制很简单,这不讲了。
我们考虑前缀线性基。
明显,我们可以把所有时候,定义状态 \(f_i\) 表示 \([1,i]\) 的线性基,并且满足每一位最大。
这怎么搞?
一种显然的想法,从第 \(i\) 为开始往后插入。
但是这不好搞,时间不允许。
假设我们已经求出 \([1,i-1]\) 满足每一位最大。
现在我们插入 \(i\) 。
考虑第 \(i\) 位肯定是要插入的。我们要把哪一位取出来。
根据贪心,后面的取出来可以从前面插入。
如果前面插入不了了,那这一位肯定要插入进去,如果大可以在后面考虑。可以发现此时插入一定不劣。
我们可以理解为把这一位基拿出来,然后塞进去,得到一个同等的子问题,但是 pos 变小。后面 pos 贪心搞即可。
- 例题
P3292
你不会写没关系,淀粉质直接薄纱 dijah 前缀线性基做法,知不知道 \(O(n\log^2 n)\) 快过理论 \(O(n\log n)\) 轰啊
点击查看代码
#include<bits/stdc++.h>
#define N 20005
#define ll long long
using namespace std;
int n,m;
int head[N],tot=1;
struct edge{
int to,next;
}e[N*2];
void add(int u,int v)
{
e[tot]=(edge){v,head[u]};
head[u]=tot++;
}
ll w[N],ans[N*10];
struct node{
int x,id;
};
vector<node> q[N];
int root,minn;
int siz[N],vis[N];
struct Xor{
ll d[62];
void insert(ll x)
{
for(int i=60;i>=0;i--)
if(x&(1ll<<i))
if(d[i]) x^=d[i];
else {d[i]=x;return ;}
}
ll ask()
{
ll sum=0;
for(int i=60;i>=0;i--)
sum=max(sum,sum^d[i]);
return sum;
}
}f[N];
Xor merges(Xor a,Xor b)
{
for(int i=0;i<=60;i++)
if(a.d[i]) b.insert(a.d[i]);
return b;
}
void groot(int now,int fa,int n)
{
int maxx=0;
siz[now]=1;
for(int i=head[now];i;i=e[i].next)
{
int son=e[i].to;
if(son==fa||vis[son]) continue;
groot(son,now,n);
siz[now]+=siz[son];
maxx=max(maxx,siz[son]);
}
maxx=max(maxx,n-siz[now]);
if(maxx<minn) minn=maxx,root=now;
}
int col[N],ids;
void getdis(int now,int fa)
{
col[now]=ids;
f[now]=f[fa],f[now].insert(w[now]);
siz[now]=1;
for(int i=head[now];i;i=e[i].next)
{
int son=e[i].to;
if(son==fa||vis[son]) continue;
getdis(son,now);
siz[now]+=siz[son];
}
}
void getans(int now,int fa)
{
for(int i=0;i<q[now].size();i++)
if(col[q[now][i].x]^col[now])
{
ans[q[now][i].id]=merges(f[now],f[q[now][i].x]).ask();
swap(q[now][i],q[now][q[now].size()-1]);
q[now].pop_back();
i--;
}
for(int i=head[now];i;i=e[i].next)
{
int son=e[i].to;
if(son==fa||vis[son]) continue;
getans(son,now);
}
}
void solve(int now,int n)
{
minn=n+2;
groot(now,0,n);
vis[root]=1;
memset(f[root].d,0,sizeof f[root].d);
f[root].insert(w[root]);
col[root]=ids=0;
for(int i=head[root];i;i=e[i].next)
{
int son=e[i].to;
if(vis[son]) continue;
++ids;
getdis(son,root);
}
getans(root,0);
for(int i=head[root];i;i=e[i].next)
{
int son=e[i].to;
if(vis[son]) continue;
solve(son,siz[son]);
}
}
int main()
{
// freopen("c.in","r",stdin);
// freopen("m.txt","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%lld",&w[i]);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
if(u^v) q[u].push_back((node){v,i});
else ans[i]=w[u];
}
solve(1,n);
for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
return 0;
}
求有多少个数比 \(x\) 小
使用贪心。
把每一位线性基里面的数拿出来。
如果 \(x\) 这一位是 \(1\) 且 存在 \(d\) 这一位也是 \(1\)
那么我们把 前面有的位数全取了,后面随便取即可。
否则退出。