大树套小树

优化建图

树套树

显然啊,树套树有很多种。

可以线段树套平衡树,平衡树套线段树,线段树套线段树,树状数组套主席树等等。

1. 线段树套平衡树

最经典的树套树,还得是模板题。

前置知识:Fhq_Treap

如果全局维护什么排名、k 小值,前驱后继什么的,单是平衡树就可以解决了。

不过,这里有区间。

对于区间问题,很容易想到区间利器:线段树。

那么很容易想到可以用线段树维护区间,区间内部用平衡树维护权值。

所以就可以在每个线段树的节点上建立一棵平衡树,来查询排名、k 小值、前驱、后继并插入、删除某个数。

可能大多数人在刚刚学树套树的时候,会直接在线段树的每个节点直接放一个平衡树。

像这样:

struct Fhq_Treap{
	...
	...
};
struct Seg_Tree{
	...
	Fhq_Treap T[inf];
};

(也可能只有我这么傻)

显然这样会炸空间,而事实上代码也不是这么实现的。

因为 Treap 等大多数平衡树都是动态开点的,所以我们只需要在线段树的每个节点上记录下来每个 Treap 的根节点,然后就相当于每个节点开了一个 Treap,但最终的 Treap 还是用的一个数组。

至于具体操作,就是把 Fhq_Treap 的查询放到线段树的区间上。

  1. 单点修改

在线段树上找到对应的 \(\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);
}
  1. 区间排名

在线段树上找到对应的节点,然后在当前节点对应的 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;
}
  1. 区间前驱

同样是在线段树中找每个节点中的前驱,然后在各个节点中取 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;
}
  1. 区间后继

同上,在线段树中找每个节点中的后继,然后在各个节点中取 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;
}
  1. 区间 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. 树状数组套主席树

填之前在可持久化那里挖的坑

前置知识:树状数组。

先看这样一个问题:

  1. 对于给定的数列,\(q\) 次询问,每次输出区间 \([l,r]\) 的区间和。

    前缀和大水题。

  2. 对于给定的数列,\(q\) 次询问,每次输出区间 \([l,r]\) 的区间 \(k\) 小值。

    主席树板子题。

  3. 对于给定的数列,\(q\) 次操作,每次单点修改,或输出区间 \([l,r]\) 的区间和。

    树状数组板子题。

  4. 对于给定的数列,\(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]);
}

然后考虑每删除一个数对序列的逆序对数会产生什么影响?

  1. 除被删除元素外其他元素之间的相对位置关系没变,逆序对数不变。

  2. 位置靠前且更大的元素与被删除元素的逆序对消失。

  3. 位置靠后且更小的元素与被删除元素的逆序对消失。

所以就是统计两种产生逆序对的元素的个数。

至于如何查询,第一维查询区间,第二维查询权值。

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;
}

至于主函数,自己填填补补就好了。

进阶

posted @ 2022-09-13 20:58  Zvelig1205  阅读(53)  评论(0编辑  收藏  举报