飘花效果

(题目讲解)异或空间线性基

比较简略咕咕咕。。。

当线性基碰上了区间修改和区间(全局)查询最大异或值

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\) 天了不然要在线性基里面泡死,不更了。

但是我找到一篇非常不错的博客,虽然很朴素但是是真优质,特点是例题特别多,容易上手。

这里!!!!!!

完结撒花★,°:.☆( ̄▽ ̄)/$:.°★

posted @ 2025-07-07 12:10  2021zjhs005  阅读(11)  评论(0)    收藏  举报