大树套小树
优化建图
树套树
显然啊,树套树有很多种。
可以线段树套平衡树,平衡树套线段树,线段树套线段树,树状数组套主席树等等。
1. 线段树套平衡树
最经典的树套树,还得是模板题。
前置知识:Fhq_Treap
如果全局维护什么排名、k 小值,前驱后继什么的,单是平衡树就可以解决了。
不过,这里有区间。
对于区间问题,很容易想到区间利器:线段树。
那么很容易想到可以用线段树维护区间,区间内部用平衡树维护权值。
所以就可以在每个线段树的节点上建立一棵平衡树,来查询排名、k 小值、前驱、后继并插入、删除某个数。
可能大多数人在刚刚学树套树的时候,会直接在线段树的每个节点直接放一个平衡树。
像这样:
struct Fhq_Treap{ ... ... }; struct Seg_Tree{ ... Fhq_Treap T[inf]; };
(也可能只有我这么傻)
显然这样会炸空间,而事实上代码也不是这么实现的。
因为 Treap 等大多数平衡树都是动态开点的,所以我们只需要在线段树的每个节点上记录下来每个 Treap 的根节点,然后就相当于每个节点开了一个 Treap,但最终的 Treap 还是用的一个数组。
至于具体操作,就是把 Fhq_Treap 的查询放到线段树的区间上。
- 单点修改
在线段树上找到对应的 个节点,然后删除原来的数,插入新的数。
时间复杂度 。
void insert(int &i,int k) {//Treap 中插入 split(i,k,x,y); i=merge(merge(x,new_(k)),y); } void remove(int &i,int k) {//Treap 中删除 split(i,k,x,z); split(x,k-1,x,y); y=merge(T[y].lc,T[y].rc); i=merge(merge(x,y),z); } void change(int i,int pos,int k) {//线段树中找节点 fhq.remove(T[i].gen,a[pos]); fhq.insert(T[i].gen,k); if(T[i].le==T[i].ri)return; int mid=(T[i].le+T[i].ri)>>1; if(pos<=mid)change(i<<1,pos,k); else change(i<<1|1,pos,k); }
- 区间排名
在线段树上找到对应的节点,然后在当前节点对应的 Fhq_Treap 中再查排名。
时间复杂度 。
int ask_rank(int i,int k) {//Treap 中查排名 split(i,k-1,x,y); int ans=T[x].siz; i=merge(x,y); return ans; } int ask_rank(int i,int l,int r,int k) {//线段树中找区间 if(l<=T[i].le&&T[i].ri<=r) return fhq.ask_rank(T[i].gen,k); int mid=(T[i].le+T[i].ri)>>1,ans=0; if(l<=mid)ans+=ask_rank(i<<1,l,r,k); if(mid<r)ans+=ask_rank(i<<1|1,l,r,k); return ans; }
- 区间前驱
同样是在线段树中找每个节点中的前驱,然后在各个节点中取 max。
时间复杂度 。
int ask_pre(int i,int k) {//Treap 中找前驱 int ans=-2147483647; split(i,k-1,x,y); if(T[x].siz)ans=ask_kth(x,T[x].siz); i=merge(x,y); return ans; } int ask_pre(int i,int l,int r,int k) {//线段树中取 max if(l<=T[i].le&&T[i].ri<=r) return fhq.ask_pre(T[i].gen,k); int mid=(T[i].le+T[i].ri)>>1,ans=-2147483647; if(l<=mid)ans=max(ans,ask_pre(i<<1,l,r,k)); if(mid<r)ans=max(ans,ask_pre(i<<1|1,l,r,k)); return ans; }
- 区间后继
同上,在线段树中找每个节点中的后继,然后在各个节点中取 min。
时间复杂度 。
int ask_nex(int i,int k) {//Treap 中找后继 int ans=2147483647; split(i,k,x,y); if(T[y].siz)ans=ask_kth(y,1); i=merge(x,y); return ans; } int ask_nex(int i,int l,int r,int k) {//线段树中取 min if(l<=T[i].le&&T[i].ri<=r) return fhq.ask_nex(T[i].gen,k); int mid=(T[i].le+T[i].ri)>>1,ans=2147483647; if(l<=mid)ans=min(ans,ask_nex(i<<1,l,r,k)); if(mid<r)ans=min(ans,ask_nex(i<<1|1,l,r,k)); return ans; }
- 区间 k 小值
这个比较特殊,不管是在线段树上还是在 Treap 上都不好直接维护。
那么我们转换一下思想,我们可以求什么?
区间排名。
那么我们可以通过二分答案,每次找到一个数并查询其排名,然后与我们要找的 k 小值作比较最终就可以得到我们想要的答案了。
时间复杂度:
二分 ,线段树 ,Treap 。
总复杂度大概是 ,离散化之后是 。
int ask_kth(int x,int y,int k) { int l=0,r=1e8; while(l<r) { int mid=(l+r+1)>>1; int ls=ask_rank(1,x,y,mid)+1; if(ls<=k)l=mid; else r=mid-1; } return l; }
时间复杂度瓶颈在于区间 k 小值,总复杂度 。
完整代码:
const int inf=5e4+7; int n,m,a[inf]; struct Fhq_Treap{ struct Treap{ int lc,rc; int siz,val,pri; }T[inf*40]; int cnt,x,y,z; int new_(int k) { T[++cnt].pri=rand(); T[cnt].siz=1,T[cnt].val=k; return cnt; } void pushup(int i) { T[i].siz=T[T[i].lc].siz+T[T[i].rc].siz+1; } void split(int i,int k,int &x,int &y) { if(i==0){x=y=0;return;} if(T[i].val<=k) x=i,split(T[i].rc,k,T[i].rc,y); else y=i,split(T[i].lc,k,x,T[i].lc); pushup(i); } int merge(int x,int y) { if(x==0||y==0)return x+y; if(T[x].pri<T[y].pri) { T[x].rc=merge(T[x].rc,y); pushup(x);return x; } else { T[y].lc=merge(x,T[y].lc); pushup(y);return y; } } void insert(int &i,int k) { split(i,k,x,y); i=merge(merge(x,new_(k)),y); } void remove(int &i,int k) { split(i,k,x,z); split(x,k-1,x,y); y=merge(T[y].lc,T[y].rc); i=merge(merge(x,y),z); } int ask_kth(int i,int k) { while(1) { if(k==T[T[i].lc].siz+1)return T[i].val; if(k<=T[T[i].lc].siz)i=T[i].lc; else k-=T[T[i].lc].siz+1,i=T[i].rc; } } int ask_rank(int i,int k) { split(i,k-1,x,y); int ans=T[x].siz; i=merge(x,y); return ans; } int ask_pre(int i,int k) { int ans=-2147483647; split(i,k-1,x,y); if(T[x].siz)ans=kth(x,T[x].siz); i=merge(x,y); return ans; } int ask_nex(int i,int k) { int ans=2147483647; split(i,k,x,y); if(T[y].siz)ans=kth(y,1); i=merge(x,y); return ans; } }fhq; struct Seg_Tree{ int le,ri; int gen; }T[inf<<2]; void build(int i,int l,int r) { T[i].le=l;T[i].ri=r; for(int j=l;j<=r;j++) fhq.insert(T[i].gen,a[j]); if(l==r)return; int mid=(l+r)>>1; build(i<<1,l,mid); build(i<<1|1,mid+1,r); } int ask_rank(int i,int l,int r,int k) { if(l<=T[i].le&&T[i].ri<=r) return fhq.ask_rank(T[i].gen,k); int mid=(T[i].le+T[i].ri)>>1,ans=0; if(l<=mid)ans+=ask_rank(i<<1,l,r,k); if(mid<r)ans+=ask_rank(i<<1|1,l,r,k); return ans; } int ask_kth(int x,int y,int k) { int l=0,r=1e8; while(l<r) { int mid=(l+r+1)>>1; int ls=ask_rank(1,x,y,mid)+1; if(ls<=k)l=mid; else r=mid-1; } return l; } void change(int i,int pos,int k) { fhq.remove(T[i].gen,a[pos]); fhq.insert(T[i].gen,k); if(T[i].le==T[i].ri)return; int mid=(T[i].le+T[i].ri)>>1; if(pos<=mid)change(i<<1,pos,k); else change(i<<1|1,pos,k); } int ask_pre(int i,int l,int r,int k) { if(l<=T[i].le&&T[i].ri<=r) return fhq.ask_pre(T[i].gen,k); int mid=(T[i].le+T[i].ri)>>1,ans=-2147483647; if(l<=mid)ans=max(ans,ask_pre(i<<1,l,r,k)); if(mid<r)ans=max(ans,ask_pre(i<<1|1,l,r,k)); return ans; } int ask_nex(int i,int l,int r,int k) { if(l<=T[i].le&&T[i].ri<=r) return fhq.ask_nex(T[i].gen,k); int mid=(T[i].le+T[i].ri)>>1,ans=2147483647; if(l<=mid)ans=min(ans,ask_nex(i<<1,l,r,k)); if(mid<r)ans=min(ans,ask_nex(i<<1|1,l,r,k)); return ans; } signed main() { n=re();m=re(); for(int i=1;i<=n;i++) a[i]=re(); build(1,1,n); for(int i=1;i<=m;i++) { int op=re(); if(op==1) { int l=re(),r=re(),k=re(); wr(ask_rank(1,l,r,k)+1),putchar('\n'); } if(op==2) { int l=re(),r=re(),k=re(); wr(ask_kth(l,r,k)),putchar('\n'); } if(op==3) { int pos=re(),k=re(); change(1,pos,k); a[pos]=k; } if(op==4) { int l=re(),r=re(),k=re(); wr(ask_pre(1,l,r,k)),putchar('\n'); } if(op==5) { int l=re(),r=re(),k=re(); wr(ask_nex(1,l,r,k)),putchar('\n'); } } return 0; }
理解了二逼平衡树,剩下的几个树套树也就好理解了。
其实就是在第一维的查询时在第二维统计答案。
2. 树状数组套主席树
填之前在可持久化那里挖的坑
前置知识:树状数组。
先看这样一个问题:
-
对于给定的数列, 次询问,每次输出区间 的区间和。
前缀和大水题。
-
对于给定的数列, 次询问,每次输出区间 的区间 小值。
主席树板子题。
-
对于给定的数列, 次操作,每次单点修改,或输出区间 的区间和。
树状数组板子题。
-
对于给定的数列, 次操作,每次单点修改,或输出区间 的区间 小值。
树状数组套主席树大水题。
通过上文,你应该已经理解树状数组套主席树是怎么工作的了。
树状数组的每个节点 代表的区间 的前缀和,那么套主席树之后的每个节点就代表 的主席树前缀和。
单点修改就修改对应的 棵主席树。
查询的时候,也不再是原来的两个树相减,而是 个树相减。
其实严格来讲,代码里并没有可持久化,所以他真正的名字应该是 树状数组套权值线段树。
修改和相减的也都只是对应的权值线段树。
至于细节问题,还是看代码吧。
const int inf=1e5+7; int n,m,a[inf]; int bok[inf<<1],cnt,num; struct Query{ char op[10]; int l,r,k,x; }h[inf]; struct Seg_Tree{ int lc,rc; int sum; }T[inf*300]; int rot[inf],sum; void insert(int &i,int l,int r,int k,int v) {//主席树的插入/删除 if(i==0)i=++sum; T[i].sum+=v; if(l==r)return; int mid=(l+r)>>1; if(k<=mid)insert(T[i].lc,l,mid,k,v); else insert(T[i].rc,mid+1,r,k,v); } int r1[inf],r2[inf],cnt1,cnt2; int kth(int l,int r,int k) {//主席树查询 k 小值 if(l==r)return bok[l]; int mid=(l+r)>>1,siz=0; for(int i=1;i<=cnt1;i++) siz-=T[T[r1[i]].lc].sum; for(int i=1;i<=cnt2;i++) siz+=T[T[r2[i]].lc].sum; if(k<=siz) { for(int i=1;i<=cnt1;i++) r1[i]=T[r1[i]].lc; for(int i=1;i<=cnt2;i++) r2[i]=T[r2[i]].lc; return kth(l,mid,k); } else { for(int i=1;i<=cnt1;i++) r1[i]=T[r1[i]].rc; for(int i=1;i<=cnt2;i++) r2[i]=T[r2[i]].rc; return kth(mid+1,r,k-siz); } } int lowbit(int x){return x&(-x);} void change(int i,int k,int v) {//树状数组 log n 棵树的单点修改 for(;i<=n;i+=lowbit(i)) insert(rot[i],1,num,k,v); } int ask_kth(int l,int r,int k) {//树状数组寻找需要查询的 2log n 个树 cnt1=cnt2=0; for(int i=l;i>0;i-=lowbit(i)) r1[++cnt1]=rot[i]; for(int i=r;i>0;i-=lowbit(i)) r2[++cnt2]=rot[i]; return kth(1,num,k); } int main() { n=re();m=re(); for(int i=1;i<=n;i++) a[i]=re(),bok[++cnt]=a[i]; for(int i=1;i<=m;i++) { scanf("%s",h[i].op); if(h[i].op[0]=='C') h[i].x=re(),h[i].k=re(),bok[++cnt]=h[i].k; else h[i].l=re(),h[i].r=re(),h[i].k=re(); } sort(bok+1,bok+cnt+1); num=unique(bok+1,bok+cnt+1)-bok-1; for(int i=1;i<=n;i++) a[i]=lower_bound(bok+1,bok+num+1,a[i])-bok; for(int i=1;i<=m;i++) if(h[i].op[0]=='C') h[i].k=lower_bound(bok+1,bok+num+1,h[i].k)-bok; for(int i=1;i<=n;i++) change(i,a[i],1); for(int i=1;i<=m;i++) { if(h[i].op[0]=='C') { change(h[i].x,a[h[i].x],-1); change(h[i].x,h[i].k,1); a[h[i].x]=h[i].k; } else wr(ask_kth(h[i].l-1,h[i].r,h[i].k)),putchar('\n'); } return 0; }
3. 线段树套线段树
相比之下,平衡树的代码量要比权值线段树长很多,就算是短小精悍的 Fhq_Treap 也是如此。
所以有时候,我们会选择用权值线段树代替平衡树进行一些维护。
所以这里的树套树就是 区间线段树套权值线段树。
比如求 动态逆序对。
首先,求动态逆序对需要先求出原始序列的逆序对数。
这里选择了权值树状数组,因为它短快。
int lowbit(int x){return x&(-x);} void add(int i){while(i<=n)s[i]++,i+=lowbit(i);} int ask(int i){int ans=0;while(i)ans+=s[i],i-=lowbit(i);return ans;} for(int i=1;i<=n;i++) { a[i]=re(),dy[a[i]]=i; ans+=ask(n)-ask(a[i]); add(a[i]); }
然后考虑每删除一个数对序列的逆序对数会产生什么影响?
-
除被删除元素外其他元素之间的相对位置关系没变,逆序对数不变。
-
位置靠前且更大的元素与被删除元素的逆序对消失。
-
位置靠后且更小的元素与被删除元素的逆序对消失。
所以就是统计两种产生逆序对的元素的个数。
至于如何查询,第一维查询区间,第二维查询权值。
Code
struct Segment_Tree{ struct Seg_Tree{ int lc,rc; int sum; }T[inf*300]; int cnt; void insert(int &i,int l,int r,int k,int v) {//单点插入或删除 if(i==0)i=++cnt; T[i].sum+=v; if(l==r)return; int mid=(l+r)>>1; if(k<=mid)insert(T[i].lc,l,mid,k,v); else insert(T[i].rc,mid+1,r,k,v); } int ask_pre(int i,int l,int r,int k) {//比 k 小的数的个数 if(i==0||l==r)return 0; int mid=(l+r)>>1; if(k<=mid)return ask_pre(T[i].lc,l,mid,k); return ask_pre(T[i].rc,mid+1,r,k)+T[T[i].lc].sum; } int ask_nex(int i,int l,int r,int k) {//比 k 大的数的个数 if(i==0||l==r)return 0; int mid=(l+r)>>1; if(mid<k)return ask_nex(T[i].rc,mid+1,r,k); return ask_nex(T[i].lc,l,mid,k)+T[T[i].rc].sum; } }xds; struct Seg_Tree{ int le,ri; int gen; }T[inf<<2]; void build(int i,int l,int r) {//建树 T[i].le=l,T[i].ri=r; for(int j=l;j<=r;j++) xds.insert(T[i].gen,1,n,a[j],1); if(l==r)return; int mid=(l+r)>>1; build(i<<1,l,mid); build(i<<1|1,mid+1,r); } void remove(int i,int pos,int k) {//删数 xds.insert(T[i].gen,1,n,k,-1); if(T[i].le==T[i].ri)return; int mid=(T[i].le+T[i].ri)>>1; if(pos<=mid)remove(i<<1,pos,k); else remove(i<<1|1,pos,k); } int ask_pre(int i,int l,int r,int k) {//比 k 小的数的个数 if(l<=T[i].le&&T[i].ri<=r) return xds.ask_pre(T[i].gen,1,n,k); int mid=(T[i].le+T[i].ri)>>1,ans=0; if(l<=mid)ans+=ask_pre(i<<1,l,r,k); if(mid<r)ans+=ask_pre(i<<1|1,l,r,k); return ans; } int ask_nex(int i,int l,int r,int k) {//比 k 大的数的个数 if(l<=T[i].le&&T[i].ri<=r) return xds.ask_nex(T[i].gen,1,n,k); int mid=(T[i].le+T[i].ri)>>1,ans=0; if(l<=mid)ans+=ask_nex(i<<1,l,r,k); if(mid<r)ans+=ask_nex(i<<1|1,l,r,k); return ans; }
至于主函数,自己填填补补就好了。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】博客园携手 AI 驱动开发工具商 Chat2DB 推出联合终身会员
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于决定:把自己家的能源管理系统开源了!
· C#实现 Winform 程序在系统托盘显示图标 & 开机自启动
· 了解 ASP.NET Core 中的中间件
· 实现windows下简单的自动化窗口管理
· 【C语言学习】——命令行编译运行 C 语言程序的完整流程