线性基学习

>>>大佬写的贼好建议参考


线性基的基本概念:

引入一下

给出一个数组a[],我们从a中得到一个数组b[],

a数组中的任何数都可以由b中一个或者几个数的异或和得到,b就是我们所说的线性基。

b数组本身具有什么性质?

(1)b数组的数量最多 = 数组a数据最大值的二进制位数

比如说a最大是int最大,那么最大是2^31-1,需要31位来表示,所以此时线性基最多31位

即,最多每个二进制位对应线性基里的一位

(2)我们用b[i]表示二进制第i位的线性基,b[i]具有的性质是第i位一定为1,更高位全为0,低于i位的0或者1随便

比如说b[3] 一定是一个这样的数:0.......01XX(x表示任意),又比如说b[31] = 1XXX...XXX


线性基的构造:

具体来说把a中元素一个个插入线性基数组b

代码挺短的,我们抽象的解释一下过程就是:我们把线性基数组b看做一些个零件,通过异或这个操作,插入a[i]到b数组中,将a[i]中包含b数组的零件卸出来(通过异或),最后如果a[i]没有办法被完全卸完,证明已有零件不能组合成a[i],剩下部分就是需要新加入线性基的一个全新零件。

如何判断没有办法被卸完:从高到低遍历b数组,由于b[i]二进制第i位一定是1(如果有),这样的话我们可以保证a[i]高位的1逐渐消去,一旦出现a[i]某一位的1无法被卸掉(对应b[i]=0,即该位目前不存在线性基,我们可以知道a[i]这一位上的1一定消不掉了)

代码:

void insert(ll x){//x是插入的数,lb[]是线性基数组
    for (int i=62;i>=0;i--){//i表示左移的位数,对应线性基第i+1位
        if ((1LL<<i)&x){
            if (!lb[i+1]){lb[i+1]=x;return ;}
            else x ^= lb[i+1];
        }
        if (!x) return ;
    }
}

线性基的基操

①求n个数异或和的最大值

把n个数插入线性基,然后利用线性基从高位到低位,当前位能取一就取一

ll getmax(){
    ll ans = 0;
    for (int i=63;i>=1;i--){
        ans = max(ans,ans^lb[i]);
    }
    return ans;
}

②判断这n个数能否异或形成x这个数

做法是n个数插入线性基,然后尝试插入x,如果能成功插入,那么就不能形成

bool insert(ll x){
    for (int i=62;i>=0;i--){
        if ((1LL<<i)&x){
            if (!lb[i+1]) return false;
            else x ^= lb[i+1];
        }
        if (!x) return false;
    }
    return true;
}

③求n个数能异或出来的数中第k小的

还不会,找到题验证了再改(咕咕咕)

 


一些例题(easy->hard,题解全放在下一个板块了)

简单的应用

>>>洛谷P3857

>>>洛谷P4570

比较难

>>>洛谷P3292

>>>2019牛客多校第一场H

>>>2019牛客多校第四场B

>>>HDU 6579


例题的题解↓↓↓

.

.

.

.

.

.

.

.

.

.

.

.

.


P3857

构造线性基然后答案就等于2的线性基次方。(每个基选或者不选)

原因是线性基可以表示所有数字,且任意线性基组合的结果都不同,因为最高位1的位置都不同。

#include<bits/stdc++.h>

using namespace std;

#define ll long long
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define for0(i,a,b) for (int i=a;i<b;i++)

const int N = 50+5;

ll lb[N];
int cnt;
char s[N];

void insert(ll x){
    for (int i=50;i>=0;i--){
        if ((1LL<<i)&x){
            if (!lb[i+1]){lb[i+1]=x;cnt++;return ;}
            else x ^= lb[i+1];
        }
        if (!x) return ;
    }
}

int main(){
    int n,m;
    cnt = 0;
    scanf("%d %d",&n,&m);
    for1(i,1,m){
        scanf("%s",s);
        ll num = 0;
        ll now = 1;
        for (int i=n-1;i>=0;i--,now*=2){
            if (s[i]=='O') num += now;
        }
        insert(num);
    }
    printf("%lld\n",(1LL<<cnt)%2008);
    return 0;
}

P4570

将这些玩意儿按照魔力值大到小排序,然后按这个顺序插入线性基即可。

我们需要证明的结论是当前插入线性基这个选择,不会对总答案产生不利影响,也就是当前选择插入这个数获得的贡献必定大于等于选择他时所阻碍的不能插入的这些数的贡献之和,而这些数本来都可以插入

称当前插入的数为a,贡献为100,之后有两个数b,c,贡献为60,70。

我们证明:

如果插入了a,会同时阻挡b和c的插入

那么b和c本身也不能同时插入

开始口胡。

我们直接跳过a所在的基之前的基,因为不管是谁作为a所在的那位基,之前的基的影响都一样:

插入a时:

b = lb(a)^lb(b^a),表示a所在的基,和a之后把b抵消的基的集合,下同

c = lb(a)^lb(c^a)

插入b:

那么 a = lb(b)^lb(a^b),显然之前存在a^b,现在b^a也存在

c = lb(b)^lb(b^c),b^c存在吗?当然,看上面b^a有,c^a也有,那么b^a^c^a  = b^c,

因此先插入b一定无法插入a,c

插入c:

道理同插入b

因此我们也可以得到,a阻挡了>=2个数的情况下,这些数都是会相互阻挡的,因此我们贪心的思路正确

代码反正就很简单了

#include<bits/stdc++.h>

using namespace std;

#define ll long long
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define for0(i,a,b) for (int i=a;i<b;i++)

const int N = 1e3+5;

struct T
{
    ll num;
    int magic;
    bool operator < (const T& a)const{
        return magic > a.magic;
    }
}stone[N];

ll lb[65],ans;

void insert(T x){
    for (int i=62;i>=0;i--){
        if ((1LL<<i)&x.num){
            if (!lb[i+1]){lb[i+1]=x.num;ans+=x.magic;return ;}
            else x.num ^= lb[i+1];
        }
        if (!x.num) return ;
    }
}

int main()
{
    int n;
    scanf("%d",&n);
    for0(i,0,n) scanf("%lld %d",&stone[i].num,&stone[i].magic);
    sort(stone,stone+n);
    ans = 0;
    for0(i,0,n) insert(stone[i]);
    printf("%lld\n",ans);
    return 0;
}

P3292

只提供我会的一种做法

用树剖获取修改的区间(>>>树剖是啥玩楞

用维护区间的数据结构,比如说线段树和ST表,但是线段数适合处理区间有修改的,ST表预处理完之后处理区间询问只需要O(1),比线段树快不少,线段树不开O2优化会T

维护区间什么?

维护区间的线性基,这东西是支持合并的,合并也非常的暴力简单,把其中一个区间的线性基扔进另一个即可。

所以说线段树不行,询问一条链的话,树剖logn×区间询问logn×暴力合并logn,多了个log会让你最后一组数据卡常卡的哭出来

#include<bits/stdc++.h>

using namespace std;

#define ll long long
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define for0(i,a,b) for (int i=a;i<b;i++)
#define pt(x) printf("%s = %d\n",#x,x)

const int N = 2e4+5;

int n;
int top[N],dep[N],fa[N],son[N],size[N],id[N];
ll w[N],a[N];
struct T
{
    T(){memset(lb,0,sizeof lb);}
    ll lb[62];
}st[N][16];
/*build edge*/
struct node
{
    int to,last;
}edge[N<<1];
int head[N],idx;
void add(int u,int v){edge[idx].to = v;edge[idx].last = head[u];head[u] = idx++;}

/*linear base 机器翻译出来的线性基*/
void insert(T &a,ll x){
    for (int i=60;i>=0;i--){
        if ((1LL<<i)&x){
            if (a.lb[i+1]) x ^= a.lb[i+1];
            else {a.lb[i+1] = x;break;}
        }
        if (!x) return ;
    }
}

T merge(T a,T b){
    for (int i=1;i<=61;i++){
        if (b.lb[i]) insert(a,b.lb[i]);
    }
    return a;
}
/*倍增ST*/
void getst(){
    for (int i=1;i<=n;i++) insert(st[i][0],a[i]);
    for (int j=1;(1<<j)<=n;j++)
        for (int i=1;i+(1<<j)-1<=n;i++)
            st[i][j] = merge(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}

T query(int l,int r){
    int k = log(r-l+1.0)/log(2.0);
    return merge(st[l][k],st[r-(1<<k)+1][k]);
}
/*link/cut tree*/
void dfs1(int now,int f,int depth){
    dep[now] = depth;
    fa[now] = f;
    size[now] = 1;
    for (int i=head[now];i!=0;i=edge[i].last){
        int v = edge[i].to;
        if (v==f) continue;
        dfs1(v,now,depth+1);
        size[now] += size[v];
        if (size[v] > size[son[now]]) son[now] = v;
    }
}

int tot;
void dfs2(int now,int topf){
    id[now] = tot++;
    a[id[now]] = w[now];
    top[now] = topf;
    if (!son[now]) return ;
    dfs2(son[now],topf);
    for (int i=head[now];i!=0;i=edge[i].last){
        int v = edge[i].to;
        if (v==fa[now] || v==son[now]) continue;
        dfs2(v,v);
    }
}
/*
void dfs(int now,int fa){
    printf("now=%d\n",now);
    for (int i=head[now];i!=0;i=edge[i].last){
        int v = edge[i].to;
        if (v!=fa)dfs(v,now);
    }
}
*/
T interval(int x,int y){
    T ans;
    while (top[x]!=top[y]){
        if (dep[top[x]]<dep[top[y]]) swap(x,y);
        //处理区间[id[top[x]],id[x]]
        ans = merge(query(id[top[x]],id[x]),ans);
        x = fa[top[x]];
    }
    if (dep[x]<dep[y]) swap(x,y);
    //处理区间[id[y],id[x]]
    ans = merge(query(id[y],id[x]),ans);
    return ans;
}
/*solve*/
ll solve(T ans){
    ll maxx = 0;
    for (int i=61;i>=1;i--)
        maxx = max(maxx,maxx^ans.lb[i]);
    return maxx;
}

int main()
{
    //freopen("C:/Users/DELL/Desktop/input.txt", "r", stdin);
    //freopen("C:/Users/DELL/Desktop/myout.txt", "w", stdout);
    int q;
    scanf("%d %d",&n,&q);
    for1(i,1,n) scanf("%lld",w+i);
    idx = 1;
    int u,v;
    for0(i,1,n) scanf("%d %d",&u,&v),add(u,v),add(v,u);

    //dfs(1,0);

    dfs1(1,0,1);
    tot = 1;
    dfs2(1,1);

    getst();

    int x,y;

    while (q--){
        scanf("%d %d",&x,&y);
        T ans = interval(x,y);
        printf("%lld\n",solve(ans));
    }

    return 0;
}

2019牛客多校第一场H

计算异或和为0的集合的元素总个数

全部n个数全部插入,获得线性基内有r个位。

我们把求解过程转为求每个数能出现在多少个异或和为零的不同集合中

那么线性基外n-r个数,每个数可以出现在2^(n-r-1)个集合中,每个数贡献就是2^(n-r-1)

由P4570可以得知,线性基内的数可以被替换,但个数永远是r个。

我们就相当于选取了r个特定的数构成线性基,另外n-r个的贡献总和就是(n-r)*2^(n-r-1)

剩余还需要计算什么?

线性基内元素的贡献:

if这个基对应原数可以被替代,贡献也为2^(n-r-1)

如果不可替代,意味着这一位上的基无法被其他异或为0,那么这个基代表的原数贡献为0

如果判断基内某个数能否被替代?

剩余n-1个数构成的线性基能否将当前枚举的这个数分解掉

 

我们把n个数插入线性基b,没成功插入的就插入c,

因此c就是n-r个数的线性基,由于线性基可以合并,那么等会求n-1个数的线性基是,可以直接在这个c的基础上插入另外r-1个数即可。

#include<bits/stdc++.h>

using namespace std;

#define ll long long
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define for0(i,a,b) for (int i=a;i<b;i++)
#define mod 1000000007

const int N = 2e6+5;

struct T//线性基
{
    ll lb[65];
    int r;//已插入的个数
    void init(){r = 0;memset(lb,0,sizeof lb);}
}b,c,d;

ll a[N];
int id[65];

ll fast_power(ll a, ll b){//快速幂
    ll ans = 1;
    while (b){
        if (b&1) ans = ans*a%mod;
        b>>=1,a = a*a%mod;
    }
    return ans;
}

bool insert(T& t,int idx,bool changeid){//changeid = true表示需要记录当前插入的这个数原数的下标
    ll x = a[idx];
    for (int i=62;i>=0;i--){
        if ((1LL<<i)&x){
            if (t.lb[i+1]) x ^= t.lb[i+1];
            else {
                t.lb[i+1] = x;
                t.r++;
                if (changeid)id[t.r] = idx;
                return true;
            }
        }
        if (!x) return false;
    }
}

int main()
{
    int n;
    while (~scanf("%d",&n)){
        b.init();
        c.init();
        for0(i,0,n){
            scanf("%lld",a+i);
            if (!insert(b,i,true)) insert(c,i,false);
        }
        ll out = n-b.r;
        ll dis = fast_power(2,out-1);
        ll ans = out*dis%mod;

        for (int i=1;i<=b.r;i++){
            d = c;
            for (int j=1;j<=b.r;j++){
                if (j==i) continue;
                insert(d,id[j],false);
            }
            if (!insert(d,id[i],false)) ans = (ans+dis)%mod;
        }

        printf("%lld\n",ans);
    }
    return 0;
}

2019牛客多校第四场B

询问区间内每个下标对应的集合对应的线性基能否异或形成x

这题考察的是线性基的求交集。

也就是说,线性基还支持求交集。

什么叫求交集?

就是求出来的这玩意能表示所有两个交集都能表示的数,且不能表示只有其中一个基能表示的任何数

宏观上来说,两个线性基a,b求交集就是把b的所有基插入a,把b中所有能在a表示出来的基以及b中几个基异或和能在a中表示的这些玩意扔进我们的答案线性基即可。

具体来讲,我们需要两个辅助线性基c,d,用于辅助b的基试插进a这个过程

c储存a的所有基以及b中未能分解完的基,初始化为与a相同,所以我们实际把b的元素插入c

d储存如果被c对应该位基分解时,可以获得的贡献值,贡献值的异或和就是等会儿插入答案的,初始化也和c相同

b的一个基插入c有两种情况:

1.能被原本a中的元素完全分解,那么我们得到的贡献也就是原本a基对应位上的异或和,也就等于当前b插入c的这一位基

2.(这个是难点)不能被完全分解,意味着c中没有这个基

那么我们知道已经有一部分已经与a的部分相同,而剩余部分是有可能与下一次尝试插入的b的某位基在该位抵消导致之后能被完全分解。

因此我们将剩余未分解部分作为c线性基内该位的基,把累积的贡献作为线性基d这一位的贡献。

懂了上面两种情况,那么我们很显然要把b线性基从低位到高位插进去

复杂度32*32

接下去就是用线段树维护区间的线性基交集。

处理询问时注意不用搞出区间的线性基交集,再用x试插,这样复杂度是logn*32*32

我们只需找到所有需要的区间段,这段区间全部有效我们直接判断x是否能够被分解,这样只需合并true/false即可,不需要线性基交集的合并,复杂度是logn*32

不过由于线段树build复杂度是n*logn*32*32

整体复杂度就是nlogn32*32,所以可以说询问其实被卡常了?

不管怎样,学到了,能维护简单的东西得出结论就不要维护奇怪的东西。

/*
构造:n*32*32
询问:n*logn(≈16)*32 可过,n*32*32过不了
*/
#include<bits/stdc++.h>

using namespace std;

#define ll long long
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define for0(i,a,b) for (int i=a;i<b;i++)
#define u32 unsigned int//%u
#define mid int m = l+r>>1
#define lson rt<<1,l,m
#define rson rt<<1|1,m+1,r
#define tl tree[rt<<1]
#define tr tree[rt<<1|1]

const int N = 5e4+5;

struct T
{
    u32 lb[35];
    bool empty;
    void init(){memset(lb,0,sizeof lb);empty = true;}
    bool insert(u32 x){
        for (int i=31;i>=0;i--){
            if ((1LL<<i)&x){
                if (lb[i+1]) x ^= lb[i+1];
                else {lb[i+1] = x;return true;}
            }
            if (!x) return false;
        }
    }
    bool check(u32 x){
        for (int i=31;i>=0;i--){
            if ((1LL<<i)&x){
                if (lb[i+1]) x ^= lb[i+1];
                else return false;
            }
        }
        return true;
    }
}tree[N<<2];

T intersect(T a,T b){//b插a
    T c = a,d = a,ans;
    ans.init();
    for (int i=1;i<=32;i++){
        u32 x = b.lb[i],same = 0;
        if (!x) continue;
        for (int j=i-1;j>=0;j--){
            if ((1LL<<j)&x){
                if (c.lb[j+1]){
                    x ^= c.lb[j+1];
                    same ^= d.lb[j+1];//累积贡献
                }
                else {
                    c.lb[j+1] = x;
                    d.lb[j+1] = same;//如果这个数不能被分解,就把贡献留在这一位上等待能够抵消的数来拿走这一部分贡献
                    break;
                }
            }
            if (!x){
                ans.lb[i] = same;
                break;
            }
        }
    }
    return ans;
}

void push_up(int rt){
    tree[rt] = intersect(tl,tr);
}

void build(int rt,int l,int r){
    if (l==r){
        int sz;
        u32 x;
        scanf("%d",&sz);
        while (sz--) scanf("%u",&x),tree[rt].insert(x);
        return ;
    }
    mid;
    build(lson);
    build(rson);
    push_up(rt);
}
/*
T query(int L,int R,int rt,int l,int r){
    if (L<=l && r<=R){
        return tree[rt];
    }
    mid;
    T ans;
    ans.init();
    if (L<=m) ans = query(L,R,lson);
    if (R>m)  ans = ans.empty? query(L,R,rson):intersect(query(L,R,rson),ans);
    return ans;
}
*/
bool query(int L,int R,u32 x,int rt,int l,int r){
    if (L<=l && r<=R){
        return tree[rt].check(x);
    }
    mid;
    bool ok = true;
    if (L<=m) ok = ok && query(L,R,x,lson);
    if (R>m)  ok = ok && query(L,R,x,rson);
    return ok;
}

int main(){
    int n,m;
    scanf("%d %d",&n,&m);
    build(1,1,n);
    int l,r;
    u32 x;
    while (m--){
        scanf("%d %d %u",&l,&r,&x);
        printf("%s\n",query(l,r,x,1,1,n)? "YES":"NO");
        //printf("%s\n",query(l,r,1,1,n).check(x) ? "YES":"NO");
    }
    return 0;
}

HDU6579

暴力构造+区间合并:nlogn*32*32

区间查询:线段树q*(logn*32*32+32),ST表q*(32*32+32)(超空间)

这样不行,时间炸,ST表顺便炸空间

所以我们用到n*32*32的贪心的方法构造以及32直接解决单次询问

我们维护前缀线性基lb[][],lb[x][ ]维护第一到第x个数的线性基

显然这只能解决【1,r】的询问

不能解决【l,r】的询问

因此我们维护的前缀线性基还需要多维护一个东西pos[][]

我们利用lb[x-1][]插入第x个数获得lb[x][]的同时,我们从高到低动态更新 线性基每一位元素 为最新的来源,

这样我们保证了前缀线性基内高位,也就是贡献更大的元素来源更加靠【l,r】的右边。

于是对于询问【l,r】,我们调用lb[r][]对于每一位pos[r][i]>=l都是有效的。

emm,就这样,如果最新的都比l小,那么这一位上的基是没有的。

也不存在我们由于将高位更新为最新,导致之后更低的几位此时都无法取导致答案偏小,因为更新更高位元素一定比更新更小的好。

贪就完事了。

#include<bits/stdc++.h>

using namespace std;

#define ll long long
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define for0(i,a,b) for (int i=a;i<b;i++)

const int N = 5e5+5;

int lb[N][35];
int pos[N][35];//记录基上这个数字原本的下标
int a[N];

void insert(int p,int x){
    int now = p;
    for (int i=30;i>=0;i--){
        if ((1<<i)&x){
            if (!lb[now][i+1]){
                lb[now][i+1] = x;
                pos[now][i+1] = p;
                return ;
            }
            else {
                if (p>pos[now][i+1]){
                    swap(x,lb[now][i+1]);
                    swap(p,pos[now][i+1]);
                }
                x ^= lb[now][i+1];
            }
        }
        if (!x) return ;
    }
}

int query(int l,int r){
    int ans = 0;
    for (int i=31;i>=1;i--){
        //printf("pos[%d]=%d,lb[%d]=%d\n",i,pos[i],i,lb[i]);
        if (pos[r][i]>=l) ans = max(ans,ans^lb[r][i]);
    }
    return ans;
}

int main()
{
    //freopen("Users/DELL/Desktop/input.txt", "r", stdin);
    //freopen("Users/DELL/Desktop/output.txt", "w", stdout);
    int T;
    scanf("%d",&T);
    while (T--){
        int n,m;
        scanf("%d %d",&n,&m);
        for1(i,1,n) {
            scanf("%d",&a[i]);
            memcpy(lb[i],lb[i-1],sizeof lb[i-1]);
            memcpy(pos[i],pos[i-1],sizeof pos[i-1]);
            insert(i,a[i]);
        }
        int op,l,r,x;
        int lastans = 0;
        while (m--){
            scanf("%d",&op);
            if (op==0){
                scanf("%d %d",&l,&r);
                l = (l^lastans)%n + 1;
                r = (r^lastans)%n + 1;
                if (l>r) swap(l,r);
                //printf("[%d,%d]\n",l,r);
                lastans = query(l,r);
                printf("%d\n",lastans);
            }
            else {
                scanf("%d",&a[++n]);
                a[n] ^= lastans;
                memcpy(lb[n],lb[n-1],sizeof lb[n-1]);
                memcpy(pos[n],pos[n-1],sizeof pos[n-1]);
                insert(n,a[n]);
            }
        }
    }
    return 0;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值