可持久化trie树

数据结构 可持久化trie树

前言

省选D1T1考察的算法。时至今日才总算残喘\(ac\)..

想当初我打了个\(dp\)自以为是正解 (/▽\)

前置知识

你需要的掌握的算法:

可持久化线段树

倘若你没学过,在百度本小破站学习一波即可

可持久化trie树

事实上,trie树本身就有主席树的一丝神韵。主席树通过重复利用之前的节点,达到节省空间的目的。trie树同样也在原来的树的基础上增增添添,空间复杂度同样优秀。

可持久化trie树,个人感觉和可持久化线段树又是有很大的共同点。回想主席树是在增加一个数据之后增添一条链,构成一个类似前缀和的树;可持久化trie树的精神与其完全一致:对于一个新的字符串,在继承原trie树的基础上,新建一个根节点,然后修修补补,传传承承,大功告成( ̄▽ ̄)*

这里不再赘述,把代码记录一下:

建立新根节点:

void create(int k,int num){
    root[k+1]=++tot;
    int u=root[k],v=root[k+1];
    for(int i=31;i>=0;--i){
        ch[v][0]=ch[u][0];
        ch[v][1]=ch[u][1];
        bool tar=(1<<i)&num;
        ch[v][tar]=++tot;
        u=ch[u][tar]; v=ch[v][tar];
        cnt[v]=cnt[u]+1;
    }
}

01trie树

想起trie树,自然会联想到一个毫无相关的运算:按位异或 (\(xor\))

当我们把一个trie树建成01trie树之后,再将数字拆成二进制,像字符串一样插入。这样,我们就能快速完成有关异或的操作。

例如需要求最大异或操作:我们只需从根节点顺序往下遍历,倘若从当前节点有一条记录着与原数上二进制位不同的边,那贪心地思考,必须走着一条。因为就算后面位数与原数二进制位全部相同,其和也不及这条边带来的收益大。

例题

最大异或和

题目传送门

题目简述:

给出一个数列,需要支持一下操作

  1. 支持在原数列末尾增添一个数字,使得数列大小\(+1\)

  2. 支持在选择给定范围内的一个位置\(p\),并给定\(x\),使得\(a[p]\oplus a[p+1]\oplus...\oplus a[n]\oplus x\)取得最大值

假设\(s[i]\)表示前缀异或和,那么原式就可以表示为 \(s[p-1]\oplus s[n] \oplus x\)最大,而后两项均一致,我们本质上要求的就是区间内异或值最大

联系上静态区间第\(k\)小的做法,直接在两棵01trie树上通过减去前缀获得目标区间的信息,然后求异或的最大值即可

// luogu-judger-enable-o2
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

int add;

struct trie{
    static const int MAX=4e7+5;
    int tot,root[MAX],ch[MAX][2],cnt[MAX];

    void init(){
        tot=1; root[0]=tot;
        tot=2; root[1]=tot;
        int u=root[1];
        for(int i=31;i>=0;--i){
            ch[u][0]=++tot;
            u=ch[u][0];
            cnt[u]++;
        }
    }

    void create(int k,int num){
        root[k+1]=++tot;
        int u=root[k],v=root[k+1];
        for(int i=31;i>=0;--i){
            ch[v][0]=ch[u][0];
            ch[v][1]=ch[u][1];
            bool tar=(1<<i)&num;
            ch[v][tar]=++tot;
            u=ch[u][tar]; v=ch[v][tar];
            cnt[v]=cnt[u]+1;
        }
    }

    bool exist(int p,int k){
        int u=root[p+1];
        for(int i=31;i>=0;--i){
            bool tar=(1<<i)&k;
            if(!ch[u][tar]) return false;
            u=ch[u][tar];
        }
        return true;
    }

    int query(int l,int r,int x){
        return xormax(l-1,r,x);
    }

    int xormax(int a,int b,int k){
        int u=root[a],v=root[b];
        int ans=0;
        for(int i=31;i>=0;--i){
            bool tar=(1<<i)&k;
            if(cnt[ch[u][tar^1]]!=cnt[ch[v][tar^1]]){
                ans+=(1<<i);
                u=ch[u][tar^1]; v=ch[v][tar^1];
            }
            else{
                u=ch[u][tar]; v=ch[v][tar];
            }
        }
        return ans;
    }
}trie;

int n,m;

inline int read();
inline char getc();

int main(){
    #ifndef ONLINE_JUDGE
    freopen("test.in","r",stdin);
    #endif

    n=read(); m=read();
    trie.init();
    for(int i=1;i<=n;++i){
    	add=add^read();
    	trie.create(i,add);
    }

    for(int i=1;i<=m;++i){
    	char type=getc(); int l,r,x;
    	switch(type){
    		case 'A':
    		x=read(); add=add^x; n++;
    		trie.create(n,add);
    		break;
    		case 'Q':
    		l=read(); r=read(); x=read();
    		printf("%d\n",trie.query(l,r,x^add));
    		break;
    	}
    }
        
    return 0;
}

inline int read(){
    char tmp=getchar(); int sum=0; bool flag=false;
    while(tmp<'0'||tmp>'9'){
        if(tmp=='-') flag=true;
        tmp=getchar();
    }
    while(tmp>='0'&&tmp<='9'){
        sum=(sum<<1)+(sum<<3)+tmp-'0';
        tmp=getchar();
    }
    return flag?-sum:sum;
}

inline char getc(){
    char tmp=getchar();
    while(tmp!='A'&&tmp!='Q') tmp=getchar();
    return tmp;
}

P5283 [十二省联考2019]异或粽子

题目传送门

省选的第一题,考察了可持久化trie树。

同样是前缀异或和的思想,问题可以转化为求前\(k\)大的\(s[i]\oplus s[j](1\leq l\leq r\leq n)\)

现在我们一脸懵逼。嗯?最大值?没有左边界,也没有右边界怎么判断?

倘若暴力枚举左右边界,显然t到飞起,那么。。

只专注于左边界(右边界同样道理也可行),设为\(l\),那么右边界\(r\)必定出现于\([l+1,n]\)。这一点我们可以用可持久化trie树快速处理出静态区间第k小。

先为每一个左边界处理出其最大的右边界,将答案塞入优先队列中。

当弹出一个答案时,说明该左边界对应的最大右边界已经作为答案贡献过了,那么接下来最可能为答案做贡献的便是第二大的右边界。再次利用到可持久化trie树快速解决

这样,不断弹出答案的同时更改答案,重复\(k\)次即为答案。

双倍经验~~ [NOI2010]超级钢琴

这道题和省选题极其相似。不同的是省选题多了个trie树作为难点,本题则是在区间上作出了略微限制。略微思考,略微思考,略微修改,即可略微\(wa\)= ̄ω ̄=

省选题的代码:

#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define int long long

const int MAX=5e5+5;

int n,k,l,r,ans;
int a[MAX],s[MAX];

int tot,root[MAX];

struct datan{
    int num,id;
}num[MAX];
int rk[MAX],sa[MAX];

struct tree{
    int l,r,val;
}node[40000005];

struct data{
    int u,limx,limy,lim,k,delta;

    bool operator < (const data &a) const{
        return delta<a.delta;
    }
};
priority_queue <data> line;

inline int read();

bool cmpn(datan a,datan b) {return a.num<b.num;}
bool cmpi(datan a,datan b) {return a.id<b.id;}

void build(int,int,int);
void updata(int,int,int,int,int);
int qsum(int,int,int,int,int);
int kth(int,int,int);
int query(int,int,int,int,int);

main(){
    #ifndef ONLINE_JUDGE
    freopen("test.in","r",stdin);
    #endif

    cin>>n>>k>>l>>r;
    for(register int i=1;i<=n;++i) a[i]=read(),s[i]=s[i-1]+a[i];
    for(register int i=1;i<=n;++i) num[i].num=s[i],num[i].id=i;
    sort(num+1,num+1+n,cmpn);
    for(register int i=1;i<=n;++i) sa[i]=num[i].id;
    for(register int i=1;i<=n;++i) rk[sa[i]]=i;

    root[0]=++tot; build(root[0],1,MAX);
    for(register int i=1;i<=n;++i) root[i]=++tot,updata(root[i-1],root[i],1,MAX,rk[i]);

    for(register int i=1;i<=n-l+1;++i){
        data tmp;
        tmp.u=i; tmp.limx=i+l-1; tmp.limy=min(i+r-1,n);
        tmp.lim=tmp.limy-tmp.limx+1; tmp.k=1;
        tmp.delta=kth(tmp.limx,tmp.limy,1)-s[tmp.u-1];
        line.push(tmp);
    }

    while(k--){
        data tmp=line.top(); line.pop();
        ans+=tmp.delta;
        if(tmp.k==tmp.lim) continue;
        tmp.k++; tmp.delta=kth(tmp.limx,tmp.limy,tmp.k)-s[tmp.u-1];
        line.push(tmp);
    }

    cout<<ans<<endl;

    return 0;
}

inline int read(){
    char tmp=getchar(); int sum=0; bool flag=false;
    while(tmp<'0'||tmp>'9'){
        if(tmp=='-') flag=true;
        tmp=getchar();
    }
    while(tmp>='0'&&tmp<='9'){
        sum=(sum<<1)+(sum<<3)+tmp-'0';
        tmp=getchar();
    }
    return flag?-sum:sum;
}

void build(int p,int l,int r){
    if(l==r) return;
    node[p].l=++tot; node[p].r=++tot;
    int mid=(l+r)>>1;
    build(node[p].l,l,mid); build(node[p].r,mid+1,r);
}

void updata(int pre,int cur,int l,int r,int k){
    node[cur].l=node[pre].l;
    node[cur].r=node[pre].r;
    node[cur].val=node[pre].val+1;
    if(l==r) return;
    int mid=(l+r)>>1;
    if(k<=mid) node[cur].l=++tot,updata(node[pre].l,node[cur].l,l,mid,k);
    if(mid+1<=k) node[cur].r=++tot,updata(node[pre].r,node[cur].r,mid+1,r,k);
}

int qsum(int p,int l,int r,int L,int R){
    if(L<=l&&r<=R) return node[p].val;
    int sum=0,mid=(l+r)>>1;
    if(L<=mid) sum+=qsum(node[p].l,l,mid,L,R);
    if(mid+1<=R) sum+=qsum(node[p].r,mid+1,r,L,R);
    return sum;
}

int kth(int a,int b,int c){
    return query(root[a-1],root[b],1,MAX,b-a-c+2);
}

int query(int a,int b,int l,int r,int k){
    if(l==r) return s[sa[l]];
    int mid=(l+r)>>1;
    int x=node[node[b].l].val-node[node[a].l].val;
    if(x>=k) return query(node[a].l,node[b].l,l,mid,k);
    else return query(node[a].r,node[b].r,mid+1,r,k-x);
}
posted @ 2020-06-29 21:55  ticmis  阅读(732)  评论(0)    收藏  举报