线性基

1 概念

线性基通常情况下指的是异或空间线性基。其标准定义是对于一个数集 S,以异或运算张成的数集与 S 相同的极大线性无关集。通俗的讲就是对于一个数集 S,线性基生成了一个集合,该集合中任意一些数的异或值组成的数集与 S 中任意一些数的异或值组成的数集相同,且该集合大小最小。

由此我们可以得出下面两条重要性质:

  • 原序列的任何一个数可以用线性基内部的一些数异或得到。

    证明显然。

  • 线性基内部任何数的异或不为 0

    证明考虑异或起来为 0 的几个数 a1,a2,akb,可以得到的是 a1a2ak=b,那么此时将 b 从线性基中删去仍不影响,就不满足线性基大小最小的条件了。所以异或值不可能是 0

下面我们来看线性基的基本操作与运用。

2 基本操作

2.1 构建

我们可以利用贪心法来构造线性基。假设我们前面已经构建好了线性基数组 p 的一部分,现在要插入 x。我们从高往低枚举 p,如果第 i 个二进制位上 x 的值为 1,我们将它和 pi 取异或,如果此时 pi 没有值,就代表我们需要向 pi 处插入此时的 x

不难发现的是,按照这样的插入方式,线性基上第 i 位存储的 pi 一定满足这个数二进制下第 i 位是 1,且更高位全部是 0

显然不管最后插入成功与否,我们都能保证 x 可以被表示。如果插入成功,那么插入的位置和前面一路走来异或过的值异或起来可以得到 x;否则就代表 x 本身就可以被表示。后面的插入不会影响前面的答案,所以这个做法是正确的。

代码如下:

//#define int long long
int p[50];
void ins(int x) {
    for(int i = 50; i >= 0; i--) {
        if(!(x >> i)) continue;//第 i 位上为 0 直接判掉
        //无需 &1 的原因是此时 x 比 i 位高的位一定都是 0 了
        if(!p[i]) {//直接插入
            p[i] = x; break;
        }
        x ^= p[i];
    }
}

显然,插入线性基的复杂度是 O(nlogV) 的,而线性基的空间是 O(logV)

2.2 求最大异或和

例题:【模板】线性基

我们可以构造出原数组的线性基,然后我们只要在线性基上求出异或最大值就是原数组的异或最大值了。在线性基上求异或最大值显然可以贪心,我们尽可能让当前位上是 1,如果现在求出的答案当前位是 0 且该位线性基不为 0,则直接异或即可;否则不作操作。

实际实现可以直接让 ans 与取异或后的值取 max 即可。代码如下:

int query() {
    int ans = 0;
    for(int i = 50; i >= 0; i--) {
        ans = max(ans, ans ^ p[i]);
    }
    return ans;
}

2.3 求最小异或和

我们只考虑线性基中的元素的话,最小值显然就是最小的有值的 pi。但是如果在插入的过程中我们有元素插入失败,也就是线性基的元素个数小于 n,就表明我们可以异或出来一个 0,那么最小值就应该是 0 了。

代码过分简单,不放了。主要是没有例题

2.4 线性基合并

这个东西其实非常简单,我们把一个线性基中的所有元素往另一个线性基里插一遍就完了。复杂度 O(log2V)

基于这个我们就可以写出一个封装后的模板:

//#define int long long
struct Linear_Basis {
	int p[61];
	void ins(int x) {
		for(int i = 60; i >= 0; i--) {
			if(!(x >> i)) continue;
			if(!p[i]) {
				p[i] = x; break;
			}
			x ^= p[i];
		}
	}
	Linear_Basis operator + (Linear_Basis b) {
		Linear_Basis c = *this;
		for(int i = 0; i <= 60; i++) {
			if(!b.p[i]) continue;
			for(int j = i; j >= 0; j--) {
				if(!(b.p[i] >> j)) continue;
				if(!c.p[j]) {
					c.p[j] = b.p[i];
					break;
				}
				b.p[i] ^= c.p[j];
			}
		}
		return c;
	}
};

3 进阶操作

3.1 求第 k 小 / 大异或和

这个就没有上面求极值那么简单了。我们以第 k 小为例,可以考虑到的是,对于线性基中第 i 个有值的位置,比它小的 i1 个数不管怎么异或都不会比它大(因为最高位不会超过第 i 位)。而比它小的数最多异或出 2i11 个数(先不考虑 0),那么当答案大于 2i11 的时候,我们肯定要异或上这个值才行。

那么我们就可以将 k 拆分二进制,从高位开始枚举,当从右往左数第 i 位上是 1 的时候,答案肯定大于 2i11,此时我们异或上第 i 个有值的二进制位,然后将 k 的这一位清零即可。不过发现我们没有必要要求枚举顺序和清零,因为最后肯定算的是 k 的二进制中为 1 的那几位。

但是接下来的问题就是,当我们的答案异或完一个值 x 的时候,比 x 小的数应该要按异或 x 之后的值排序,因为有的数的最高位可能改变,这样复杂度不够优秀。考虑到问题的根源在于对于一个最高位的 1 来说,在它之前就可能有人这一位上异或了 1

那么我们对线性基进行重构,使得对于每一个最高位的 1 来说,只有它自己在这一位上是 1 即可。这样就简单了,我们枚举 pi,然后从高位往低位枚举二进制位,如果这一位上是 1,就异或上对应的 pj 即可。代码如下:

void rebuild() {
    for(int i = 0; i <= 50; i++) {//暴力重构
        for(int j = i - 1; j >= 0; j--) {
            if((p[i] >> j) & 1) p[i] ^= p[j];
        }
    }
    cnt = 0;
    for(int i = 0; i <= 50; i++) {//记录有值的线性基值
        if(p[i]) d[++cnt] = p[i];
    }
}

那么查询之前先重构一次,然后按照刚刚讲的办法做即可。注意先特判有 0 的情况。代码如下:

int kth(int k) {
    if(cnt < n) k--;//有 0
    if((1ll << cnt) <= k) return -1;//线性基总数不够,直接返回
    rebuild();//重构
    int res = 0, pos = 1;
    while(k) {//拆二进制,找 1 的位
        if(k & 1) res ^= d[pos];//异或上
        pos++;
        k >>= 1;
    }
    return res;
}

不难发现对于动态插入和询问,求第 k 小的复杂度是 O(log2V) 的。

3.2 求排名

注意这里的排名指的是去重之后的排名,且要保证查询数可以被表示。不去重的排名见下面的例 1。

我们还是使用上面的 Rebuild 操作,操作完之后每一个最高位的 1 都只在当前位置上出现。那么对于 x 来讲,假设其第 i 位为 1,如果此时 pi 有值的话,我们只能异或上 pi 使得这一位上是 1,根据上一节的推论,我们此时的排名会加上 2k1k 表示 pi 是第 k 个有值的线性基位置);否则的话说明我们可以在后面异或出来这一位上的 1,那就不用管了。

不过可以发现,上面的判断操作只和 pi 有没有值有关,而 Rebuild 操作是不改变 pi 有没有值这个状态的,所以可以不进行 Rebuild 操作,直接查询即可。复杂度是 O(logV) 的。

代码如下:

int rnk(int x) {
    int res = 0, mul = 1;
    for(int i = 0; i <= 30; i++) {//枚举每一位
        if(!p[i]) continue;//要保证 p[i] 有值
        if((x >> i) & 1) {//x 这一位有值,加上 2^(k-1)
            res += mul;
        }
        mul <<= 1;//记录当前的 2^k
    }
    res++;//如果要算空集的 0,这一行要加上
    return res;
}

3.3 前缀线性基

这个东西可以在离线的情况下实现区间线性基的效果,并且复杂度基本复杂度还是 O(logV)

假如我们现在枚举到的右端点是 r,我们希望从右到左建出线性基,此时我们除了 pi 以外再记录一个 posi,表示这一位上的数是插入 aposi 时候插入进来的。这样假如我们查询的是 [l,r] 的信息,只需要看 posil 的部分就可以得到 [l,r] 的线性基了。

问题就在于当我们从 rr+1 的时候怎样转移,我们本来是从右往左建,所以第一个插的是 ar+1。当我们来到它本来应该在的位置 pi 的时候,这个位置上可能已经有数字了。但是现在 ar+1 插入时间更早,所以它应该在这里,那么我们就将这个位置的数改成 ar+1,而原本的 pi 就代替 ar+1 继续向下找插入位置。

所以原理其实还是线性基的贪心构建思想,假如现在数字 ax 要插到 pi,若 posi<x,说明这个位置上的数应该是 ax,将 axpi 交换、posix 交换,然后继续向下插即可。如此可以保证 posi 都是最大的,即插入时间尽可能早,就能保证查询时的区间是正确的。

代码如下:

void ins(int x, int p) {
    for(int i = 50; i >= 0; i--) {
        if(!(x >> i)) continue;
        if(!p[i]) {
            p[i] = x, pos[i] = p;
            break;
        }
        else if(pos[i] < p) {
            swap(p[i], x), swap(pos[i], p);
        }
        x ^= p[i];
    }
}

3.4 图上线性基

其实这个图上线性基指的是一类用线性基求解的图论问题,他们的基本思想都是一致的。

首先要提到一道经典例题:[WC2011] 最大XOR和路径

由于路径可能有重点重边,所以难以用最短路算法来求解。考虑到这个路径是异或起来的,所以我们只需要关注一条边走过的次数即可。考虑到我们从 1n,要么直接走了一条简单路径,要么是在路径之外还走了环。但是发现如果我们要去走环,那么走到环和走回来的路径是一致的,因此会被抵消掉。

于是可以得出一个重要结论:答案的权值是一条 1n 的简单路径和若干个环的权值异或而成的。

然后考虑怎样求这个东西。首先看环,我们可以跑一边 DFS 树,只保留有一条非树边的环。可以发现,对于其它的不满足这个要求的环,一定可以用这些环异或得到,所以不需要考虑。

接着考虑 1n 的简单路径,我们其实只需要随便取一条即可。因为即使有若干条,它们也一定构成了环,通过异或这些环就可以得到另外的路径了。

所以我们只需要将所有含一条非树边的环扔进线性基,然后查询 1n 任意一条简单路径在线性基中的最大异或和即可。

4 例题

例 1 albus 就是要第一个出场

Link

发现这道题就是要求排名,但是换成了不去重的排名。

先用上文讲过的 rnk 操作求出其在不可重集下的排名,然后考虑前面的每一种数字出现了多少次。发现重复的来源就是一些异或和为 0 的部分,我们考虑在插入的时候插入失败的一些数,他们共有 ncnt 个。这些数一定可以和线性基中的一些数异或起来得到 0,那么我们从中任选几个就可以凑出异或和为 0 的部分。注意到一些数可能选多次,不过我们只关注选的次数的奇偶性,因此不会不合法。

那么任选数字的方案数应该有 2ncnt1 种,加上原来不异或 0 的方案,共 2ncnt 种。所以前面每一种数字出现了 2ncnt 次,答案即为 (rnk1)×2ncnt+1

例 2 [BZOJ3811] 玛里苟斯

题意: 求出一个可重集 S 的所有子集的 异或和的 k 次方 的期望。

由于异或较难处理,所以必须要拆位。我们要求的是 E(xk),考虑转化为 E((xi×2i)k),其中 xix 的二进制位中的第 i 位。然后进一步转化期望为总方案乘概率,可以得到 (xi×2i)k2n

然后看到 k 次方就必须要想到转化组合意义,我们可以得到 (xi×2i)k 的组合意义为:从 x 的二进制位中任意选出 k 个为 1 的二进制位 xi,钦定一种选法的方案为 2i,求所有选法的权值和。

考虑到 k 并不大,并且二进制位最多 63 个,直接爆搜出这 k 个二进制位 xi。接下来就是要求有多少个子集的异或和在这 k 个位置上是 1 了。考虑将每个数中的这 k 个位置提出来,扔到线性基里。然后我们看这 k 个二进制位构成的一个数字是否能被线性基中的数表达。如果可以,那么根据上一道题的结论,我们的方案数应该有 2ncnt 种,加上权值 2i 和概率 12n,可得当前的贡献为 2icnt;否则的话没有任何贡献。

但是如果每一次都插入 n 个数的话过于浪费时间,我们在最开始就先将所有数插一遍线性基,只保留插入成功的数,它们构成的线性基一定也可以表示剩下的数,所以只需要保留这些数即可。

时间复杂度 O(logk+1V),由于保证了答案不超过 263,所以 logV 会随 k 的增大不断递减,因此可以通过。

基于这个数据特点还有一种做法不难想到,由于 k3 的时候 logV 只有不到 21,足以 O(2logV) 暴力求解;而 k2 的情况就不难直接拆位求解了。剩余细节留给读者思考。

例 3 [BZOJ3569] DZY Loves Chinese II

Link

这个题如果不强制在线的话理论上可以线段树分治乱做,但是有强制在线就会很难受。

首先会去考虑连通图怎样才能变成非连通图,即考虑要删掉哪些边。我们先随意建一棵 DFS 树,如果我们只删非树边的话一定不行,因为还有树边,所以至少要删一条树边。考虑删掉这条树边后怎样才能让这个树边的两端在两个连通块中,发现必须要让所有跨越它的非树边都被删掉。

现在的问题在于如何判断这个东西,考虑利用随机哈希,对非树边赋随机权值,每一条树边赋所有跨越它的非树边的权值异或和。那么如果删去的边集中有子集异或和为 0,就满足了上面的条件,即为非连通图,否则为连通图。

判断是否有子集异或和为 0 就是线性基的基本操作了,复杂度 O(qklogV)

例 4 [Beijing2011] 梦想封印

题意:给定一张无向图,单次删去一条边,定义一条路径的权值为经过的边的权值异或和。求从 1 出发能走出的所有路径的权值数量(不包括 0)。

我们需要运用上面讲过的有关图论的技巧,仍然考虑拆成链和环考虑。把环直接扔到线性基里,现在的问题就是从链中任选一条,和若干个环异或后能得到多少不同的值。

考虑这样一件事:对于两个数 a,b 和一个线性基,如果 a,b 在线性基中的异或最小值相等,那么这两个数在线性基中能异或出来的数也相等。实际上就是考虑将 a,b 插入,二者最后插入的值相等或均插入失败,线性基的张集是一样的。

那么我们对于所有的链就可以这样维护,把链扔到线性基中扫一遍,去重后计算答案即可。显然这可以用 set 维护,而最后的答案就是 p×2cnt1pset 的大小,cnt 为线性基大小,1 是为了去掉 0

现在考虑如何动态维护这个东西。我们先套路的删边改加边,然后分类讨论:

  • 如果 u,v 均与 1 相连,加入这个环即可。
  • 如果 u,v 有一个与 1 相连,暴力跑一遍另外一个点对应的图,跑出一棵 DFS 树然后将上面对应的环和链加入即可。
  • 如果 u,v 均不与 1 相连,只连边即可。

注意每次插入数字到线性基中后要重构一边 set。时间复杂度在于重构的复杂度,最多重构 O(logV) 次,每次重构遍历查询的复杂度是 O(nlogV),总复杂度 O(nlog2V)

例 5 [SCOI2016] 幸运数字

Link

首先看到这是个树上问题,求异或最大值,第一个想到的肯定是树剖。但是为了维护异或最大值的话我们需要线性基,并且树剖后查的还是区间线性基,需要用线段树维护线性基。这样做的复杂度是 O(qlog2nlog2V) 的,特别不优,但是由于线性基常数较小可以勉强在 6s 内卡过。

考虑处理树上问题的另一种方式,即倍增。预处理 LB(x,i) 表示 x 到它的 2i 级祖先路径上所有数构成的线性基,然后单次查询倍增合并线性基即可,复杂度降到 O(qlognlog2V)。由于常数小所以这个可以稳过。

但实际上理论复杂度最优的解法不止于此。考虑线性基合并是具有可重复贡献性的,所以不需要保证插入的线性基在树上是不重合的。因此可以借鉴 ST 表的查询思路,将 ulca 的路径拆成两个线性基,然后直接合并即可。这样我们只需要做 O(1) 次线性基合并,复杂度 O(qlog2V),可以通过。

posted @   UKE_Automation  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示