要不是学这个我才不学什么权值线段树呢。
主席树
很高大上?
其实就是可持久化的数据结构
在学习权值线段树时,我们可能会想,如果求任意区间第(k)小(大)咋办呢?
题目链接:P3834 【模板】可持久化线段树 1(主席树)
就是他了!
乍一想与可持久化没啥关系,但是你先听我说。
我们考虑一颗维护([0,l-1])区间的权值线段树(T_1),和维护([0,r])区间的权值线段树(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;
}
写法上基本上没多大区别。