• 学习笔记:主席树(可持久化线段树)


    要不是学这个我才不学什么权值线段树呢。

    主席树

    很高大上?
    其实就是可持久化的数据结构
    在学习权值线段树时,我们可能会想,如果求任意区间第(k)小(大)咋办呢?
    题目链接:P3834 【模板】可持久化线段树 1(主席树)
    就是他了!
    乍一想与可持久化没啥关系,但是你先听我说。
    我们考虑一颗维护([0,l-1])区间的权值线段树(T_1),和维护([0,r])区间的权值线段树(T_2),那么定义:

    [T_1-T_2 ]

    为每个点权值之差,得到的新权值线段树(T),就维护了([l,r])的权值,依上题跑可以解决。
    这样的时间复杂度为(O(mnlog n)),更恐怖的是空间相当于(2m)棵线段树,还是算了吧。
    (>)(ps:)是不是从(0)开始的区间比较别扭?
    (>)(ps:)由于要应对从一开始的询问,为避免特判,建一棵权值全为(0)的树。
    于是我们想到预处理每个([0,r])的线段树,可是仍然吃不消,那我盗几张图吧:
    先抛出一个数列一个数列(4,1,1,2,8,9,4,4,3),去重后得到:
    (1,2,3,4,8,9)
    ([0,9])权值线段树:

    ([0,8])权值线段树:

    ([0,7])权值线段树:

    再手玩一下(当时我只学到了这里,下面全都是自己的见解了),发现每变一次只有一条链变了!
    所以我们构造这样的一棵根树(重要的是,更新多少次就有多少根):

    时间和空间都降为了优秀的(O(nlog n))(bushi)
    下面就是代码了:

    大常数主席树:

    (Part;1).离散化:

    这里权值线段树锅了,差点让我自闭......
    注意(c)设为(1),上篇博客已改,要不大数据会(WA)

    struct Node
    {
    	int id,val; 	
    }t[MAXN];
    int b[MAXN],num[MAXN],cntt[MAXN],c=1;
    bool cmp(Node n,Node m){return n.val<m.val;} 
    void hash(int n)
    {
        sort(t+1,t+n+1,cmp);
        for(int i=1;i<=n;i++)
        {
            if(num[c]!=t[i].val) b[t[i].id]=++c,num[c]=t[i].val;
            else b[t[i].id]=c;
        }
    }
    

    注意不要统计(cntt),因为下面从([0,0])开始建树,要不断统计出现次数。

    (Part;2).骨架树

    这名是我自己给他起的(qwq)
    就是权值都为(0)的树。
    开始我这样理解主席树,就是将一条链拽下来,然后他所连的边也被拽下来了(qwq)
    注意主席树点号较复杂,不能用(×2)的方法得到了,要记录下来。
    这样写:

    //骨架树qwq
    struct node
    {
    	int l,r,ls,rs,sum;
    	node()
    	{
    		l=r=ls=rs=sum=0;	
    	}	
    }a[MAXN<<5];
    int cnt=0,root[MAXN];
    void update(int k){a[k].sum=a[a[k].ls].sum+a[a[k].rs].sum;}
    int build_bone(int l,int r)
    {
    	cnt++;//点的编号
    	int op=cnt;//由于cnt是动态变化的,我们要把他存起来
    	a[op].l=l,a[op].r=r;//表示的左右端点
    	int mid=(l+r)>>1;
    	if(l==r)
    	{
    		a[op].sum=0;
    		return op;//叶节点的左右儿子都是0 
    	}
    	a[op].ls=build_bone(l,mid),a[op].rs=build_bone(mid+1,r);
    	update(op);
    	return op;
    }
    

    (Part;3).可持久化

    俩个版本一起跑即可,注意判断变的点在左还是在右。

    int build_chain(int k,int cur,int x)//对k点可持久化成x 
    {
    	cnt++;//点的编号
    	int op=cnt;//由于cnt是动态变化的,我们要把他存起来
    	a[op].l=a[cur].l,a[op].r=a[cur].r;//端点
    	int mid=(a[cur].l+a[cur].r)>>1;
    	if(a[cur].l==a[cur].r)
    	{
    		a[op].sum=x;
    		return op;
    	}
        //目标点是左儿子的,那么他和上一版本的左儿子依然不同,右儿子一样
    	if(k<=mid) a[op].ls=build_chain(k,a[cur].ls,x),a[op].rs=a[cur].rs;
    	////目标点是优儿子的,那么他和上一版本的右儿子依然不同,左儿子一样
        else if(k>mid) a[op].rs=build_chain(k,a[cur].rs,x),a[op].ls=a[cur].ls;
    	update(op);//记得更新
    	return op;
    }
    

    (Part;4).回答询问

    每次做差即可,是(O(1))的,剩下的等同于权值线段树。

    //查询第x小值 
    int query(int k1,int k2,int x)
    {
    	if(a[k1].l==a[k1].r) return num[a[k1].l];
    	int mid=a[a[k1].ls].sum-a[a[k2].ls].sum;
    	if(x<=mid) return query(a[k1].ls,a[k2].ls,x);
    	else if(x>mid) return query(a[k1].rs,a[k2].rs,x-mid);
    }
    

    (Part;5).处理根

    只需枚举右端点,处理出每个根,询问时(O(1))查询根,然后向下跑就行了。
    大概是这样的:

        root[0]=build_bone(1,c); //零号根即骨架树的根(是1)。
    	for(int i=1;i<=n;i++) cntt[b[i]]++,root[i]=build_chain(b[i],root[i-1],cntt[b[i]]);
    	for(int i=1;i<=m;i++)
    	{
    		l=read(),r=read(),k=read();
    		printf("%d
    ",query(root[r],root[l-1],k));	
    	} 
    

    总的说,时间复杂度为(O((n+m)log n)),空间复杂度是(O(mlog n+n)),可以通过本题(然而他还是大常数主席树)。
    下面放下(AC)代码:

    (Code):

    #include<iostream>
    #include<cstdio>
    #include<cmath>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    const int MAXN=2e5+5;
    struct Node
    {
    	int id,val; 	
    }t[MAXN];
    int b[MAXN],num[MAXN],cntt[MAXN],c=1;
    bool cmp(Node n,Node m)
    {
        return n.val<m.val;
    } 
    void hash(int n)
    {
        sort(t+1,t+n+1,cmp);
        for(int i=1;i<=n;i++)
        {
            if(num[c]!=t[i].val) b[t[i].id]=++c,num[c]=t[i].val;
            else b[t[i].id]=c;
        }
    }
    //骨架树qwq
    struct node
    {
    	int l,r,ls,rs,sum;
    	node()
    	{
    		l=r=ls=rs=sum=0;	
    	}	
    }a[MAXN<<5];
    int cnt=0,root[MAXN];
    void update(int k){a[k].sum=a[a[k].ls].sum+a[a[k].rs].sum;}
    int build_bone(int l,int r)
    {
    	cnt++;
    	int op=cnt;
    	a[op].l=l,a[op].r=r;
    	int mid=(l+r)>>1;
    	if(l==r)
    	{
    		a[op].sum=0;
    		return op;//根的左右儿子都是0 
    	}
    	a[op].ls=build_bone(l,mid),a[op].rs=build_bone(mid+1,r);
    	update(op);
    	return op;
    }
    //可持久化  
    int build_chain(int k,int cur,int x)//对k点可持久化成x 
    {
    	cnt++;
    	int op=cnt;
    	a[op].l=a[cur].l,a[op].r=a[cur].r;
    	int mid=(a[cur].l+a[cur].r)>>1;
    	if(a[cur].l==a[cur].r)
    	{
    		a[op].sum=x;
    		return op;
    	}
    	if(k<=mid) a[op].ls=build_chain(k,a[cur].ls,x),a[op].rs=a[cur].rs;
    	else if(k>mid) a[op].rs=build_chain(k,a[cur].rs,x),a[op].ls=a[cur].ls;
    	update(op);
    	return op;
    }
    //查询第x小值 
    int query(int k1,int k2,int x)
    {
    	if(a[k1].l==a[k1].r) return num[a[k1].l];
    	int mid=a[a[k1].ls].sum-a[a[k2].ls].sum;
    	if(x<=mid) return query(a[k1].ls,a[k2].ls,x);
    	else if(x>mid) return query(a[k1].rs,a[k2].rs,x-mid);
    }
    int n,m,k,l,r;
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++) scanf("%d",&t[i].val),t[i].id=i;
    	hash(n);
    	root[0]=build_bone(1,c); 
    	for(int i=1;i<=n;i++) cntt[b[i]]++,root[i]=build_chain(b[i],root[i-1],cntt[b[i]]);
    	for(int i=1;i<=m;i++)
    	{
    		scanf("%d%d%d",&l,&r,&k);
    		printf("%d
    ",query(root[r],root[l-1],k));	
    	} 
    	return 0;
    } 
    

    这是主席树经典的静态区间第(k)小值问题,当然还有一个板子,我(A)了会再写写的(背过板子就好了)。


    (upd;at;2020.3.23):终于把那个板子过了
    P3919 【模板】可持久化数组(可持久化线段树/平衡树)
    除了炸一次空间,就一次过了
    发现好水啊,连区间和都不用维护,还是老套路:

    struct node
    {
    	int l,r,ls,rs,val;
    	node(){l=r=ls=rs=val=0;}
    }a[24000005];
    int cnt=0;
    int root[1000005];
    int build_bone(int l,int r)
    {
    	int k=++cnt;
    	a[k].l=l,a[k].r=r;
    	if(l==r){a[k].val=t[l];return k;}
    	int mid=(l+r)>>1;
    	a[k].ls=build_bone(l,mid),a[k].rs=build_bone(mid+1,r);
    	return k;
    }
    

    这里的骨架树就是第(0)个版本了呀,直接建树就好了。

    int build_chain(int s,int x,int y)//复制s节点,并将x位的值改为y 
    {
    	int k=++cnt;
    	a[k].l=a[s].l,a[k].r=a[s].r;
    	if(a[k].l==a[k].r){a[k].val=y;return k;}
    	int mid=(a[s].l+a[s].r)>>1;
    	if(x<=mid) a[k].rs=a[s].rs,a[k].ls=build_chain(a[s].ls,x,y);
    	else a[k].ls=a[s].ls,a[k].rs=build_chain(a[s].rs,x,y);
    	return k; 
    }
    

    生成一条链,将某个位置(x)修改为(y)
    然后就可以查询某个位置的数了:

    int query(int x,int y)//查询x位上的值,到了y这个节点
    {
    	int mid=(a[y].l+a[y].r)>>1;
    	if(a[y].l==a[y].r) return a[y].val;
    	if(x<=mid) return query(x,a[y].ls);
    	else return query(x,a[y].rs);
    } 
    

    我们分析一下两个操作:
    对于操作(1),显然就是复制一条链,改一下最后的数就好了,记得把根记录下来。
    对于操作(2),我们查询完之后大可以不必完全复制,只需要把此版本的根赋值为原版的根就好了。
    下面是完整的代码:

    (Code):

    #include<iostream>
    #include<cstdio>
    #include<cmath>
    #include<cstring>
    using namespace std;
    int n,m,v,flag,l,r,t[1000005];
    inline int read()
    {
        int x=0,w=1;
        char c=getchar();
        while(c>'9'||c<'0')
        {
            if(c=='-') w=-1;
            c=getchar();
        }
        while(c<='9'&&c>='0')
        {
            x=(x<<1)+(x<<3)+(c^'0');
            c=getchar();
        }
        return x*w;
    }
    struct node
    {
        int l,r,ls,rs,val;
        node(){l=r=ls=rs=val=0;}
    }a[24000005];
    int cnt=0;
    int root[1000005];
    int build_bone(int l,int r)
    {
        int k=++cnt;
        a[k].l=l,a[k].r=r;
        if(l==r){a[k].val=t[l];return k;}
        int mid=(l+r)>>1;
        a[k].ls=build_bone(l,mid),a[k].rs=build_bone(mid+1,r);
        return k;
    }
    int build_chain(int s,int x,int y)//复制s节点,并将x位的值改为y 
    {
        int k=++cnt;
        a[k].l=a[s].l,a[k].r=a[s].r;
        if(a[k].l==a[k].r){a[k].val=y;return k;}
        int mid=(a[s].l+a[s].r)>>1;
        if(x<=mid) a[k].rs=a[s].rs,a[k].ls=build_chain(a[s].ls,x,y);
        else a[k].ls=a[s].ls,a[k].rs=build_chain(a[s].rs,x,y);
        return k; 
    }
    int query(int x,int y)//查询x位上的值,到了y这个节点
    {
        int mid=(a[y].l+a[y].r)>>1;
        if(a[y].l==a[y].r) return a[y].val;
        if(x<=mid) return query(x,a[y].ls);
        else return query(x,a[y].rs);
    } 
    int main()
    {
        n=read(),m=read();
        for(int i=1;i<=n;i++) t[i]=read();
        root[0]=build_bone(1,n);
        for(int i=1;i<=m;i++)
        {
            v=read(),flag=read();
            if(flag==1) l=read(),r=read(),root[i]=build_chain(root[v],l,r);
            else{l=read();root[i]=root[v];printf("%d
    ",query(l,root[v]));} 
        }
        return 0;
    }//不加快读会T一个点 
    

    关于空间:主席树的空间开到(O(mlog n+4n))就没问题了,完全用不了。
    果然是大常数主席树,跑的真**慢,自己(yy)的算法果然与正解有差距,不过应付一般的主席树问题应该还是(ok)的,毕竟(O((n+m)log n))的题出到(10^6)是真的毒瘤。


    令人谔谔的是,这篇文章还没有完结:
    (upd;at;2020.3.25):恭喜我又找到一个板子题,还是个紫的!
    挂上链接:SP3946 MKTHNUM - K-th Number
    区间第(k)大,直接主席树搞就行了,实测能过。
    代码一样,不占空间了。


    再更新一发:

    在大佬万万没想到的帮助下,有了可以减小常数的做法。
    我们以第二个题为例,发现可以不用维护每个点的区间两端点,在递归时直接计算即可。
    这样不但减小了空间消耗,还能减小过多调用带来的时间浪费。
    实测(5.64s->3.90s),还是很优秀的。

    (Code):

    #include<iostream>
    #include<cstdio>
    #include<cmath>
    #include<cstring>
    using namespace std;
    int n,m,v,flag,l,r,t[1000005];
    inline int read()
    {
    	int x=0,w=1;
    	char c=getchar();
    	while(c>'9'||c<'0')
    	{
    		if(c=='-') w=-1;
    		c=getchar();
    	}
    	while(c<='9'&&c>='0')
    	{
    		x=(x<<1)+(x<<3)+(c^'0');
    		c=getchar();
    	}
    	return x*w;
    }
    struct node
    {
    	int ls,rs,val;
    	node(){ls=rs=val=0;}
    }a[24000005];
    int cnt=0;
    int root[1000005];
    int build_bone(int l,int r)
    {
    	int k=++cnt;
    	if(l==r){a[k].val=t[l];return k;}
    	int mid=(l+r)>>1;
    	a[k].ls=build_bone(l,mid),a[k].rs=build_bone(mid+1,r);
    	return k;
    }
    int build_chain(int s,int x,int y,int l,int r)//复制s节点,并将x位的值改为y 
    {
    	int k=++cnt;
    	if(l==r){a[k].val=y;return k;}
    	int mid=(l+r)>>1;
    	if(x<=mid) a[k].rs=a[s].rs,a[k].ls=build_chain(a[s].ls,x,y,l,mid);
    	else a[k].ls=a[s].ls,a[k].rs=build_chain(a[s].rs,x,y,mid+1,r);
    	return k; 
    }
    int query(int x,int y,int l,int r)//查询x位上的值,到了y这个节点
    {
    	int mid=(l+r)>>1;
    	if(l==r) return a[y].val;
    	if(x<=mid) return query(x,a[y].ls,l,mid);
    	else return query(x,a[y].rs,mid+1,r);
    } 
    int main()
    {
    	n=read(),m=read();
    	for(int i=1;i<=n;i++) t[i]=read();
    	root[0]=build_bone(1,n);
    	for(int i=1;i<=m;i++)
    	{
    		v=read(),flag=read();
    		if(flag==1) l=read(),r=read(),root[i]=build_chain(root[v],l,r,1,n);
    		else{l=read();root[i]=root[v];printf("%d
    ",query(l,root[v],1,n));} 
    	}
    	return 0;
    } 
    

    写法上基本上没多大区别。

  • 相关阅读:
    其他技术----mongoDB基础
    redis学习----Redis入门
    网络通信学习----HTTP请求方法
    spring boot 学习 ---- spring boot admin
    java拓展----(转)synchronized与Lock的区别
    spring boot 学习 ---- spring MVC
    解决ubuntu的apt-get命令被占用
    阴暗
    图像分割实战-视频背景替换
    「知乎」你们觉得响应式好呢,还是手机和PC端分开来写?
  • 原文地址:https://www.cnblogs.com/tlx-blog/p/12356899.html
Copyright © 2020-2023  润新知