【模版】线性基 + 变形


线性基(极值、区间极值、第k大小)


线性基的性质:
1. 1. 1.线性基能相互异或得到原集合的所有相互异或得到的值。
2. 2. 2.线性基是满足性质 1 1 1 的最小的集合
3. 3. 3.线性基没有异或和为 0 0 0 的子集。


先引入 p [ i ] p[i] p[i] 的概念和用法:
① ① p [ i ] p[i] p[i] 记录 原集合的数字,该数字的二进制下从小到大第 i i i 位为1。
② ② 每个数字最多被记录一次。
③ ③ 数字优先从高位 " 1 " "1" "1"到低位 " 1 " "1" "1"记录!(即从该数的二进制的最高位的 " 1 " "1" "1" 开始记录,一旦发现 p [ i ] = 0 p[i] = 0 p[i]=0即存入 p [ i ] = x p[i] = x p[i]=x ,然后枚举下一个数字。)


构造线性基的方法:
对于原集合中的每一个数:
① ① 若其二进制下最高位第 i i i 位的 “ 1 ” “1” 1未被记录过,即 ( p [ i ] = 0 ) (p[i] = 0) (p[i]=0),则更新令 p [ i ] = x p[i]=x p[i]=x
② ② 否则在寻找其次高位下的 “ 1 ” “1” 1,直到枚举到其第 0 0 0


给定 n n n 个整数(数字可能重复)。
求在这些数中选取任意个,使得他们的异或和最大。


选择数字的方式显然贪心,二进制下 高位的 1 1 1 显然比 低位的 1 1 1 优先选择。
那么考虑当一个数字对答案的贡献时,我们用 p [ i ] p[i] p[i] 数组记录 其 二进制下的最大贡献,即贡献至少为 2 i 2^i 2i。但如果 另出现一个数字,与之前 p [ i ] p[i] p[i] 中记录的数字的最高位 均为 1 1 1 的时候,我们只能二选一,所以我们需要将后来的数字异或 p [ i ] p[i] p[i],得到新数字,然后继续向下枚举新数字的次高位 1 1 1 。能记录则记录。

void seperate(long long x)
{
    for(int i = 50; i >= 0; i--)
    {
        if(x >> (long long)i == 0)  continue;
        if(!p[i])
        {
            p[i] = x;
            return;
        }
        x ^= p[i];
    }
}

可能初学者会产生一个问题,若出现若干个数字,其最高位都是同一个位置,我们应该如何选择呢?
若论我们的选法,我们的 最高位的 i i i p [ i ] p[i] p[i] 只会记录第一个出现最高位的数字。尽管后面出现的数字的最高位均与 p [ i ] p[i] p[i] 相同,但是没法选入 p [ i ] p[i] p[i] 。我们的算法是这些数字 依次 与 p [ i ] p[i] p[i] 异或,然后寻找次高位的 " 1 " "1" "1",但会出现两种情况:
我们不妨设 p [ i ] = x p[i] = x p[i]=x , 与 x x x 最高位冲突的数字设为 y y y
① ① 原来记录的数字 x x x 可以贡献 最高位 和 次高位的答案,即 x x x 的次高位要比 y y y 的次高位要高,那么显然 记录 x x x 答案会更大一些。算法没有问题
② ② 原来记录的数字 x x x 的次高位不如 y y y 的次高位高, 那么显然当 y y y 异或 x x x 的的时候, y y y 的次高位 " 1 " "1" "1" 与 对应位下的 x x x " 0 " "0" "0" 一异或,答案为 1 1 1,那么我们对应的 p [ j ] p[j] p[j] 就可以记录 x x x 异或 y y y 的答案了。 那么我们答案会统计 两个 p [ i ] p[i] p[i] 数组的值,这两个数组的贡献仍然是最大的,可以理解为代替 ① ① 的一个 p [ i ] p[i] p[i] 答案,并且没有冲突,我们仍然遵循贪心的侧罗。那么显然算法也不会出现问题。


完整代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <cstdlib>
using namespace std;
int n;
long long ans, p[60];
long long read()
{
    long long rt = 0, in = 1; char ch = getchar();
    while(ch < '0' || ch > '9') {if(ch == '-') in = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9') {rt = rt * 10 + ch - '0'; ch = getchar();}
    return rt * in;
}

void seperate(long long x)
{
    for(int i = 50; i >= 0; i--)
    {
        if(x >> (long long)i == 0)  continue;
        if(!p[i])
        {
            p[i] = x;
            return;
        }
        x ^= p[i];
    }
}

int main()
{  
    n = read();
    for(int i = 1; i <= n; i++)
    {
        long long x = read();
        seperate(x);
    }
    for(int i = 50; i >= 0; i--)    ans = max(ans, ans ^ p[i]);
    printf("%lld",ans);
    system("pause");
    return 0;
}

例题:
[模版]线性基
WC 2011 最大异或和


线性基变形


在洛谷提供的模版上,我们只写了 n n n 个数里 任意挑选数,使得其中异或值最大。
若规定好区间,询问区间内的最大值或者最小值,或者能否通过异或得到数字 K K K ,我们就需要对原来的线性基模版进行变形推广。


N N N 个数字,有 Q Q Q 次询问:

询问 1 1 1: 格式: 1 1 1 L L L R R R 。求区间 [ L , R ] [L,R] [L,R]中任意数字的最大异或值

询问 2 2 2: 格式: 2 2 2 L L L R R R 。求区间 [ L , R ] [L,R] [L,R]中任意数字的最小异或值

询问 3 3 3: 格式: 3 3 3 L L L R R R K K K 。如果在 [ L , R ] [L,R] [L,R] 中能否通过异或得到数字 K K K,则输出 Y E S YES YES,否则输出 N O NO NO


对于这道题,又对于之前线性基的模版:

p [ i ] p[i] p[i] 数组不再适用,因为其保存的数字可能不是由区间 [ L , R ] [L,R] [L,R] 中异或得来的。
我们需要另辟蹊径,将 p [ i ] p[i] p[i] 开成 二维 p [ j ] [ i ] p[j][i] p[j][i] ,这个二维数组表示 在前 j j j 个数中二进制下第 i i i 位 存的是 哪个数字(这个数字可能是由 原数 之间 异或得来的, 大小并不一定是 原来的数字)。再换句话说,二维的 p [ j ] [ i ] p[j][i] p[j][i] p [ i ] p[i] p[i] 多记录了 N − 1 N-1 N1 个状态。开这个数组的意义就是为了存状态的。

我们还需要确定, 对于 每个 p [ j ] [ i ] = x p[j][i] = x p[j][i]=x x x x 这个数字是插入哪个数字得到的,我们需要确定这个数字是否在区间 [ L , R ] [L,R] [L,R] 中。因此还需要在一个同 p [ j ] [ i ] p[j][i] p[j][i] 大小空间相同的 数组 p o s [ j ] [ i ] pos[j][i] pos[j][i] 。记录 p [ j ] [ i ] p[j][i] p[j][i] 的 数字 是由 第几个数异或 得来的。

在插入一个数字前,先用递推式 获得并记录 前 ( i d − 1 ) (id-1) (id1) ( i d id id 表示这个数字是第几个) 个数字时的 p [ j ] [ i ] p[j][i] p[j][i] p o s [ j ] [ i ] pos[j][i] pos[j][i] 状态。 然后对于插入的数字 X X X ,找其二进制下的从高位到低位的 1 1 1
对于数字 X X X 其二进制下的从高位到低位的 1 1 1 (设对应在第 i i i 位):
1. 1. 1. p [ j ] [ i ] p[j][i] p[j][i] 未存入数字,那把当前数字放入,然后更新 p o s [ j ] [ i ] pos[j][i] pos[j][i] 数组,结束此流程。
2. 2. 2. p [ j ] [ i ] p[j][i] p[j][i] 已经存入了其他数字,并且放入的这个数字是在 i d id id 之前。那么我们的选择是,替换掉这个数字,然后 p o s [ j ] [ i ] pos[j][i] pos[j][i] 也被当前 i d id id 替换掉(注意此时 i d id id 的值被更新)。然后 令 X = X X = X X=X ^ p [ j ] [ i ] p[j][i] p[j][i] 。 然后重复此流程(但 1 1 1是从当前位向低位继续枚举)。

此时初次学习 可能会有几个小问题,我列举两个典型问题。

Q 1. Q1. Q1.: 为什么当满足上述条件 2 2 2 时,数字一定要被替换呢?

A 1. A1. A1.: 因为每次询问都是固定区间 [ L , R ] [L,R] [L,R]。我们要保证区间里的数字一定是最新的,只有这样才能保证区间 [ L , R ] [L,R] [L,R] 正确使用。(因为对于每一个 i i i ,我们都要用最新的数字来更新它,同时也解释了为什么还要有“放入的这个数字是在 i d id id 之前”这个限制条件)

Q 2. Q2. Q2.: 为什么取代之后,还要令 X = X X = X X=X ^ p [ j ] [ i ] p[j][i] p[j][i] 呢?

A 2. A2. A2.: 二进制的 第 i i i 位被操作 2 2 2 更新了,那么我们需要取 这两个数次高位的 1 1 1 , 那么用什么方法 能确定这两个数 哪个数的次高位 1 1 1 靠前呢,我们只需要异或一下,因为
①如果两个数在同一个位置次高位都是 1 1 1。那么替换后的答案不变,无事发生。
②如果两个数次高位 1 1 1 不在同一位,那么显然 高次的 1 1 1 对答案贡献会大,因此我们异或是为了取 下一个高次 1 1 1 的。用下一个高次 1 1 1 来更新后边的 p [ j ] [ i ] p[j][i] p[j][i] p o s [ j ] [ i ] pos[j][i] pos[j][i] 数组。

上述内容的代码如图所示:

void seperate(int x, int id)
{
    int t = id;
    for(int i = 30; i >= 0; i--)
    {
        p[id][i] = p[id-1][i];
        pos[id][i] = pos[id-1][i];
    }
    for(int i = 30; i >= 0; i--)
    {
        if(x >> i == 0) continue;
        if(!p[id][i])
        {
            p[id][i] = x;
            pos[id][i] = t;
            return;
        }
        else if(pos[id][i] < t)
        {
            swap(p[id][i], x);
            swap(pos[id][i], t);
            
        }
        x ^= p[id][i];
    } 
}

在查询给定的区间 [ L , R ] [L,R] [L,R] 异或最大或最小值时,显然我们要从 p [ r ] [ i ] p[r][i] p[r][i] 中查询给定的区间的数字。只有这样才能把所有数字都用上,但是用上的数字还必须属于 [ L , R ] [L,R] [L,R],因此再加一个限定条件 p o s [ r ] [ i ] ≥ l pos[r][i] \geq l pos[r][i]l

一、查询最大值还是原来的求最大值方法,贪心取高位的 1 1 1 ,再用大小判断 取得的高位 1 1 1 不会被下一个 p [ j ] [ i ] p[j][i] p[j][i] 通过异或抵消掉。

算法正确性证明:
高位的 1 1 1 大于 低位所有 1 1 1 。那么贪心从高位 i i i 选择 p [ j ] [ i ] p[j][i] p[j][i]进行异或。
高位的 1 1 1 在选择的同时,低位的 1 1 1 可能会与之一同选上。但枚举到低位 1 1 1 的时候,我们选择它不一定会令答案变大,因此需要比较一下。同时,我们选择低位的 p [ j ] [ i ] p[j][i] p[j][i] 时,它一定不会影响高位的 1 1 1,因为在预处理 p [ j ] [ i ] p[j][i] p[j][i]的时候,如果同为 1 1 1, 我们令其与原来的 p [ j ] [ i ] p[j][i] p[j][i] 进行异或了,高位 1 1 1 相抵消成 0 0 0 了。

二、查询最小值,直接正序查找就好了,当 i i i 最小且 p [ j ] [ i ] p[j][i] p[j][i] 存在即可。

算法正确性证明:
X X X 为最小值答案,再设 存在一个 p [ j ] [ i ] p[j][i] p[j][i] X X X 小,若 p [ j ] [ i ] p[j][i] p[j][i] X X X 小,(二进制下 即 p [ j ] [ i ] p[j][i] p[j][i] 最高位的 1 1 1 一定不会早于 X X X)那么用反证法证明:

①假设 我们的答案 X X X 二进制下的 高位 1 1 1 p [ j ] [ i ] p[j][i] p[j][i] 二进制下的高位 1 1 1 要靠前!但假设不成立,因为我们 i i i 0 0 0开始枚举的, p [ j ] [ i ] p[j][i] p[j][i] 会比 X X X 更早被枚举到,因此答案不会被记为 X X X
②假设 我们的答案 X X X 二进制下的 高位 1 1 1 p [ j ] [ i ] p[j][i] p[j][i] 二进制下的高位 1 1 1 相同。那么既然我们的答案由 p [ j ] [ i ] p[j][i] p[j][i] 得到, 又存在一个新的 p [ j ] [ i ] p[j][i] p[j][i] 比当前答案小,那么显然这两个答案异或一下,得到的答案更小,这与假设的答案 p [ j ] [ i ] p[j][i] p[j][i]最小矛盾,所以假设不成立。

代码如图所示:

int query_max(int l, int r)
{
    int ans = 0;
    for(int i = 30; i >= 0; i--)
        if(pos[r][i] >= l && ans^p[r][i] > ans)
            ans ^= p[r][i];
    return ans;
}
int query_min(int l, int r)
{
    for(int i = 0; i <= 30; i++)
        if(pos[r][i] >= l && p[r][i])
            return p[r][i];
    return 0;
}

对于操作 3 3 3 ,我们要查找 在区间 [ L , R ] [L,R] [L,R] 里能否通过异或得到数字 k k k
那么把 k k k 自行看成二进制数,我们只要在符合范围条件的 线性基中,通过异或,把 二进制下的 k k k 1 1 1 都通过异或消掉,最后把 k k k 消成 0 0 0 就好。
那么 k k k 二进制里每一个 为 1 1 1 i i i 位,我们必须令 p [ j ] [ i ] p[j][i] p[j][i] 与其异或,才能消掉,那么一路枚举下来,逢 1 1 1 便异或,如果 k k k 最后消成 0 0 0 ,那么就说明可以 通过区间里的数字异或得到 k k k

算法正确性证明:
在构造 p [ j ] [ i ] p[j][i] p[j][i] 的时候,我们通过 冲突时 进行异或的方式,找下一个 高位 1 1 1 填入下一个低位 p [ j ] [ i ] p[j][i] p[j][i] 。因此我们这种处理方式 是将 存入值的 p [ j ] [ i ] p[j][i] p[j][i] 的数量最多化。同时冲突时,我们进行了异或,高位 1 1 1 便转化成了 0 0 0。也就是说 p [ j ] [ i ] p[j][i] p[j][i] 的数的大小 超不过 2 i + 1 2^{i+1} 2i+1。也就是二进制下 比 i i i 次高的二进制位上都是 0 0 0 。因此我们顺着枚举下来就好且顺序从高位到地位,因为选高位的时候,会影响低位的答案!

代码如下:

int query_min(int l, int r)
{
    for(int i = 0; i <= 30; i++)
        if(pos[r][i] >= l && p[r][i])
            return p[r][i];
    return 0;
}

完整代码如下:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <cstdlib>
using namespace std;
int n,q;
int pos[101010][40],p[101010][40];
int read()
{
    int rt = 0, in = 1; char ch = getchar();
    while(ch < '0' || ch > '9') {if(ch == '-') in = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9') {rt = rt * 10 + ch - '0'; ch = getchar();}
    return rt * in;
}
int query_max(int l, int r)
{
    int ans = 0;
    for(int i = 30; i >= 0; i--)
        if(pos[r][i] >= l && ans^p[r][i] > ans)
            ans ^= p[r][i];
    return ans;
}
int query_min(int l, int r)
{
    for(int i = 0; i <= 30; i++)
        if(pos[r][i] >= l && p[r][i])
            return p[r][i];
    return 0;
}
bool exist(int l, int r, int k)
{
    for(int i = 30; i >= 0; i--)
        if(pos[r][i] >= l && (k>>i))
            k ^= p[r][i];
    return (k == 0) ? true : false;
}
void seperate(int x, int id)
{
    int t = id;
    for(int i = 30; i >= 0; i--)
    {
        p[id][i] = p[id-1][i];
        pos[id][i] = pos[id-1][i];
    }
    for(int i = 30; i >= 0; i--)
    {
        if(x >> i == 0) continue;
        if(!p[id][i])
        {
            p[id][i] = x;
            pos[id][i] = t;
            return;
        }
        else if(pos[id][i] < t)
        {
            swap(p[id][i], x);
            swap(pos[id][i], t);
            
        }
        x ^= p[id][i];
    } 
}
int main()
{
    n = read(), q = read();
    for(int i = 1; i <= n; i++)
    {
        int x = read();
        seperate(x, i);
    }
    for(int i = 1; i <= q; i++)
    {
        int ins = read(), l = read(), r = read();
        if(ins == 1)    printf("%d\n",query_max(l, r));
        else if(ins == 2)   printf("%d\n",query_min(l, r));
        else if(ins == 3)
        {
            int k = read();
            printf(exist(l, r, k) ? "YES\n" : "NO\n");
        }
    }
    system("pause");
    return 0;
}

例题:暂无


给定 n n n 个整数(数字可能重复)。
输出异或第 k k k 小的数。


p [ i ] p[i] p[i] 数组与最初的异或求最大值代码中的一样。
只是多了一个 r e b u i l d rebuild rebuild 函数。

r e b u i l d rebuild rebuild 操作 类似于化学的提纯,每一个 p [ i ] p[i] p[i] 尽量只保留最高第 i i i 位的 1 1 1。其他位的 1 1 1 通过异或消掉。
然后记录重构的 p [ i ] p[i] p[i] 数组就好啦。

对于查询:
先判断新的 p [ i ] p[i] p[i] 数量是否与 n n n 相同,如果不相同则说明,存在两个数异或值为 0 0 0 ,所以 k k k 应该 自减 1 1 1
k k k 用二进制考虑,考虑选 p [ i ] p[i] p[i]
从最大位开始选择,选和不选都 第k大异或值都是 2 i 2^i 2i 大小的影响。
所以这就是把 k k k 用二进制考虑的原因。

完整代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <cstdlib>
using namespace std;
int n,k,cnt;
int p[40];
int read()
{
    int rt = 0, in = 1; char ch = getchar();
    while(ch < '0' || ch > '9') {if(ch == '-') in = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9') {rt = rt * 10 + ch - '0'; ch = getchar();}
    return rt * in;
}

void seperate(int x)
{
    for(int i = 30; i >= 1; i--)
    {
        if(x >> i == 0) continue;
        if(!p[i])
        {
            p[i] = x;
            return;
        }
        x ^= p[i];
    }
}
void rebuild()
{
    for(int i = 30; i >= 0; i--)
        for(int j = i-1; j >= 0; j--)
            if( (p[i] >> j) & 1)
                p[i] ^= p[j];
    for(int i = 0; i <= 30; i++)
        if(p[i])    p[cnt++] = p[i];
}
int search()
{
    if(n != cnt)    k--;
    int ans = 0;
    if(k >= (1 << cnt)) return -1;
    for(int i = 30; i >= 1; i--)
        if( (k >> i) & 1 )  ans ^= p[i];
    return ans;
}
int main()
{
    n = read(), k = read();
    for(int i = 1; i <= n; i++)
    {
        int x = read();
        seperate(x);
    }
    rebuild();
    printf("%d",search());
    system("pause");
    return 0;
}

例题:暂无。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值