(题目讲解)异或空间线性基
比较简略咕咕咕。。。
当线性基碰上了区间修改和区间(全局)查询最大异或值
Round 1 Ynoi Easy Round 2025 TEST_34。
这个好像 P 哥的桶,但是那题是单点修改,这题是区间修改。
我们可以转化为多个单点修改,但是会和线段树一样 TLE。
考虑维护延时标记,但是这玩意儿。。。不知道咋维护,一言难尽,重点是我们需要的是线性基。
我们再想想做树状数组题目的时候,区间修改+区间查询是怎么做得?差分!
没错,这里就是差分,我们设 \(d_i = a_i\oplus a_{i - 1}\)。
这样区间修改的时候,只需要修改 \(d_l\) 和 \(d_{r + 1}\),原因:
-
\(1 \le x < l\) 或者 \(r < x \le n\),此时明显不变。
-
\(x = l\) 或 \(x = r + 1\),只有 \(a_{x - 1}\) 需要修改,则 \(d'_x = a_x \oplus (a_{x - 1} \oplus v) = d_x\oplus v\)。
-
\(l < x \le r\),\(a_x\) 和 \(a_{x - 1}\) 都要修改,则 \(d'_x = (a_x\oplus v)\oplus (a_{x - 1} \oplus v) = a_x \oplus a_{x - 1} = d_x\)。
而我们又发现一个妙妙的事情,对于查询的 \([l,r]\),\(a_{l\cdots r}\) 都可以被 \(a_l\),\(a_l\oplus a_{l + 1}\),\(a_{l+1}\oplus a_{l+2}\),\(\cdots\),\(a_{r - 1}\oplus a_r\) 表示出来。
- 原因:\(a_{l + 1} = a_l \oplus (a_l\oplus a_{l + 1}) = a_{l + 1}\),\(a_{l + 2} = a_{l - 1}(\text{由前一步得到})\oplus (a_{l+1}\oplus a_{l+2}) = a_{l+2}\),以此类推。
然而 \(a_{l}\oplus a_{l + 1} = d_{l + 1}\),……,以此类推,也就是说:
对于查询的 \([l,r]\),\(a_{l\cdots r}\) 都可以被 \(a_l\),\(d_{l + 1}\),\(d_{l + 2}\),\(\cdots\),\(d_r\) 表示出来。
那么它们的组合异或值也可以被表示出来,因为异或值两两抵消,每个数留下的有效次数为 \(0/1\) 次。
我们又发现维护了这个东西,对于区间修改,\(d\) 只用单点修改两次即可!
而在查询的时候,我们只需要知道 \(a_l\) 和 【\(b_{l + 1\cdots r}\) 的线性基】,把 \(a_l\) 放进这个线性基即可,这个线性基可以用线段树进行查询。
那难道我们要维护两棵线段树,一棵懒标记维护 \(a\),一棵不用懒标记维护 \(d\)?
实则不然,因为 \(a_i = \bigoplus\limits_{j=1}^i d_j\),直接查询就可以了,这样维护一棵线段树足矣。
另外这题的异或条件还是到子节点修改更方便,因为只有一个值,然后 pushup 用线性基合并板子就行了。
Coding time
# include <bits/stdc++.h>
# define int long long
# define up(i ,x ,y) for (int i = x ; i <= y ; i ++)
# define dn(i ,x ,y) for (int i = x ; i >= y ; i --)
using namespace std;
inline int read (){int s = 0 ; bool w = 0 ; char c = getchar () ; while (!isdigit (c)) {w |= (c == '-') ,c = getchar () ;} while (isdigit (c)){s = (s << 1) + (s << 3) + (c ^ 48) ; c = getchar ();}return w ? -s : s;}
inline void write (int x){if (x < 0) putchar ('-') ,x = -x; if (x > 9) write (x / 10) ; putchar (x % 10 | 48);}
inline void writesp (int x){write (x) ,putchar (' ');}
inline void writeln (int x){write (x) ,putchar ('\n');}
const int N = 1e5 + 10 ,mod = 10086;
int n ,m ,a[N] ,d[N] ,XOR[N << 2];
struct BASIS {
int base[35];
inline void init (){up (i ,0 ,31) base[i] = 0;}
inline void insert (int x){//构造线性基板子。
dn (i ,31 ,0) {
if ((x >> i) & 1) {
if (!base[i]) {
base[i] = x;
return ;
} x ^= base[i];
}
}
} inline BASIS merge (BASIS x ,BASIS y) {//线性基合并板子。
BASIS res;
res.init ();
dn (i ,31 ,0) if (x.base[i]) res.insert (x.base[i]);
dn (i ,31 ,0) if (y.base[i]) res.insert (y.base[i]);
return res;
}
} basis[N << 2] ,ans;
inline void pushup (int u){
XOR[u] = (XOR[u << 1] ^ XOR[u << 1 | 1]);
basis[u] = basis[u].merge (basis[u << 1] ,basis[u << 1 | 1]);
} inline void build (int u ,int l ,int r){
if (l == r) {XOR[u] = d[l] ; basis[u].init () ; basis[u].insert (d[l]); return ;}//初始化不要忘记或者漏掉。
int mid = ((l + r) >> 1);
build (u << 1 ,l ,mid);
build (u << 1 | 1 ,mid + 1 ,r);
pushup (u);
} inline void modify (int u ,int l ,int r ,int x ,int v){
if (l == r) {
XOR[u] ^= v;
basis[u].init ();
basis[u].insert (XOR[u]);
return ;
} int mid = ((l + r) >> 1);
if (x <= mid) modify (u << 1 ,l ,mid ,x ,v);
else modify (u << 1 | 1 ,mid + 1 ,r ,x ,v);
pushup (u);
} inline int query_num (int u ,int l ,int r ,int ql ,int qr){//求 d[ql...qr] 的异或值。
if (l >= ql && r <= qr) return XOR[u];
int mid = ((l + r) >> 1) ,res = 0;
if (ql <= mid) res = query_num (u << 1 ,l ,mid ,ql ,qr);
if (mid < qr) res ^= query_num (u << 1 | 1 ,mid + 1 ,r ,ql ,qr);
return res;
} inline void query (int u ,int l ,int r ,int ql ,int qr){//求 d[ql...qr] 的线性基。
if (l >= ql && r <= qr){
up (i ,0 ,31) if (basis[u].base[i]) ans.insert (basis[u].base[i]);
return;
} int mid = ((l + r) >> 1);
if (ql <= mid) query (u << 1 ,l ,mid ,ql ,qr);
if (mid < qr) query (u << 1 | 1 ,mid + 1 ,r ,ql ,qr);
} signed main (){
n = read () ,m = read ();
up (i ,1 ,n) a[i] = read () ,d[i] = (a[i] ^ a[i - 1]);
build (1 ,1 ,n);
while (m --) {
int op = read () ,l = read () ,r = read () ,v = read ();
if (op == 1){
modify (1 ,1 ,n ,l ,v) ;
if (r != n) modify (1 ,1 ,n ,r + 1 ,v);//如果 r = n ,r + 1 = n + 1,这样搜到的叶节点为 [n ,n],会出错。
}
else {
int num = query_num (1 ,1 ,n ,1 ,l);//求 a[l](= d[1...l] 的异或和)。
// cout << num << endl;
ans.init ();
if (l < r) query (1 ,1 ,n ,l + 1 ,r);
ans.insert (num);//不要忘记。
// dn (i ,31 ,0) cout << i << ' ' << ans.base[i] << endl;
dn (i ,31 ,0) v = max (v ,v ^ ans.base[i]);//贪心求最大。
writeln (v);
}
}
return 0;
}
Round 2 [集训队互测 2015] 最大异或和
什么集训队互测考板子?
还是一样的,这里多了一个操作 QWQ。
等一下不对,集训队怎么可能考板子啊。
因此上面的做法是不对的,因为我们有操作"区间覆盖“,无法进行有效的快速的差分,因为覆盖 \(d_{l+1,r} = 0\) 费时间,懒标记似乎也不可行。
但是我们观察到数据范围:\(1\le n,m,Q\le 2000\)!我的天,什么,竟然是暴力!
但是这又太简单了,集训队怎么会出这种题?
仔细思考,我们发现这是异或上一个字符串,因为大小不超过 \(2^{2000}\)!那最坏时间复杂度是 \(\mathcal O(nmQ)\) 啊!
可是我们无后路可走,我们只有暴力。
于是用上我们的优化神器:bitset!
这样时间复杂度就变为了 \(\mathcal O(\frac{nmQ}{w})\),\(w\) 取决于你家计算机/老年评测机的位数,通常为 \(32\) 或 \(64\)。
bitset <2005> a[2005];
//---此处省略若干代码。
int op = read ();
if (op == 1) {
int l = read () ,r = read ();
bitset <2005> x; cin >> x;//读入类型由字符串->bitset。
up (i ,l ,r) a[i] ^= x;
} else if (op == 2) {
int l = read () ,r = read ();
bitset <2005> x ; cin >> x;
up (i ,l ,r) a[i] ^= x;
}
好的我们继续,但是发现不差分还是很难做。
我们已经把覆盖和异或暴力修改了,那么我们是不是可以差分了?!(惊喜 surprise!)
然后观察到一次修改,本质上是一次删除与插入线性基,而且是有时间线的修改。
样例不怎么好(修改一次就查询蒟蒻看着不爽,凭啥),蒟蒻来举个例子:
Q = 5
a = [2 ,5 ,3 ,7 ,4]
1 2 3 2
2 3 5 5
3
1 1 3 8
3
我们姑且把这些数看作正整数不看作 bitset,来说明一下哈:
我们把询问+修改看作时间轴,比如上例中 2 3 5 5 的时间线为 2。
未操作前,计算得 d = [2 ,7 ,6 ,4 ,3]。
对于操作一,更改后 a = [2 ,7 ,1 ,7 ,4],d' = [2 ,5 ,6 ,6 ,3](d' 是修改后的差分数组)。
发现 d[2] != d'[2] ,d[4] != d'[4],我们可以看作这两个 d 的状态:
- d[2](=7)在时刻 0 结束后出现,时刻 1 结束消失,时刻 1 结束时诞生 d'[2](=5)。
- d[4](=4)在时刻 0 结束后出现,时刻 1 结束消失,时刻 1 结束时诞生 d'[4](=6)。
然后让 d <- d'。
同理对于操作 2 后,a = [2 ,7 ,5 ,5 ,5] ,d = [2 ,5 ,2 ,0 ,0]。
- d[3] != d'[3],d[3](=6)在时刻 0 结束后出现,时刻 2 结束后消失,时刻 2 结束时诞生 d'[3](=2)。
- d[4] != d'[4],d[4](=6)在时刻 1 结束后出现,时刻 2 结束后消失,时刻 2 结束时诞生 d'[4](=0)。
- d[5] != d'[5],d[5](=6)在时刻 0 结束后出现,时刻 2 结束后消失,时刻 2 结束时诞生 d'[5](=0)。
然后 d<- d'。
操作 3 是查询,给予所有数存活时间。
操作 4 后,a = [10 ,15 ,13 ,5 ,5],d = [10 ,5 ,2 ,8 ,0]。
- d[1] != d'[1],d[1](=2)在时刻 0 结束后出现,时刻 4 结束后消失,时刻 4 结束时诞生 d'[1](=10)。
- d[4] != d'[4],d[4](=0)在时刻 2 结束后出现,时刻 4 结束后消失,时刻 4 结束时诞生 d'[4](=8)。
这个 4 好可怜。/doge
操作 5 是查询,给予所有数存活时间。
结束后,我们需要默认所有数在 Q + 1 时刻消失,方便我们写代码。
- d[1](=10)在时刻 4 结束后出现,时刻 6 结束后消失,时刻 6 彻底毁灭。
- d[2](=5)在时刻 1 结束后出现,时刻 6 结束后消失,时刻 6 彻底毁灭。
- d[3](=2)在时刻 2 结束后出现,时刻 6 结束后消失,时刻 6 彻底毁灭。
- d[4](=8)在时刻 4 结束后出现,时刻 6 结束后消失,时刻 6 彻底毁灭。
- d[5](=0)在时刻 2 结束后出现,时刻 6 结束后消失,时刻 6 彻底毁灭。
因此我们记录 \(vec_i\) 为时刻 \(i\) 出现的数,它们的数值以及消失时间。
然后我们进行思考:如果 \(d_i = 0\),那么还需要放进 \(vec\) 里面吗?
实则不用,放进去也没必要,任何数异或 \(0\) 仍是它本身。
可以证明,有效的次数在 \(\mathcal O(n + Q)\) 级别,具体蒟蒻也不会证,咕咕咕可以翻有证明的题解。
然后我们只用跑删除线性基就可以了,注意是离线。
那么时间复杂度为 \(\mathcal O(\frac{nmQ}{w} + \frac{(n + Q)mQ}{w})\),具体可以见代码分析。
coding time
# include <bits/stdc++.h>
# define int long long
# define up(i ,x ,y) for (int i = x ; i <= y ; i ++)
# define dn(i ,x ,y) for (int i = x ; i >= y ; i --)
using namespace std;
inline int read (){int s = 0 ; bool w = 0 ; char c = getchar () ; while (!isdigit (c)) {w |= (c == '-') ,c = getchar () ;} while (isdigit (c)){s = (s << 1) + (s << 3) + (c ^ 48) ; c = getchar ();}return w ? -s : s;}
inline void write (int x){if (x < 0) putchar ('-') ,x = -x; if (x > 9) write (x / 10) ; putchar (x % 10 | 48);}
inline void writesp (int x){write (x) ,putchar (' ');}
inline void writeln (int x){write (x) ,putchar ('\n');}
const int N = 2000 + 10;
int n ,m ,Q ,op[N];
bitset <2005> a[N] ,d[N];
vector < pair <bitset <2005> ,int> > vec[N];
struct BASIS {
bitset <2005> base[2005];
int tim[2005];
inline void init (){up (i ,0 ,m - 1) base[i].reset () ,tim[i] = -1;}//初始化。
//我们记 tim[i] = -1 表示删除,tim[i] = 0 表示在 0 时刻(操作+询问前)出现,注意区分。
inline void insert (bitset <2005> x ,int delt){//离线的可删除线性基板子。
dn (i ,m - 1 ,0) {
if (x.test (i)) {
if (tim[i] < delt) swap (tim[i] ,delt) ,swap (base[i] ,x);
if (!delt) break;
x ^= base[i];
}
}
} inline bitset <2005> qmax () {
bitset <2005> res;
res.reset ();
dn (i ,m - 1 ,0)//注意到是倒着的。
if (!res.test(i) && tim[i] != -1) res ^= base[i];//没有被删除,并且 res 该位为 0(0^1 = 1),base[i] 可以为 res 作贡献。
return res;
}
} basis;
int lst[N];// lst[i] 为当前 d[i] 的出现时间。
inline void calc (int tims ,int l ,int r){
up (i ,l ,min (r + 1 ,n)) {
bitset <2005> dpie = (a[i] ^ a[i - 1]);//d'[i]。
if (dpie != d[i]) {//不相等则是 d[i] 消失了,d'[i] 出现了。
if (d[i].any ()) vec[lst[i]].push_back ({d[i] ,tims});//放进 vector 里面。
d[i] = dpie ,lst[i] = tims;// 更新对应信息。
}
}
} inline void print (bitset <2005> x){
dn (i ,m - 1 ,0) write (x.test (i));
puts ("");
} signed main (){
n = read () ,m = read () ,Q = read ();
up (i ,1 ,n) cin >> a[i] ,d[i] = (a[i] ^ a[i - 1]);
basis.init ();
up (i ,1 ,Q) {
op[i] = read ();
if (op[i] == 1) {
int l = read () ,r = read ();
bitset <2005> x; cin >> x;
up (i ,l ,r) a[i] ^= x;
calc (i ,l ,r);
} else if (op[i] == 2) {
int l = read () ,r = read ();
bitset <2005> x ; cin >> x;
up (i ,l ,r) a[i] = x;
calc (i ,l ,r);
}
}
up (i ,1 ,n) if (d[i].any ()) vec[lst[i]].push_back ({d[i] ,Q + 1});//补刀,在 Q + 1 时刻消失。
up (i ,0 ,Q) {
if (op[i] != 3){// 修改操作。
up (j ,0 ,m - 1) if (basis.tim[j] == i) basis.base[j].reset () , basis.tim[j] = -1; // 先删除。
int sz = vec[i].size ();
up (j ,0 ,sz - 1) basis.insert (vec[i][j].first ,vec[i][j].second);//再插入。
} else print (basis.qmax ());//询问。
}
return 0;
}
蒟蒻来补一补上一篇文章的两道习题。
Round 1 [HAOI2017] 八纵八横
正好是删除线性基趁热打铁来实现一下。
呃呃呃和这篇博客里分析的最大 XOR 和路径很像,双倍经验,这题只是改成了删除线性基,有个环的有关的结论不知道点那个链接。
我们把操作同上题看成时间轴。
对于三种操作:
-
Add:插入。 -
Cancel:删除。 -
Change:先删除原边再插入一条长 \(w\) 的边。
还是用线性基维护,离线 \(+\) bitset 做。
这题就没了。
Coding time
蒟蒻的代码出了点小问题,正在发帖求巨佬帮助 QwQ,放不上来。
Round 2 [SCOI2016] 幸运数字。
听说有淀粉质大佬 %%%,我们还是用线性基哈。
看到“异或值最大”,肯定第一时间想到线性基。
然后考虑怎么维护线性基。
如果每次询问都暴力那复杂度飞上天。
但是我们发现 \(n\) 较少,我们可以从 \(n\) 入手。
首先一条 \(u\),\(v\) 之间的路径可以拆分成 \(u-\operatorname{lca}(u,v)\) 和 \(\operatorname{lca(u,v)}-v\) 这两条路径,我们可以用它们维护线性基。
但是如果路径重复呢?没关系,线性基重复插入是可以的,只不过会被杀掉插不进去而已。
然后我们可以运用 ST 表思想,把 \(u-\operatorname{lca}(u,v)\)(\(\operatorname{lca(u,v)}-v\) 同理)拆分成两条路径,这两条路径可以有交集,因为重复贡献无影响。
这条路径的长度可以是 \(2^{lg = \lfloor \log_2{\text{深度之差}}\rfloor}\)(和 ST 表一样),起点是 \(u\);另一条路径的长度也是 \(2^{\lfloor log_2{\text{深度之差}}\rfloor}\),但是起点是 \(u \text{ 的(深度之差} - 2^{lg} )\text{ 级祖先}\)。
这样两条路径没有交集(当然只要可以有交集也没关系),然后做 \(\operatorname{lca(u,v)}-v\) 条路径,我指的有交集是指这两部可能有交集。
容易证明这样做最多只会有个 LCA 没有考虑到,额外 insert 就可以了。
然后我们记录 \(basis_{u ,i}\) 为 \(u\) 向上 \(2^i\) 个节点(包括 \(u\))的线性基,预处理即可从容应对查询。
现在的问题是如何求 \(u\) 的 \(k\) 级祖先。
把 \(k\) 二进制分解,考虑到 \(k = 2^0 \times (0/1) + 2^1 \times (0/1) + \cdots + 2^x\times (0/1)\),如果第 \(i\) 位是 \(1\) 就让 \(x\gets fa_{x,i}\),也就是跳到了第 \(2^i\) 级祖先,这样做的正确性就在左边的算式。
这样预处理的时间复杂度是 \(\mathcal O(n\log n\log^2 V)\),查询是 \(\mathcal O(Q\log^2 V)\),瓶颈在于线性基合并的 \(\mathcal O(\log^2 V)\)。
但是这其实是能过的,我们发现时限最多为 \(6s\),卡卡常(甚至不卡)都能过,并且这个可能卡不满,有可能实际合并的次数更少,或者线性基的元素在有几次合并的时候很少,减少时间。
Coding time
# include <bits/stdc++.h>
# define int long long
# define up(i ,x ,y) for (int i = x ; i <= y ; i ++)
# define dn(i ,x ,y) for (int i = x ; i >= y ; i --)
using namespace std;
inline int read (){int s = 0 ; bool w = 0 ; char c = getchar () ; while (!isdigit (c)) {w |= (c == '-') ,c = getchar () ;} while (isdigit (c)){s = (s << 1) + (s << 3) + (c ^ 48) ; c = getchar ();}return w ? -s : s;}
inline void write (int x){if (x < 0) putchar ('-') ,x = -x; if (x > 9) write (x / 10) ; putchar (x % 10 | 48);}
inline void writesp (int x){write (x) ,putchar (' ');}
inline void writeln (int x){write (x) ,putchar ('\n');}
const int N = 2e4 + 10;
int n ,Q ,a[N] ,fa[N][21] ,dep[N] ,lg[N];
vector <int> edge[N];
struct BASIS {//封装上我的线性基。
int base[62];
inline void init () {up (i ,0 ,61) base[i] = 0;}
inline void insert (int x) {
dn (i ,61 ,0)
if ((x >> i) & 1) {
if (!base[i]) {base[i] = x ; return ;}
x ^= base[i];
}
} inline int qmax () {
int res = 0;
dn (i ,61 ,0) res = max (res ,res ^ base[i]);
return res;
} inline BASIS merge (BASIS x ,BASIS y) {
BASIS res;
res.init ();
dn (i ,61 ,0) if (x.base[i]) res.insert (x.base[i]);
dn (i ,61 ,0) if (y.base[i]) res.insert (y.base[i]);
return res;
}
}basis[N][21] ,ans;
inline void dfs (int u ,int fath){
fa[u][0] = fath ,dep[u] = dep[fath] + 1;
up (i ,1 ,20){
fa[u][i] = fa[fa[u][i - 1]][i - 1];
basis[u][i] = basis[u][i].merge (basis[u][i - 1] ,basis[fa[u][i - 1]][i - 1]);//倍增合并。
} for (int v : edge[u])
if (v ^ fath) dfs (v ,u);
} inline int LCA (int u ,int v){//求 LCA。
if (dep[u] < dep[v]) swap (u ,v);
while (dep[u] ^ dep[v]) u = fa[u][lg[dep[u] - dep[v]]];
if (u == v) return u;
dn (i ,20 ,0) if (fa[u][i] ^ fa[v][i]) u = fa[u][i] ,v = fa[v][i];
return fa[u][0];
} inline int kth (int x ,int k){//求 x 的第 k 级祖先。
dn (i ,20 ,0) if ((k >> i) & 1) x = fa[x][i];
return x;
} signed main (){
n = read () ,Q = read ();
up (i ,1 ,n) up (j ,0 ,20) basis[i][j].init ();
up (i ,1 ,n) a[i] = read () ,basis[i][0].insert (a[i]);//一开始 2^0 = 1 个节点包含自己。
lg[0] = -1;
up (i ,1 ,n) lg[i] = lg[i >> 1] + 1;
up (i ,1 ,n - 1) {
int u = read () ,v = read ();
edge[u].push_back (v) ; edge[v].push_back (u);
} dfs (1 ,0);
while (Q --) {
int u = read () ,v = read ();
int L = LCA (u ,v);
int lg1 = lg[dep[u] - dep[L]] ,lg2 = lg[dep[v] - dep[L]];
ans.init ();
if (dep[u] - dep[L]){//注意,如果 dep[u] = dep[L],则 lg[0] = -1,会越界,要特判(下同)。
ans = ans.merge (basis[u][lg1] ,basis[kth (u ,dep[u] - dep[L] - (1ll << lg1))][lg1]);
}
if (dep[v] - dep[L]){
ans = ans.merge (ans ,basis[v][lg2]);
ans = ans.merge (ans ,basis[kth (v ,dep[v] - dep[L] - (1ll << lg2))][lg2]);
}
ans.insert (a[L]);//不要舍弃 LCA。
writeln (ans.qmax ());
}
return 0;
}
Ex Coding time & 求调
蒟蒻瞎写了个 bitset,但是负优化了,不开 O2 8 个点被创 TLE 了,有没有大佬解释一下是为什么(评论)。
# include <bits/stdc++.h>
# define int long long
# define up(i ,x ,y) for (int i = x ; i <= y ; i ++)
# define dn(i ,x ,y) for (int i = x ; i >= y ; i --)
using namespace std;
inline int read (){int s = 0 ; bool w = 0 ; char c = getchar () ; while (!isdigit (c)) {w |= (c == '-') ,c = getchar () ;} while (isdigit (c)){s = (s << 1) + (s << 3) + (c ^ 48) ; c = getchar ();}return w ? -s : s;}
inline void write (int x){if (x < 0) putchar ('-') ,x = -x; if (x > 9) write (x / 10) ; putchar (x % 10 | 48);}
inline void writesp (int x){write (x) ,putchar (' ');}
inline void writeln (int x){write (x) ,putchar ('\n');}
const int N = 2e4 + 10;
int n ,Q ,a[N] ,fa[N][21] ,dep[N] ,lg[N];
vector <int> edge[N];
bitset <62> w[N];
struct BASIS {
bitset <62> base[62];
inline void init () {up (i ,0 ,61) base[i].reset ();}
inline void insert (bitset <62> x) {
dn (i ,61 ,0)
if (x.test (i)) {
if (base[i].none ()) {base[i] = x ; return ;}
x ^= base[i];
}
} inline bitset <62> qmax () {
bitset <62> res;
res.reset ();
dn (i ,61 ,0) if (!res.test(i)) res ^= base[i];
return res;
} inline BASIS merge (BASIS x ,BASIS y) {
BASIS res;
res.init ();
dn (i ,61 ,0) if (x.base[i].any ()) res.insert (x.base[i]);
dn (i ,61 ,0) if (y.base[i].any ()) res.insert (y.base[i]);
return res;
}
}basis[N][21] ,ans;
inline void dfs (int u ,int fath){
fa[u][0] = fath ,dep[u] = dep[fath] + 1;
up (i ,1 ,20){
fa[u][i] = fa[fa[u][i - 1]][i - 1];
basis[u][i] = basis[u][i].merge (basis[u][i - 1] ,basis[fa[u][i - 1]][i - 1]);
} for (int v : edge[u])
if (v ^ fath) dfs (v ,u);
} inline int LCA (int u ,int v){
if (dep[u] < dep[v]) swap (u ,v);
while (dep[u] ^ dep[v]) u = fa[u][lg[dep[u] - dep[v]]];
if (u == v) return u;
dn (i ,20 ,0) if (fa[u][i] ^ fa[v][i]) u = fa[u][i] ,v = fa[v][i];
return fa[u][0];
} inline int kth (int x ,int k){
dn (i ,20 ,0) if ((k >> i) & 1) x = fa[x][i];
return x;
} inline void print (bitset <62> x){
int res = 0;
dn (i ,61 ,0) if (x.test(i)) res |= (1ll << i);
writeln (res);
} signed main (){
n = read () ,Q = read ();
up (i ,1 ,n) up (j ,0 ,20) basis[i][j].init ();
up (i ,1 ,n) {
a[i] = read ();
dn (j ,61 ,0) if ((a[i] >> j) & 1) w[i].set (j);
// dn (j ,5 ,0) cout << w.test (j); puts ("");
basis[i][0].insert (w[i]);
}
lg[0] = -1;
up (i ,1 ,n) lg[i] = lg[i >> 1] + 1;
up (i ,1 ,n - 1) {
int u = read () ,v = read ();
edge[u].push_back (v) ; edge[v].push_back (u);
} dfs (1 ,0);
while (Q --) {
int u = read () ,v = read ();
int L = LCA (u ,v);
int lg1 = lg[dep[u] - dep[L]] ,lg2 = lg[dep[v] - dep[L]];
ans.init ();
if (dep[u] - dep[L]){
ans = ans.merge (basis[u][lg1] ,basis[kth (u ,dep[u] - dep[L] - (1ll << lg1))][lg1]);
}
if (dep[v] - dep[L]){
ans = ans.merge (ans ,basis[v][lg2]);
ans = ans.merge (ans ,basis[kth (v ,dep[v] - dep[L] - (1ll << lg2))][lg2]);
}// ans.insert (w[u]) ,ans.insert (w[v]);
ans.insert (w[L]);
print (ans.qmax ());
}
return 0;
}
大战异或空间线性基的杂题?
准备再写两题就滚。
Round 1 P5556 圣剑护符
终于有一道紫色的线性基不看 tj AC 了(上面拿到紫本来想写树剖但是发现复杂度不对,又没学过淀粉质,于是 c 了一下思路 QWQ)。
没错这题就是树剖,树上修改和查询的问题不是树剖还是啥?!
首先我们从修改入手,直接套树剖和线段树就行了。
然后入手查询,但是好像不好做。
于是想到两个性质:
- 线性基中不存在子集使得异或和为 \(0\),一个数不能插入线性基当且仅当它能被线性基中的元素表示出来。
那么询问就是在问我们有没有数插不进线性基,有答案是 YES,否则是 NO。
- 线性基的大小至多为 \(\log_2 (V + 1)\),其中 \(V\) 为值域。
观察到值域为 \([1,2^{30} - 1]\),于是线性基打大小至多为 \(30\),那么插入第 \(31\) 个数的时候一定会存在异或和为 \(0\),所以两点之间的点数最多为 \(30\),距离最多为 \(29\),也就是说两点距离大于等于 \(30\) 的时候答案一定为 Yes (分析成 \(31\) 也没关系,反正点数很少,\(29\) 就有概率被 HACK 了)。
这时候我们为了求两点之间的距离,需要用 LCA,正好我们写了树剖就用它了!
然后因为点数最多 \(30\),所以可以暴力跳,再也不用想啥查询用树剖了。
时间复杂度为 \(\mathcal O(m\log n\log V)\),足以通过(\(\mathcal O(\log n)\) 是 query 中暴力跳的时候为了获取某个节点的异或值要用线段树)。
Coding time
PS:码风有点奇怪,是因为有的是从树链剖分那直接 copy + 修改的啊。:(
#include<bits/stdc++.h>
#define int long long
#define up(i ,x ,y) for (int i = x ; i <= y ; i ++)
#define dn(i ,x ,y) for (int i = x ; i >= y ; i --)
using namespace std;
inline int read (); inline void write (int x) ; inline void writesp (int x) ; inline void writeln (int x);
#define lc u << 1
#define rc u << 1 | 1
const int N = 1e5 + 10;
int n ,Q ,a[N];
vector <int> edge[N];
int fa[N], deep[N], sz[N], wson[N], top[N], id[N], w[N], cnt;
struct BASIS {//竟然是封装的线性基!
int base[31];
inline void init () {up (i ,0 ,30) base[i] = 0;}
inline bool insert (int x) {
dn (i ,30 ,0)
if ((x >> i) & 1) {
if (!base[i]) {base[i] = x ; return 1;}
x ^= base[i];
}
return 0;
}
} ans;
inline void dfs1(int u, int fath) {//未封装的树剖。
deep[u] = deep[fath] + 1 ,sz[u] = 1 ,fa[u] = fath;
for (int v : edge[u]) {
if (v == fath) continue;
dfs1(v, u);
sz[u] += sz[v];
if (sz[wson[u]] < sz[v]) wson[u] = v;
}
} inline void dfs2(int u, int t) { //处理 top ,id,nw。
top[u] = t ,id[u] = ++ cnt ,w[cnt] = a[u];
if (!wson[u]) return;
dfs2(wson[u], t);
for (int v : edge[u]) {
if (v == fa[u] || v == wson[u]) continue;
dfs2(v, v);
}
}
struct SGT {int l ,r ,XOR ,sum;}tr[N << 2];//未封装的线段树。
inline void pushup(int u) {
tr[u].sum = (tr[lc].sum ^ tr[rc].sum);
}
inline void pushdown(int u) {
if (tr[u].XOR) {
tr[lc].sum ^= ((tr[lc].r - tr[lc].l + 1) & 1 ? tr[u].XOR : 0);
tr[lc].XOR ^= tr[u].XOR;
tr[rc].sum ^= ((tr[rc].r - tr[rc].l + 1) & 1 ? tr[u].XOR : 0);
tr[rc].XOR ^= tr[u].XOR;
tr[u].XOR = 0;
}
}
inline void build(int u, int l, int r) {
tr[u] = {l, r, 0, w[r]};
if (l == r) return;
int mid = ((l + r) >> 1);
build(lc, l, mid);
build(rc, mid + 1, r);
pushup(u);
}
inline int query(int u, int x) {
int l = tr[u].l, r = tr[u].r;
if (l == r) return tr[u].sum;
pushdown(u);
int mid = ((l + r) >> 1);
if (x <= mid) return query(lc, x);
return query(rc, x);
}
inline void update(int u, int L, int R, int k) {
int l = tr[u].l, r = tr[u].r;
if (l >= L && r <= R) {
tr[u].XOR ^= k;
tr[u].sum ^= (r - l + 1) & 1 ? k : 0;
return;
}
pushdown(u);
int mid = ((l + r) >> 1);
if (L <= mid) update(lc, L, R, k);
if (mid < R) update(rc, L, R, k);
pushup(u);
}
inline void update_path(int u, int v, int k) {
while (top[u] != top[v]) {
if (deep[top[u]] < deep[top[v]]) swap(u, v);
update(1, id[top[u]], id[u], k);
u = fa[top[u]];
}
if (deep[u] > deep[v]) swap(u, v);
update(1, id[u], id[v], k);
}
inline int LCA (int u, int v) {
while (top[u] ^ top[v]) {
if (deep[top[u]] < deep[top[v]]) swap(u, v);
u = fa[top[u]];
}
if (deep[u] > deep[v]) swap(u, v);
return u;
}
signed main() {
n = read () ,Q = read ();
up (i ,1 ,n) a[i] = read ();
up (i ,1 ,n - 1) {
int u = read () ,v = read ();//cout << u << ' ' << v << endl;
edge[u].push_back (v) ; edge[v].push_back (u);
} dfs1 (1 ,0) ; dfs2 (1 ,1); build (1 ,1 ,n);
while (Q --) {
string str; cin >> str;
if (str[0] == 'U') {
int x = read () ,y = read () ,z = read ();
update_path (x ,y ,z);
} else {
int x = read () ,y = read ();
int L = LCA (x ,y);//用树剖替代倍增(偷懒)。
int dis = (deep[x] + deep[y] - 2 * deep[L]);//求树上两点距离的公式。
if (dis >= 30) {puts ("YES") ; continue; }
ans.init ();
bool f = 1;
while (x ^ L){//暴力跳。
f &= ans.insert (query (1 ,id[x])) ,x = fa[x];//注意是 query (1 ,id[x]) 不是 query (1 ,x),蒟蒻在写代码的时候写对了又删掉,提交时瞄了一眼才改过来。
//这里用 & 可以满足我们的要求,少些 if!
if (!f) break;
}
while (y ^ L){
f &= ans.insert (query (1 ,id[y])) ,y = fa[y];
if (!f) break;
} f &= ans.insert (query (1 ,id[L]));
puts (!f ? "YES" : "NO");
}
}
return 0;
}
inline int read() {
int x = 0, f = 0;
char ch = getchar();
while (!isdigit(ch)) {
f |= (ch == '-');
ch = getchar();
}
while (isdigit(ch))x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
return f ? -x : x;
}
inline void write(int x) {
if (x < 0)putchar('-'), x = -x;
if (x > 9)write(x / 10);
putchar(x % 10 | 48);
}
inline void writeln(int x) {
write(x), putchar('\n');
}
inline void writesp(int x) {
write(x), putchar(' ');
}
Round 2 [蓝桥杯 2024 国 Java B] 最优路径
蓝桥杯就是喜欢改编:(
我们还是有最大 XOR 和路径的经验,把环放到线性基里。
但是直接做又不能,原因是我们不知道 \(start\) 和 \(end\)。
如果我们从 \(0\) 建立超级源点,但是会 WA,以样例为例,\(basis\) 中含有 \(1\),\(2\),然而 \(dis_3 = 3\),这样求出来答案就成 \(0\) 了!(原因画一下图就知道了,留给读者实践一下)
于是我们发现 \(1\le n\le 500\),然后暴力枚举 \(start\) 并以 \(start\) 为源点,做和链接那题一样的 dfs 即可。
最后枚举 \(end\),如果能联通(注意不能是子集)就调用 qmin,得到的答案比最小就可以了。
如果还是一开始赋值的 \(+\infty\) 那就输出 \(-1\) 即可。
# include <bits/stdc++.h>
# define int long long
# define up(i ,x ,y) for (int i = x ; i <= y ; i ++)
# define dn(i ,x ,y) for (int i = x ; i >= y ; i --)
# define inf 1e18
using namespace std;
inline int read (){int s = 0 ; bool w = 0 ; char c = getchar () ; while (!isdigit (c)) {w |= (c == '-') ,c = getchar () ;} while (isdigit (c)){s = (s << 1) + (s << 3) + (c ^ 48) ; c = getchar ();}return w ? -s : s;}
inline void write (int x){if (x < 0) putchar ('-') ,x = -x; if (x > 9) write (x / 10) ; putchar (x % 10 | 48);}
inline void writesp (int x){write (x) ,putchar (' ');}
inline void writeln (int x){write (x) ,putchar ('\n');}
const int N = 505;
int n ,m ,basis[15] ,ans ,a[N] ,dis[N];
vector < pair <int ,int> > edge[N];
bool vis[N];
inline void insert (int x){//线性基板子,懒得封装了。
dn (i ,10 ,0) {
if ((x >> i) & 1) {
if (!basis[i]) {
basis[i] = x;
return ;
} x ^= basis[i];
}
}
} inline int qmin (int x){
int res = dis[x];//初值是 dis[x]。
// up (i ,0 ,5) cout << basis[i] << endl;
dn (i ,10 ,0) res = min (res ,res ^ basis[i]);//和 max 的做法类似,理解就行。
return res;
} inline void add (int u ,int v ,int w) {edge[u].push_back ({v ,w});}
inline void dfs (int u ,int Xor){//一样的 dfs。
vis[u] = 1 ,dis[u] = Xor;
for (auto i : edge[u]) {
int v = i.first ,w = i.second;
if (vis[v]) insert (Xor ^ w ^ dis[v]);
else dfs (v ,Xor ^ w);
}
} signed main (){
n = read () ,m = read ();
up (i ,1 ,m) {
int u = read () ,v = read () ,w = read ();
add (u ,v ,w) ; add (v ,u ,w);
} int res = inf;
up (start ,1 ,n) {//枚举 start。
up (i ,1 ,n) dis[i] = vis[i] = 0;
up (i ,1 ,10) basis[i] = 0;
dfs (start ,0);
up (_end ,1 ,n) {//枚举 end。
if (start ^ _end && vis[_end]) res = min (res ,qmin (_end));
}
} if (res == inf) puts ("-1") ; else writeln (res);
return 0;
}
关于更多的线性基题目(比如 CF、QOJ、UOJ)等。
蒟蒻因为篇幅问题,又因为懒,更因为我已经肝了 \(5\) 天了不然要在线性基里面泡死,不更了。
但是我找到一篇非常不错的博客,虽然很朴素但是是真优质,特点是例题特别多,容易上手。
点这里!!!!!!。
完结撒花★,°:.☆( ̄▽ ̄)/$:.°★ 。
本文来自博客园,作者:2021zjhs005,转载请注明原文链接:https://www.cnblogs.com/2021zjhs005/p/18970219

浙公网安备 33010602011771号