线性基

https://www.luogu.com.cn/blog/szxkk/solution-p3813

用处

线性基是可以帮助我们 \(O(n \log n)\) 找到一个数组中的数异或出来的数 最大值/最小值/\(k\) 大值/包含不包含某个数 的东西。

定义

  • 线性无关:若数组 \(S\) 线性无关,那么它的任何一个数都无法被另外若干个数异或出来。换句话说,没有 \(S\) 的任何一个子集的异或和为 \(0\)
  • 线性基:数组 \(S\) 的线性基 \(B\) 也是一个数组,它线性无关,并且可以异或出 \(S\) 任何一个子集的异或和。

求法

采取逐个数插入的方式。一次插入的核心目标是使原数组的这个数能表示成线性基内若干个数的异或和,如果现有的线性基内元素不足以异或出它,那么就插入一个可以异或出它的数。否则就不要插入了,否则就不满足线性无关了。

最后出来的是一个最多 \(\log_2 n\) 个元素的数组,这些元素分别按照自己的最高位标号,为 \(p_i\)。不存在两个最高位为同一位的数。(看看下面的插入过程就知道了)\(p_i\) 也可以为 \(0\),代表线性基里不存在以 \(p_i\) 为最高位的数。

f(i, 1, n) {
	int x = a[i];
	for(int i = 62; i >= 0; i--) {
		if((x << i) & 1) {
			if(!p[i]) {p[i] = x; break;}
			else {x ^= p[i];}
		}
	}
}

例如:
\(S = \{(1001)_2,(100)_2,(101)_2,(11)_2\}\)
\(p_0 = (1)_2,p_1=(11)_2,p_2=(100)_2,p_3=(1001)_2\)

证明

性质 \(1\):原有线性基中元素可以将其异或出来。
如果在插入一个元素之前异或了 \(p_1, p_2,...,p_k\),最后插入的是 \(p_k'\)(或者没有插入即为 \(0\))那么 \(p_1 \oplus p_2 \oplus ... \oplus p_k \oplus p_k'\) 等于这个元素。

性质 \(2\):线性基满足线性无关。
如果 \(p_1 \oplus p_2 \oplus ... \oplus p_k = 0\),当 \(k=1\) 时即 \(p_1=0\),显然矛盾,因为线性基里不存在 \(0\)。否则,\(p_1 \oplus ... \oplus p_{k-1} = p_k\),那么 \(p_k\) 不会被加入线性基中,而是会异或掉这些东西。

用法

最经典的应用之一就是寻找原数组内的任意子集异或和的最大值了。
由于线性基按照位数排序,更低位的数不会存在更高位,所以我们可以直接从高位到低位贪心,如果当前结果异或 \(p_i\) 比当前结果大,那么就采用异或后的结果。否则采用当前结果。
举个例子:当前结果 \((100101)_2\)\(p_4 = 11010\),那么我们肯定选择异或之后 \((110000)_2\) 而不是之前的,因为只要高位有一个 \(1\)\(0\) 大,后面再小都是大。

P3812
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
int a[55];
int p[55];
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
    //think twice,code once.
    //think once,debug forever.
    int n; cin >> n;
    f(i, 1, n) {
        cin >> a[i]; int x = a[i];
        for(int i = 51; i >= 0; i--) {
            if((x >> i) & 1) {
                if(!p[i]) {p[i] = x; break;}
                else x ^= p[i];
            }
        }
    }
    int ans = 0;
    for(int i = 51; i >= 0; i--) {
        if((ans ^ p[i]) > ans) ans ^= p[i];
    }
    cout << ans << endl;
    return 0;
}

如果要查找原数组的真子集组成的异或和最小值,那么先记录一下能不能生成 \(0\)(当一个数无法插入的时候就是生成 \(0\) 了)然后如果行就输出 \(0\),不行就返回线性基里最小的那个数。
证明:假设最小的数是 \(p_i\),如果还可以生成更小的数,那么一定是可以把 \(p_i\) 抵消掉的。这个数在线性基里的位置肯定不可能比 \(1\) 还低位,否则不可能在第 \(i\) 位为 \(1\)。那么一定是更大的数,那么会贡献一个更高位的 \(1\),想抵消就只能找更高位借...直到借不到为止。于是失败了。故这个数一定是能异或最小的数。

查询排名啥的。留坑。

HDU7184

题意:
有一个长度为 \(n\) 的数组,其中保证存在两个一样的数。可以进行若干个操作,使得 \(a_{l,...,r}\) 都赋值为 \(a_l \oplus a_{l+1} \oplus ... \oplus a_r\)。这些操作之后,你需要得到所有元素都一样的数组。求这个一样的元素的最大值。
\(\sum n \le 10^6\)

分析:
是个思维+结论题,可以证明,从这 \(n\) 个数里任取一些数异或起来的方案,都是可以构造出对应的操作来做到的。
所以,问题完全等价于给 \(n\) 个数,从中选一些数,使得这些数的异或和最大。
这是线性基的板题,抄一个板子即可。

下面给出证明:

1、如果序列里有连续的两个 \(0\),那么一定都可以构造出操作方案。
因为可以一直执行两种操作:跳过/擦除。
跳过,指两个 \(0\) 旁边有一个需要保留的数,需要把它挪到另一边,那么 \(0,0,x \rightarrow x,x,x \rightarrow x,0,0\) 即可。
擦除,指两个 \(0\) 旁边有一个不需要保留的数,需要将它同化为 \(0\),那么 \(0,0,x \rightarrow 0,x,x \rightarrow 0,0,0\) 即可。
因为上述操作总可以保证操作完后至少还有两个 \(0\),所以可以以此类推一直往一边处理。
如果两个 \(0\) 两边都有数字当然也没问题,我们先
处理完一边儿的再回过头来处理另一边。

2、如果用 \(A\) 来表示一个需要保留的数字,\(B\) 来表示一个想要删去的数字,则出现 \(AAB\)\(BAA\) 的形状时,可以通过操作删掉想删的那个数并制造出两个 \(0\)。以 \(AAB\) 为例:\(x,y,B\rightarrow x\oplus y,x\oplus y,B \rightarrow x\oplus y \oplus B,x\oplus y \oplus B\rightarrow x\oplus y,0,0\) 就可以做到只删掉 \(B\) 而产生两个 \(0\)

3、由上面2和1可以看出,只要出现 \(AAB\)\(BAA\) 就可以完全解决问题,也就是出现连续两个 \(A\) 就完全可以构造出任意方案,而有连续的两个 \(B\) 显然更容易完全解决问题。综上,有连续两个 \(A\) 和连续两个 \(B\) 的序列,都可以造出来任意的异或方案。

4、所以现在唯一处理不了的就是既没有连续 \(A\) 也没有连续 \(B\) 的序列,也就是形如 \(ABABABAB\)\(BABABABA\) 这样的情况,这时就要用到两个数相同的条件,该条件实际上是保证了不会出现这种情况。假设相同的两个数都是 \(k\),如果最后方案里没有异或上 \(k\),则两个 \(k\) 的位置可以都当作 \(0\) 或都当作 \(1\);如果最后的方案里有异或上 \(k\),则两个 \(k\) 可以一个填 \(0\) 一个填 \(1\),也可以一个填 \(1\) 一个填 \(0\)。而上述两种情况下,都有两种填法,且两种填法之一一定保证存在连续 \(A\) 或连续 \(B\),因此不会出现这种情况。

posted @ 2022-08-11 21:47  OIer某罗  阅读(46)  评论(0编辑  收藏  举报