……

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

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

主席树

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

\[T_1-T_2 \]

为每个点权值之差,得到的新权值线段树\(T\),就维护了\([l,r]\)的权值,依上题跑可以解决。
这样的时间复杂度为\(O(mn\log 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(n\log 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\n",query(root[r],root[l-1],k));	
	} 

总的说,时间复杂度为\(O((n+m)\log n)\),空间复杂度是\(O(m\log 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\n",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\n",query(l,root[v]));} 
    }
    return 0;
}//不加快读会T一个点 

关于空间:主席树的空间开到\(O(m\log 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\n",query(l,root[v],1,n));} 
	}
	return 0;
} 

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

posted @ 2020-02-24 14:40  童话镇里的星河  阅读(225)  评论(0编辑  收藏  举报