大树套小树
优化建图
树套树
显然啊,树套树有很多种。
可以线段树套平衡树,平衡树套线段树,线段树套线段树,树状数组套主席树等等。
1. 线段树套平衡树
最经典的树套树,还得是模板题。
前置知识:Fhq_Treap
如果全局维护什么排名、k 小值,前驱后继什么的,单是平衡树就可以解决了。
不过,这里有区间。
对于区间问题,很容易想到区间利器:线段树。
那么很容易想到可以用线段树维护区间,区间内部用平衡树维护权值。
所以就可以在每个线段树的节点上建立一棵平衡树,来查询排名、k 小值、前驱、后继并插入、删除某个数。
可能大多数人在刚刚学树套树的时候,会直接在线段树的每个节点直接放一个平衡树。
像这样:
struct Fhq_Treap{
...
...
};
struct Seg_Tree{
...
Fhq_Treap T[inf];
};
(也可能只有我这么傻)
显然这样会炸空间,而事实上代码也不是这么实现的。
因为 Treap 等大多数平衡树都是动态开点的,所以我们只需要在线段树的每个节点上记录下来每个 Treap 的根节点,然后就相当于每个节点开了一个 Treap,但最终的 Treap 还是用的一个数组。
至于具体操作,就是把 Fhq_Treap 的查询放到线段树的区间上。
- 单点修改
在线段树上找到对应的 \(\log n\) 个节点,然后删除原来的数,插入新的数。
时间复杂度 \(O(n\log^2n)\)。
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 中再查排名。
时间复杂度 \(O(n\log^2n)\)。
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。
时间复杂度 \(O(n\log^2n)\)。
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。
时间复杂度 \(O(n\log^2n)\)。
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 小值作比较最终就可以得到我们想要的答案了。
时间复杂度:
二分 \(\log W\),线段树 \(\log n\),Treap \(\log n\)。
总复杂度大概是 \(\log ^2n\log W\),离散化之后是 \(\log^3n\)。
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 小值,总复杂度 \(O(n\log^3n)\)。
完整代码:
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. 树状数组套主席树
填之前在可持久化那里挖的坑
前置知识:树状数组。
先看这样一个问题:
-
对于给定的数列,\(q\) 次询问,每次输出区间 \([l,r]\) 的区间和。
前缀和大水题。
-
对于给定的数列,\(q\) 次询问,每次输出区间 \([l,r]\) 的区间 \(k\) 小值。
主席树板子题。
-
对于给定的数列,\(q\) 次操作,每次单点修改,或输出区间 \([l,r]\) 的区间和。
树状数组板子题。
-
对于给定的数列,\(q\) 次操作,每次单点修改,或输出区间 \([l,r]\) 的区间 \(k\) 小值。
树状数组套主席树大水题。
通过上文,你应该已经理解树状数组套主席树是怎么工作的了。
树状数组的每个节点 \(i\) 代表的区间 \([1,i]\) 的前缀和,那么套主席树之后的每个节点就代表 \([1,i]\) 的主席树前缀和。
单点修改就修改对应的 \(\log n\) 棵主席树。
查询的时候,也不再是原来的两个树相减,而是 \(2\log\) 个树相减。
其实严格来讲,代码里并没有可持久化,所以他真正的名字应该是 树状数组套权值线段树。
修改和相减的也都只是对应的权值线段树。
至于细节问题,还是看代码吧。
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;
}
至于主函数,自己填填补补就好了。