[笔记]树套树 之 树状数组套权值线段树

树状数组套权值线段树,是众多树套树的一种(下文树套树默认指树状数组套权值线段树),可以在单次查询 / 点修均 \(O(\log^2 n)\) 的时间复杂度内,在线处理二维数点 / 区间第 \(k\) 小等问题。

  • 前置知识:
    • 线段树
    • 树状数组
    • 可持久化线段树
  • 本文代码中 auto [a,b,c]=q[i] 的写法仅 c++17 及以上可用,请勿在 NOIp 等考场上使用 ><
    upd on 2025/10/17:经了解,因为 c++14 会向后兼容,所以应该是可以使用的,只是会报警告。

思路简述

后面会有对应的例题。

二维偏序

二维偏序问题,即在一个由 \(n\) 个二元组构成的集合 \(\{(x_1,y_1),\dots,(x_n,y_n)\}\) 中,求与 \((x_i,y_i)\) 满足某种偏序关系,即 \((x_i,y_i)\prec(x_j,y_j)\)\(j\) 的个数。

这其中,“\(\prec\)”是一种具有自反性、反对称性、传递性的二元关系,例如:

  • \(x_i\le x_j,\ y_i<y_j\)
  • \(x_i>x_j,\ y_i\le y_j\)
  • \(\dots\)

逆序对就是常见的二维偏序问题,此时这个集合可以写为 \(\{(1,a_1),\dots,(n,a_n)\}\)


我们先考虑下面的问题:

对于一个长度为 \(n\) 的序列 \(A\),你需要回答若干个询问:

  • 求下标在 \([l,r]\) 范围内,值域在 \([a,b]\) 范围内的元素个数。

若操作离线,我们可以使用主席树。用前缀和的思想,令第 \(i\) 棵权值线段树存储 \(A_{1\sim i}\) 的数据。查询时用第 \(r\) 和第 \(l-1\) 棵线段树作差,在这棵新的线段树上查询 \([a,b]\) 区间和即可。

若强制在线,我们类比序列上的操作,将前缀和改为树状数组来维护,每个树状数组的节点都是一棵线段树,这棵线段树就是其管辖范围内所有线段树的和。

修改和查询和普通树状数组完全一样,只不过是数值的操作变成了对线段树的操作。即:

  • 修改时最多修改 \(\log n\) 棵线段树。
  • 用于查询的 \([l,r]\) 区间的线段树,可以表示为最多 \(\log V\) 棵线段树与最多 \(\log V\) 棵线段树作差的形式。

外层树状数组操作是 \(O(\log n)\),内层权值线段树是 \(O(\log V)\),其中 \(V\) 为值域。于是我们可以达到修改、查询均为 \(O(\log n\log V)\) 的时间复杂度。

为了让空间不炸,我们选择动态开点的权值线段树。线段树用去的节点数量上界是 \(n\log n\log V\)(实际情况远小于上界)。

\(V\) 较大时,我们常常先对值域离散化,这样上文的 \(\log V\) 会降为 \(\log n\)

这就是树套树的基本框架。

struct SEG{//权值线段树
	int idx;
	struct node{int lc,rc,sum;}tr[N*logN*logV];//这个要视情况而定
	#define lc(x) (tr[x].lc)//请注意,不使用undef的话,define的作用域是从此处直到文件结尾
	#define rc(x) (tr[x].rc)//放在这里面只是为了条理一些
	#define sum(x) (tr[x].sum)//也可以使用内联函数(inline)来解决这个问题
	void chp(int &x,int a,int v,int l,int r){
		if(!x) x=++idx;//动态开点
		sum(x)+=v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc(x),a,v,l,mid);
		else chp(rc(x),a,v,mid+1,r);
	}
	int query(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return sum(x);
		int mid=(l+r)>>1,ans=0;
		if(a<=mid) ans+=query(lc(x),a,b,l,mid);
		if(b>mid) ans+=query(rc(x),a,b,mid+1,r);
		return ans;
	}
}tr;
struct BIT{//树状数组
	void chp(int x,int a,int v){//第x棵线段树的第a位累加v
		while(x<=n) tr.chp(root[x],a,v,1,n),x+=lowbit(x);
	}
	int query(int x,int a,int b){//[1,x]线段树在[a,b]上的和
		int ans=0;
		while(x) ans+=tr.query(root[x],a,b,1,n),x-=lowbit(x);
		return ans;
	}
}bit;

进而我们就可以很轻松地解决二维偏序问题,令外层树状数组维护下标,内层线段树维护值域。统计答案时按上面的问题查询即可。

树状数组上的查询依赖于数据的可加减性,因此树状数组套权值线段树所维护的内容一般需要是可加减的(比如区间和,但区间最值不是);如果想维护区间最值之类,可以将外层的树状数组换成线段树。

二维数点

二维数点,即在二维平面上给出若干点,然后进行若干次查询,每次询问某个矩形中有多少个点。

形式化地,即:有 \(n\) 个二元组 \((x_1,y_1),\dots,(x_n,y_n)\),每次询问给定 \(a,b,c,d\),求满足 \(x_i\in [a,b],y_i\in [c,d]\)\(i\) 有多少个。


离散化一下就可以用上面的方法来解决了,外层树状数组维护 \(x\),内层线段树维护 \(y\)

三维偏序

相较二维偏序多了一维,变成 \((x,y,z)\)


能在线求二维偏序,就能离线求三维偏序。拿 \(x_i\le x_j,y_i\le y_j,z_i\le z_j\) 为例。

我们将所有点离线下来排序,按 \(x\) 从小到大遍历,并将 \((y,z)\) 放入树套树,以保证查询 \((x,y,z)\) 时,有且仅有满足 \(x'\le x\)\((x',y',z')\) 被放入树套树即可。这样第一维 \(x\) 解决,剩下的就是 \(y,z\) 的二维偏序了,直接搬上面的代码即可。

三维数点

三维坐标系中的数点。

形式化地,即:有 \(n\) 个三元组 \((x_1,y_1,z_1),\dots,(x_n,y_n,z_n)\),每次询问给定 \(a,b,c,d,e,f\),求满足 \(x_i\in [a,b],y_i\in [c,d],z_i\in [e,f]\)\(i\) 有多少个。


仍然将操作离线。\(x\in [a,b]\) 的答案,可以拆成两次查询,即 \(x\in [1,b]\)\(x\in [1,a-1]\) 的答案作差。

也就是说,我们仍然将所有点按 \(x\) 从小到大排序,并在遍历过程中将 \((y,z)\) 放入树套树。这样第一维 \(x\) 解决掉。

\(x\in[1,w]\) 的点添加完毕后,则根据我们的查询内容,对 \(y,z\) 进行二维数点,对答案进行累加 / 累减。

例题

P3157 [CQOI2011] 动态逆序对

树套树基本操作的例题。

不难发现,每删除 \(1\) 个元素 \(v\),就将答案减去“\(v\) 左边比 \(v\) 小的元素个数”和“\(v\) 右边比 \(v\) 大的元素个数”。

查询它们,直接套上面的代码就可以。

时间复杂度 \(O((n+m)\log^2 n)\)

节点总数 \(n \log^2 n\)

点击查看代码 R223054208
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int n,q,a[N],pos[N],root[N],ans;
inline int lowbit(int x){return x&-x;}
struct SEG{
	int idx;
	struct node{int lc,rc,sum;}tr[N*18*18];
	#define lc(x) (tr[x].lc)
	#define rc(x) (tr[x].rc)
	#define sum(x) (tr[x].sum) 
	void chp(int &x,int a,int v,int l,int r){
		if(!x) x=++idx;
		sum(x)+=v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc(x),a,v,l,mid);
		else chp(rc(x),a,v,mid+1,r);
	}
	int query(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return sum(x);
		int mid=(l+r)>>1,ans=0;
		if(a<=mid) ans+=query(lc(x),a,b,l,mid);
		if(b>mid) ans+=query(rc(x),a,b,mid+1,r);
		return ans;
	}
}tr;
struct BIT{
	void chp(int x,int a,int v){
		while(x<=n) tr.chp(root[x],a,v,1,n),x+=lowbit(x);
	}
	int query(int x,int a,int b){//[1,x]线段树在[a,b]上的和
		int ans=0;
		while(x) ans+=tr.query(root[x],a,b,1,n),x-=lowbit(x);
		return ans;
	}
}bit;
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;i++){
		cin>>a[i],pos[a[i]]=i;
		ans+=bit.query(i-1,a[i],n);
		bit.chp(i,a[i],1);//本应该是a[i]+1,但a[i]互不相同所以没关系,况且边界处理稍麻烦 
	}
	int v,p;
	while(q--){
		cin>>v,p=pos[v];
		cout<<ans<<"\n";
		bit.chp(p,v,-1);
		ans-=bit.query(p-1,v,n);//同理
		ans-=bit.query(n,1,v)-bit.query(p-1,1,v);
	}
	return 0;
}

与此同理的还有 P1975 [国家集训队] 排队和它的加强版 P12685 [国家集训队] 排队 加强版

后面那道我已经尽力卡常了(哭)

以下是随机数据下的运行情况:img

很奇怪,为什么跑得比题解快还会 T,难道洛谷数据不是随机的吗?过几天再探究下原因,并会将 AC 代码放上来。

此问题也发在讨论区,欢迎分享你的见解。

P3810 【模板】三维偏序(陌上花开)

三维偏序例题。

时间复杂度 \(O(n\log n\log k)\)。也可以离散化做到 \(O(n\log^2 n)\),不过因为 \(k\)\(n\) 大不了多少,所以没有必要。

节点总数 \(n\log n\log k\)

点击查看代码 R223063850
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,K=2e5+10;
int n,k,root[K],ans[N];
inline int lowbit(int x){return x&-x;}
struct node{int a,b,c;}a[N];
struct SEG{
	int idx;
	struct node{int lc,rc,sum;}tr[N*18*18];
	#define lc(x) (tr[x].lc)
	#define rc(x) (tr[x].rc)
	#define sum(x) (tr[x].sum)
	void chp(int &x,int a,int v,int l,int r){
		if(!x) x=++idx;
		sum(x)+=v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc(x),a,v,l,mid);
		else chp(rc(x),a,v,mid+1,r);
	}
	int query(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return sum(x);
		int mid=(l+r)>>1,ans=0;
		if(a<=mid) ans+=query(lc(x),a,b,l,mid);
		if(b>mid) ans+=query(rc(x),a,b,mid+1,r);
		return ans;
	}
}tr;
struct BIT{
	void chp(int x,int a,int v){
		while(x<=k) tr.chp(root[x],a,v,1,k),x+=lowbit(x);
	}
	int query(int x,int a,int b){
		int ans=0;
		while(x) ans+=tr.query(root[x],a,b,1,k),x-=lowbit(x);
		return ans;
	}
}bit;
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n;i++) cin>>a[i].a>>a[i].b>>a[i].c;
	sort(a+1,a+1+n,[](node a,node b){return a.a<b.a;});
	int last=1;
	for(int i=1;i<=n;i++){
		bit.chp(a[i].b,a[i].c,1);
		if(a[i].a!=a[i+1].a){
			while(last<=i){
				ans[bit.query(a[last].b,1,a[last].c)-1]++;
				last++;
			}
		}
	}
	for(int i=0;i<n;i++) cout<<ans[i]<<"\n";
	return 0;
}

CF848C Goodbye Souvenir

二维数点例题。

“每种颜色最右边 \(-\) 最左边求和”看起来不好直接维护,但是可以转化为“相邻同色位置的下标差求和”。

也就是说,如果令 \(pre_i\) 表示最大的 \(j<i\) 使得 \(a_j=a_i\)(不存在为 \(0\)),那么 \([l,r]\) 的答案就是:

\[\sum\limits_{i\le r,pre_i\ge l} i-pre_{i} \]

如果将 \((i,pre_i)\) 看作二维平面上的点,\(i-pre_{i}\) 看作点权,那么我们要统计的实际上是一个矩形区域内的点权和。因此这是一个二维数点问题。

可以用外层树状数组维护第一维,内层线段树维护第二维。

单点修改时,受到影响的只可能有:

  • 被修改的位置。
  • 与旧颜色相同的后一位。
  • 与新颜色相同的后一位。

可以使用 set 很方便地维护这些位置信息。

需要注意的是,如果你使用节点回收,那么需要特判 \(a_x=y\) 的情况,否则可能会 WA on #6。

这是因为如果 \(a_x=y\),在处理中途线段树上可能出现负数,而节点回收条件是 sum(x)=0,所以可能会在子节点的 \(sum\neq 0\) 的情况下将 \(sum=0\) 的父节点错误地回收掉。

解决方法就是加上特判 / 不使用节点回收。

下面的样例的答案应为 9 9,可用于验证。

6 3
1 2 3 1 3 2
2 1 6
1 2 2
2 1 6
点击查看代码 #329304500
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10,V=1e5+10;
int n,m,root[N],a[N];
set<int> p[V];
inline int lb(int x){return x&-x;}
struct SEG{
	stack<int> gar;
	int idx,lc[120*N],rc[120*N];ll sum[120*N];
	void del(int &x){gar.push(x),x=0;}
	void newnode(int &x){
		if(!gar.empty()) x=gar.top(),gar.pop(),lc[x]=rc[x]=sum[x]=0;
		else x=++idx;
	}
	void chp(int &x,int a,int v,int l,int r){
		if(!x) newnode(x);
		sum[x]+=v;
		if(l<r){
			int mid=(l+r)>>1;
			if(a<=mid) chp(lc[x],a,v,l,mid);
			else chp(rc[x],a,v,mid+1,r);
		}
		if(!sum[x]) del(x);
	}
	ll query(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return sum[x];
		int mid=(l+r)>>1;ll ans=0;
		if(a<=mid) ans+=query(lc[x],a,b,l,mid);
		if(b>mid) ans+=query(rc[x],a,b,mid+1,r);
		return ans;
	}
}seg;
struct BIT{
	void chp(int x,int ia,int iv){
		for(;x<=n;x+=lb(x)) seg.chp(root[x],ia,iv,0,n);
	}
	ll query(int x,int ia,int ib){
		ll ans=0;
		for(;x;x-=lb(x)) ans+=seg.query(root[x],ia,ib,0,n);
		return ans;
	}
}bit;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1,pre;i<=n;i++){
		cin>>a[i];
		pre=p[a[i]].empty()?0:(*p[a[i]].rbegin());
		bit.chp(i,pre,i-pre);
		p[a[i]].insert(i);
	}
	int op,x,y,pre,nxt;
	set<int>::iterator it;
	while(m--){
		cin>>op>>x>>y;
		if(op==1){
			if(a[x]==y) continue;
			p[a[x]].erase(x);
			it=p[y].lower_bound(x+1),pre=0;//1.与新颜色相同的后一位
			if(it!=p[y].begin()) pre=*(--it);
			it=p[y].upper_bound(x);
			if(it!=p[y].end()){
				nxt=*it;
				bit.chp(nxt,x,nxt-x);
				bit.chp(nxt,pre,pre-nxt);
			}
			it=p[a[x]].lower_bound(x+1),pre=0;//2.与旧颜色相同的后一位
			if(it!=p[a[x]].begin()) pre=*(--it);
			it=p[a[x]].upper_bound(x);
			if(it!=p[a[x]].end()){
				nxt=*it;
				bit.chp(nxt,x,x-nxt);
				bit.chp(nxt,pre,nxt-pre);
			}
			bit.chp(x,pre,pre-x);//3.被修改的位置
			a[x]=y,pre=0;
			it=p[y].lower_bound(x+1);
			if(it!=p[y].begin()) pre=*(--it);
			p[y].insert(x);
			bit.chp(x,pre,x-pre);
		}else{
			cout<<bit.query(y,x,n)<<"\n";
		}
	}
	return 0;
}

P3759 [TJOI2017] 不勤劳的图书管理员

题面太抽象了,人话就是要维护序列 \(a,v\),需要支持 \(x\)\(y\) 位交换,并支持查询:

\[\sum\limits_{i<j\wedge a_i>a_j}(v_i+v_j) \]

动态逆序对的加强版,现在是带权的。

这样我们需要在线段树上维护两个值,一个是个数,一个是权值和。

查询时需要将所有满足 \(i<j\)\(a_i>a_j\)\(j\) 在线段树上求出权值和,再加上总个数 \(\times v_i\) 即可。

点击查看代码 R225456803
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e4+10,V=1e5+1,P=1e9+7;
int n,m,a[N],v[N],root[N];ll ans;
struct Data{
	int cnt;ll sum;
	Data operator+(const Data& b) const{return {cnt+b.cnt,sum+b.sum};}
	Data operator-(const Data& b) const{return {cnt-b.cnt,sum-b.sum};}
};
struct SEG{
	int idx,lc[200*N],rc[200*N];
	Data d[200*N];
	void chp(int& x,int a,Data v,int l,int r){
		if(!x) x=++idx;
		d[x]=d[x]+v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc[x],a,v,l,mid);
		else chp(rc[x],a,v,mid+1,r);
	}
	Data query(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return d[x];
		int mid=(l+r)>>1;Data ans{0,0};
		if(a<=mid) ans=ans+query(lc[x],a,b,l,mid);
		if(b>mid) ans=ans+query(rc[x],a,b,mid+1,r);
		return ans;
	}
}seg;
inline int lb(int x){return x&-x;}
struct BIT{
	void chp(int x,int ia,Data iv){
		for(;x<=n;x+=lb(x)) seg.chp(root[x],ia,iv,0,V);
	}
	Data query(int x,int ia,int ib){
		Data ans{0,0};
		for(;x;x-=lb(x)) ans=ans+seg.query(root[x],ia,ib,0,V);
		return ans;
	}
	Data query(int x,int y,int ia,int ib){
		return query(y,ia,ib)-query(x-1,ia,ib);
	}
}bit;
ll calc(int x,int y,int ia,int ib,int v){
	Data d=bit.query(x,y,ia,ib);
	return 1ll*d.cnt*v+d.sum;
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>v[i];
		ans+=calc(1,i-1,a[i]+1,V,v[i]);//这里如果按i离线,额外开一个树状数组会更快些
		bit.chp(i,a[i],{1,v[i]});
	}
	while(m--){
		int x,y;
		cin>>x>>y;
		if(x>y) swap(x,y);
		ans+=calc(x,y,a[x]+1,V,v[x]);
		ans-=calc(x,y,0,a[x]-1,v[x]);
		ans-=calc(x,y,a[y]+1,V,v[y]);
		ans+=calc(x,y,0,a[y]-1,v[y]);
		if(a[x]<a[y]) ans-=v[x]+v[y];
		else if(a[x]>a[y]) ans+=v[x]+v[y];
		bit.chp(x,a[x],{-1,-v[x]});
		bit.chp(y,a[y],{-1,-v[y]});
		swap(a[x],a[y]),swap(v[x],v[y]);
		bit.chp(x,a[x],{1,v[x]});
		bit.chp(y,a[y],{1,v[y]});
		cout<<ans%P<<"\n";
	}
	return 0;
}

P4396 [AHOI2013] 作业

二维 & 三维数点例题。

第一问是裸的二维数点。

第二问考虑转化一下,与上题类似地,我们令 \(pre_i\) 为最大的 \(j<i\) 使得 \(a_j=a_i\),若不存在则为 \(0\)

这样转化成三维数点问题,即求满足下列条件的 \(i\) 的个数:

  • \(i\in[l,r]\)
  • \(a_i\in[a,b]\)
  • \(pre_i\in[0,l-1]\)

用这个 \(pre\) 来解决区间数颜色问题是一个很常用的技巧,后面我们还会遇到~

第一维离线干掉,剩下两维树套树。

考虑到 \(pre_i\) 会取到 \(0\),为了方便实现,我们用树状数组维护第二维,线段树维护第三维。

由于第二问离线了,所以第一问跟着变成一维数点,开一个树状数组即可解决。

时间复杂度 \(O(n\log n\log V)\)

节点总数 \(n\log n\log V\)

点击查看代码 R224341732
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10,M=1e5+10,V=1e5;
int n,m,a[N],idx,pre[N],pos[V+10],root[V],ans[M][2],CNT;
struct Query{int id,v,p,oa,ob,ib;}q[M<<1];
inline int lb(int x){return x&-x;}
struct SEG{
	int idx=0,lc[300*M],rc[300*M],sum[300*M];
	void chp(int& x,int a,int v,int l,int r){
		if(!x) x=++idx;
		sum[x]+=v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc[x],a,v,l,mid);
		else chp(rc[x],a,v,mid+1,r);
	}
	int query(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return sum[x];
		int mid=(l+r)>>1,ans=0;
		if(a<=mid) ans+=query(lc[x],a,b,l,mid);
		if(b>mid) ans+=query(rc[x],a,b,mid+1,r);
		return ans;
	}
}seg;
struct firBIT{//二维数点用
	int sum[V+10];
	void chp(int x,int v){for(;x<=V;x+=lb(x)) sum[x]+=v;}
	int query(int x){int ans=0;for(;x;x-=lb(x)) ans+=sum[x];return ans;}
	int query(int x,int y){return query(y)-query(x-1);}
}fbit;
struct BIT{//三维数点用
	void chp(int x,int ia,int iv){
		for(;x<=V;x+=lb(x)) seg.chp(root[x],ia,iv,0,n);
	}
	int query(int x,int ia,int ib){
		int ans=0;
		for(;x;x-=lb(x)) ans+=seg.query(root[x],ia,ib,0,n);
		return ans;
	}
	int query(int x,int y,int ia,int ib){
		return query(y,ia,ib)-query(x-1,ia,ib);
	}
}bit;
void solve(){
	int t=0;
	for(int i=1;i<=idx;i++){
		while(t+1<=q[i].p) ++t,bit.chp(a[t],pre[t],1),fbit.chp(a[t],1),CNT++;
		ans[q[i].id][0]+=q[i].v*fbit.query(q[i].oa,q[i].ob);
		ans[q[i].id][1]+=q[i].v* bit.query(q[i].oa,q[i].ob,0,q[i].ib);
	}
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i],pre[i]=pos[a[i]],pos[a[i]]=i;
	for(int i=1,l,r,a,b;i<=m;i++){
		cin>>l>>r>>a>>b;// ↓ 一个询问拆成俩
		q[++idx]={i, -1, l-1, a, b, l-1};
		q[++idx]={i,  1,   r, a, b, l-1};
	}
	sort(q+1,q+1+idx,[](Query a,Query b){return a.p<b.p;});
	solve();
	for(int i=1;i<=m;i++) cout<<ans[i][0]<<" "<<ans[i][1]<<"\n";
	return 0;
}

CF1093E Intersection of Permutations

很好玩的思维题。

注意到 \(a,b\) 都是 \(1,2,\dots,n\) 的排列,因此我们可以将 \(a_{i}\) 映射到 \(i\)\(b\) 也随之发生变化。显然这样子不会影响之后的查询。

这样求 \(|S_a\cap S_b|\),其实就是求 \(S_b\)\([l_a,r_a]\) 值域上的元素个数,使用树套树即可解决。


另外,这道题不进行任何优化,节点总数达到 \((n+2m)\log^2 n\),空间会超限。因此我们可以使用节点回收来重复利用不需要的点(本题中,不需要的点就是被 sum 被减到 \(0\) 的点),节点总数降为 \(n\log^2 n\)

虽然受空间限制仍然不能开到 \(n\log^2 n\),但是由于我们充分利用了之前的节点,并且实际情况下节点总数远小于理论上界,所以放心,不会有问题的 (^^;

代码中的 newnode()del() 都是垃圾回收,原理见代码。

遇到节点数量不够用的题,节点回收是一个很好的习惯~

时间复杂度 \(O((n+m)\log^2 n)\)

节点总数 \(n\log^2 n\)

点击查看代码 #327886986
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,m,a[N],b[N],to[N],root[N];
inline int lowbit(int x){return x&-x;}
struct SEG{
	stack<int> gar;
	int idx;
	struct node{
		int lc,rc,sum;
		void init(){*this={0,0,0};}
	}tr[N*100];//虽然还是开不下Nlog^2 N,但是很难卡到空间上界
	#define lc(x) (tr[x].lc)
	#define rc(x) (tr[x].rc)
	#define sum(x) (tr[x].sum)
	void del(int &x){gar.push(x),x=0;}
	void newnode(int &x){
		if(!gar.empty()) x=gar.top(),gar.pop(),tr[x].init();
		else x=++idx;
	}
	void chp(int &x,int a,int v,int l,int r){
		if(!x) newnode(x);
		sum(x)+=v;
		if(l<r){
			int mid=(l+r)>>1;
			if(a<=mid) chp(lc(x),a,v,l,mid);
			else chp(rc(x),a,v,mid+1,r);
		}
		if(!sum(x)) del(x);
	}
	int query(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return sum(x);
		int mid=(l+r)>>1,ans=0;
		if(a<=mid) ans+=query(lc(x),a,b,l,mid);
		if(b>mid) ans+=query(rc(x),a,b,mid+1,r);
		return ans;
	}
}tr;
struct BIT{
	void chp(int x,int a,int v){
		while(x<=n) tr.chp(root[x],a,v,1,n),x+=lowbit(x);
	}
	int query(int x,int a,int b){
		int ans=0;
		while(x) ans+=tr.query(root[x],a,b,1,n),x-=lowbit(x);
		return ans;
	}
}bit;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i],to[a[i]]=i;
	for(int i=1;i<=n;i++) cin>>b[i],b[i]=to[b[i]],bit.chp(i,b[i],1);
	int op,la,ra,lb,rb,x,y;
	while(m--){
		cin>>op;
		if(op==1){
			cin>>la>>ra>>lb>>rb;
			cout<<bit.query(rb,la,ra)-bit.query(lb-1,la,ra)<<"\n";
		}else{
			cin>>x>>y;
			bit.chp(x,b[x],-1),bit.chp(y,b[y],-1);//先删
			swap(b[x],b[y]);
			bit.chp(x,b[x],1),bit.chp(y,b[y],1);//后加
		}
	}
	return 0;
}

P2617 Dynamic Rankings

该题一改之前的区间求和,变成了区间第 \(k\) 小。

发现不能按上题的方法做了,因为区间第 \(k\) 小不能在从线段树每次的查询结果合并而来,而必须在作差后得到的线段树上完成。

当然也不能将所有节点都求出来,否则单次时间复杂度就带一个 \(O(n)\) 了。

只要现用现求,线段树遍历到哪个节点计算哪个节点就好,具体见代码。

int cnt[2],t[20][2];
struct SEG{
	......
	int query(int k,int l,int r){
		if(l==r) return l;
		int mid=(l+r)>>1,sum=0;
		for(int i=1;i<=cnt[1];i++) sum+=sum(lc(t[i][1]));
		for(int i=1;i<=cnt[0];i++) sum-=sum(lc(t[i][0]));
		if(k<=sum){//左子树的sum>=k,说明第k名在左子树
			for(int i=1;i<=cnt[1];i++) t[i][1]=lc(t[i][1]);
			for(int i=1;i<=cnt[0];i++) t[i][0]=lc(t[i][0]);
			return query(k,l,mid);
		}else{//否则,第k名在右子树
			for(int i=1;i<=cnt[1];i++) t[i][1]=rc(t[i][1]);
			for(int i=1;i<=cnt[0];i++) t[i][0]=rc(t[i][0]);
			return query(k-sum,mid+1,r);
		}
	}
}
struct BIT{
	......
	int query(int k,int l,int r){
		cnt[0]=cnt[1]=0,l--;//t[][0]为要减的节点,t[][1]为要加的节点
		while(r) t[++cnt[1]][1]=root[r],r-=lowbit(r);
		while(l) t[++cnt[0]][0]=root[l],l-=lowbit(l);
		return tr.query(k,1,tn);
	}
}

注意离散化。

时间复杂度 \(O((n+m)\log^2 n)\)

节点总数 \(n\log^2 n\)

点击查看代码 R222231320
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,Q=1e5+10;
int n,q,a[N],tmp[N+Q],idx,tn,root[N];
int cnt[2],t[20][2];
unordered_map<int,int> ma;
inline int lowbit(int x){return x&-x;}
struct Ope{char c;int l,r,k;}op[Q];
struct SEG{
	int idx;
	struct node{int lc,rc,sum;}tr[N*18*18];
	#define lc(x) (tr[x].lc)
	#define rc(x) (tr[x].rc)
	#define sum(x) (tr[x].sum)
	void chp(int &x,int a,int v,int l,int r){
		if(!x) x=++idx;
		sum(x)+=v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc(x),a,v,l,mid);
		else chp(rc(x),a,v,mid+1,r);
	}
	int query(int k,int l,int r){
		if(l==r) return l;
		int mid=(l+r)>>1,sum=0;
		for(int i=1;i<=cnt[1];i++) sum+=sum(lc(t[i][1]));
		for(int i=1;i<=cnt[0];i++) sum-=sum(lc(t[i][0]));
		if(k<=sum){
			for(int i=1;i<=cnt[1];i++) t[i][1]=lc(t[i][1]);
			for(int i=1;i<=cnt[0];i++) t[i][0]=lc(t[i][0]);
			return query(k,l,mid);
		}else{
			for(int i=1;i<=cnt[1];i++) t[i][1]=rc(t[i][1]);
			for(int i=1;i<=cnt[0];i++) t[i][0]=rc(t[i][0]);
			return query(k-sum,mid+1,r);
		}
	}
}tr;
struct BIT{
	void chp(int x,int a,int v){//第x棵线段树第pos位+v 
		while(x<=n) tr.chp(root[x],a,v,1,tn),x+=lowbit(x);
	}
	int query(int k,int l,int r){
		cnt[0]=cnt[1]=0,l--;
		while(r) t[++cnt[1]][1]=root[r],r-=lowbit(r);
		while(l) t[++cnt[0]][0]=root[l],l-=lowbit(l);
		return tr.query(k,1,tn);
	}
}bit;
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;i++) cin>>a[i],tmp[++idx]=a[i];
	for(int i=1;i<=q;i++){
		cin>>op[i].c;
		if(op[i].c=='Q') cin>>op[i].l>>op[i].r>>op[i].k;
		else{
			cin>>op[i].l>>op[i].k;
			tmp[++idx]=op[i].k;
		}
	}
	sort(tmp+1,tmp+1+idx);
	tn=unique(tmp+1,tmp+idx+1)-tmp-1;
	for(int i=1;i<=tn;i++) ma[tmp[i]]=i;
	for(int i=1;i<=n;i++) a[i]=ma[a[i]],bit.chp(i,a[i],1);
	for(int i=1;i<=q;i++){
		if(op[i].c=='Q'){
			cout<<tmp[bit.query(op[i].k,op[i].l,op[i].r)]<<"\n";
		}else{
			bit.chp(op[i].l,a[op[i].l],-1);
			a[op[i].l]=ma[op[i].k];
			bit.chp(op[i].l,a[op[i].l],1);
		}
	}
	return 0;
}

P3380 【模板】树套树

查询第 \(k\) 名和 Dynamic Ranking 相同。

查询值 \(k\) 的排名,其实就是查权值线段树 \([1,k)\) 的和,再 \(+1\)

查询 \(k\) 的前驱、后继,用前两个操作组合起来即可完成,具体见下面的代码,并不难理解。

点击查看代码 R222738377
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+10,Q=5e4+10,inf=INT_MAX;
int n,tn,q,a[N],b[N+Q],idx,root[N];
int t[N][2],cnt[2];
unordered_map<int,int> ma;
struct Que{
	int op,l,r,k;
}que[Q];
inline int lowbit(int x){return x&-x;}
struct SEG{
	int idx;
	struct node{int lc,rc,sum;}tr[(N+Q)*17*17];
	#define lc(x) (tr[x].lc)
	#define rc(x) (tr[x].rc)
	#define sum(x) (tr[x].sum)
	void chp(int &x,int a,int v,int l,int r){
		if(!x) x=++idx;
		sum(x)+=v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc(x),a,v,l,mid);
		else chp(rc(x),a,v,mid+1,r);
	}
	int query(int x,int a,int l,int r){
		if(l==r) return sum(x);
		int mid=(l+r)>>1;
		if(a<=mid) return query(lc(x),a,l,mid);
		else return query(rc(x),a,mid+1,r);
	}
	int kth(int k,int l,int r){//第k个1在哪个下标
		if(l==r) return l;
		int mid=(l+r)>>1,z=0;
		for(int i=1;i<=cnt[1];i++) z+=sum(lc(t[i][1]));
		for(int i=1;i<=cnt[0];i++) z-=sum(lc(t[i][0]));
		if(k<=z){
			for(int i=1;i<=cnt[1];i++) t[i][1]=lc(t[i][1]);
			for(int i=1;i<=cnt[0];i++) t[i][0]=lc(t[i][0]);
			return kth(k,l,mid);
		}else{
			for(int i=1;i<=cnt[1];i++) t[i][1]=rc(t[i][1]);
			for(int i=1;i<=cnt[0];i++) t[i][0]=rc(t[i][0]);
			return kth(k-z,mid+1,r);
		}
	}
	int ran(int k,int l,int r){//下标在[1,k)的元素总和,实际排名还要+1
		if(l==r) return 0;
		int mid=(l+r)>>1;
		if(k<=mid){
			for(int i=1;i<=cnt[1];i++) t[i][1]=lc(t[i][1]);
			for(int i=1;i<=cnt[0];i++) t[i][0]=lc(t[i][0]);
			return ran(k,l,mid);
		}else{
			int z=0;
			for(int i=1;i<=cnt[1];i++) z+=sum(lc(t[i][1])),t[i][1]=rc(t[i][1]);
			for(int i=1;i<=cnt[0];i++) z-=sum(lc(t[i][0])),t[i][0]=rc(t[i][0]);
			return z+ran(k,mid+1,r);
		}
	}
}tr;
struct BIT{
	void chp(int x,int a,int v){
		while(x<=n) tr.chp(root[x],a,v,1,tn),x+=lowbit(x);
	}
	void init(int l,int r){
		cnt[0]=cnt[1]=0,l--;
		while(l) t[++cnt[0]][0]=root[l],l-=lowbit(l);
		while(r) t[++cnt[1]][1]=root[r],r-=lowbit(r);
	}
	int kth(int k,int l,int r){return init(l,r),b[tr.kth(k,1,tn)];}
	int ran(int k,int l,int r){return init(l,r),tr.ran(k,1,tn)+1;}
	int pre(int k,int l,int r){
		int z=ran(k,l,r)-1;//k所在的名次-1,对应的值就是k的前驱了
		return z?kth(z,l,r):-inf;
	}
	int nxt(int k,int l,int r){
		if(k==tn) return inf;//ran的返回值始终在[1,n]之间,所以必须特判
		int z=ran(k+1,l,r);
		return z<=r-l+1?kth(z,l,r):inf;
	}
}bit;
signed main(){
	cin>>n>>q;
	for(int i=1;i<=n;i++) cin>>a[i],b[++idx]=a[i];
	for(int i=1,op,l,r,k;i<=q;i++){
		l=r=k=0;
		cin>>op;
		if(op==3) cin>>l>>k;
		else cin>>l>>r>>k;
		if(op!=2) b[++idx]=k;
		que[i]={op,l,r,k};
	}
	sort(b+1,b+1+idx);
	tn=unique(b+1,b+1+idx)-b-1;
	for(int i=1;i<=tn;i++) ma[b[i]]=i;
	for(int i=1;i<=n;i++) a[i]=ma[a[i]],bit.chp(i,a[i],1);
	for(int i=1;i<=q;i++){
		auto [op,l,r,k]=que[i];
		if(op==3){
			bit.chp(l,a[l],-1);
			a[l]=ma[k];
			bit.chp(l,a[l],1);
		}else if(op==1) cout<<bit.ran(ma[k],l,r)<<"\n";
		else if(op==2) cout<<bit.kth(k,l,r)<<"\n";
		else if(op==4) cout<<bit.pre(ma[k],l,r)<<"\n";
		else cout<<bit.nxt(ma[k],l,r)<<"\n";
	}
	return 0;
}

P4175 [CTSC2008] 网络管理

此题大致有 \(2\) 种解法。

树链剖分

树上路径维护首先想到树剖。

用树剖将要操作的路径分成最多 \(\log n\) 段连续的区间,然后再用树套树来维护这些区间对应线段树的动态第 \(k\) 大。

有点毒瘤的说,好想但是不好写(我只能想到这个了 w),用来练手还是很不错的。

时间复杂度 \(O((n+m)\log^3 n)\)

节点总数 \(n\log^3 n\)

点击查看代码 R223183396
#include<bits/stdc++.h>
using namespace std;
const int N=8e4+10,Q=8e4+10;
int n,tn,q,idx,head[N],w[N],dc[N+Q];
int dep[N],fa[N],siz[N],son[N],top[N],dfn[N],tim;
int root[N],cnt[2],t[N][2];//t: nlog^2 n 
unordered_map<int,int> ma;
inline int lowbit(int x){return x&-x;}
struct Que{int k,a,b;}qu[Q];
struct SEG{
	stack<int> gar;
	int idx;
	struct node{
		int lc,rc,sum;
		void init(){*this={0,0,0};}
	}tr[200*N];//tr: nlog^3 n
	#define lc(x) (tr[x].lc)
	#define rc(x) (tr[x].rc)
	#define sum(x) (tr[x].sum)
	void del(int &x){gar.push(x),x=0;}
	void newnode(int &x){
		if(!gar.empty()) x=gar.top(),gar.pop(),tr[x].init();
		else x=++idx;
	}
	void chp(int &x,int a,int v,int l,int r){
		if(!x) newnode(x);
		sum(x)+=v;
		if(l<r){
			int mid=(l+r)>>1;
			if(a<=mid) chp(lc(x),a,v,l,mid);
			else chp(rc(x),a,v,mid+1,r);
		}
		if(!sum(x)) del(x);
	}
	int kth(int k,int l,int r){
		if(l==r) return l;
		int mid=(l+r)>>1,z=0;
		for(int i=1;i<=cnt[1];i++) z+=sum(rc(t[i][1]));
		for(int i=1;i<=cnt[0];i++) z-=sum(rc(t[i][0]));
		if(k<=z){
			for(int i=1;i<=cnt[1];i++) t[i][1]=rc(t[i][1]);
			for(int i=1;i<=cnt[0];i++) t[i][0]=rc(t[i][0]);
			return kth(k,mid+1,r);
		}else{
			for(int i=1;i<=cnt[1];i++) t[i][1]=lc(t[i][1]);
			for(int i=1;i<=cnt[0];i++) t[i][0]=lc(t[i][0]);
			return kth(k-z,l,mid);
		}
	}
}tr;
struct BIT{
	void chp(int x,int a,int v){
		while(x<=n) tr.chp(root[x],a,v,1,tn),x+=lowbit(x);
	}
}bit;
void clearT(){cnt[0]=cnt[1]=0;}
void addT(int l,int r){
	l--;
	while(r) t[++cnt[1]][1]=root[r],r-=lowbit(r);
	while(l) t[++cnt[0]][0]=root[l],l-=lowbit(l);
}
struct edge{int nxt,to;}e[N<<1];
void add(int u,int v){e[++idx]={head[u],v},head[u]=idx;}
void dfs1(int u){
	dep[u]=dep[fa[u]]+1,siz[u]=1;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa[u]) continue;
		fa[v]=u,dfs1(v),siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
void dfs2(int u,int t){
	top[u]=t,dfn[u]=++tim;
	if(son[u]) dfs2(son[u],t);
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v!=son[u]&&v!=fa[u]) dfs2(v,v);
	}
}
int qpath(int u,int v){//将u~v路径上的加减情况存储在t中,并返回u,v间的节点数量
	int sum=0;
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]]) swap(u,v);
		addT(dfn[top[u]],dfn[u]),sum+=dfn[u]-dfn[top[u]]+1;
		u=fa[top[u]];
	}
	if(dep[u]<dep[v]) swap(u,v);
	addT(dfn[v],dfn[u]);
	return sum+dfn[u]-dfn[v]+1;
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;i++) cin>>w[i],dc[++tn]=w[i];
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		add(u,v),add(v,u);
	}
	dfs1(1),dfs2(1,1);
	for(int i=1;i<=q;i++){
		cin>>qu[i].k>>qu[i].a>>qu[i].b;
		if(!qu[i].k) dc[++tn]=qu[i].b;
	}
	sort(dc+1,dc+1+tn);
	tn=unique(dc+1,dc+1+tn)-dc-1;
	for(int i=1;i<=tn;i++) ma[dc[i]]=i;
	for(int i=1;i<=n;i++) w[i]=ma[w[i]],bit.chp(dfn[i],w[i],1);
	for(int i=1;i<=q;i++){
		auto [k,a,b]=qu[i];
		if(!k){
			bit.chp(dfn[a],w[a],-1);
			w[a]=ma[b];
			bit.chp(dfn[a],w[a],1);
		}else{
			clearT();
			if(qpath(a,b)<k) cout<<"invalid request!\n";
			else cout<<dc[tr.kth(k,1,tn)]<<"\n";
		}
	}
	return 0;
}

树上差分

参考了此题解 by jiqimao(实现真的非常优雅)。

要是这道题没有点修,我们其实根本不会瞥树剖一眼的,而是选择使用树上差分来维护一条路径的信息(这里的信息,其实就是一棵权值线段树啦),即:

\(u\)\(v\) 的路径信息(拿点差分来举例),相当于:

  • \(\ \ \ \ u\) 到根的路径信息
  • \(+\ v\) 到根的路径信息
  • \(-\ \text{LCA}(u,v)\) 到根的路径信息
  • \(-\ fa_{\text{LCA}(u,v)}\) 到根的路径信息

现在有了点修操作,我们真的要臣服于树剖了吗?

实际上,我们如果已经记录了每个点到根节点的路径信息。那么对 \(u\) 进行点修,所影响的只有 \(u\) 子树中 点到根的路径信息。

我们将节点按 DFS 序编号,这样子树中节点的编号就是连续的。

因此外层的树状数组需要支持区间修改,单点查询。通过维护原序列的差分数组即可做到,可以参考此文 by RabbitHu。

内层正常跑动态开点的权值线段树就可以了。

时间复杂度 \(O((n+m)\log^2 n)\)

节点总数 \(n\log^2 n\)

点击查看代码 R223183057
#include<bits/stdc++.h>
using namespace std;
const int N=8e4+10,Q=8e4+10;
int n,tn,q,idx,head[N],w[N],dc[N+Q],root[N];
int dep[N],fa[N][17],dfn[N],tim,rr[N];
unordered_map<int,int> ma;
inline int lowbit(int x){return x&-x;}
struct Que{int k,a,b;}qu[Q];
struct edge{int nxt,to;}e[N<<1];
void add(int u,int v){e[++idx]={head[u],v},head[u]=idx;}
void dfs(int u,int ff){
	fa[u][0]=ff,dep[u]=dep[ff]+1,dfn[u]=++tim;
	for(int i=0;i<16;i++) fa[u][i+1]=fa[fa[u][i]][i];
	for(int i=head[u];i;i=e[i].nxt) if(e[i].to!=ff) dfs(e[i].to,u);
	rr[u]=tim;//子树u的时间戳:dfn[u]~rr[u]
}
int lca(int u,int v){
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=16;~i;i--) if(dep[fa[u][i]]>=dep[v]) u=fa[u][i];
	if(u==v) return u;
	for(int i=16;~i;i--) if(fa[u][i]!=fa[v][i]) u=fa[u][i],v=fa[v][i];
	return fa[u][0];
}
struct node{int lc,rc,sum;}tr[200*N];//nlog^2 n
#define lc(x) (tr[x].lc)
#define rc(x) (tr[x].rc)
#define sum(x) (tr[x].sum)
struct group{
	int n,a[N];
	int calc(){int ans=0;for(int i=1;i<=n;i++) ans+=sum(rc(a[i]));return ans;}
	void L(){for(int i=1;i<=n;i++) a[i]=lc(a[i]);}
	void R(){for(int i=1;i<=n;i++) a[i]=rc(a[i]);}
	void init(int x){n=0;for(;x;x-=lowbit(x)) a[++n]=root[x];}
}A,B,C,D;
struct SEG{
	int idx;
	void chp(int& x,int a,int v,int l,int r){
		if(!x) x=++idx;
		sum(x)+=v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc(x),a,v,l,mid);
		else chp(rc(x),a,v,mid+1,r);
	}
	int kth(int k,int l,int r){
		if(l==r) return l;
		int mid=(l+r)>>1,z=A.calc()+B.calc()-C.calc()-D.calc();
		if(k<=z) return A.R(),B.R(),C.R(),D.R(),kth(k,mid+1,r);
		else return A.L(),B.L(),C.L(),D.L(),kth(k-z,l,mid);
	}
}seg;
struct BIT{
	void chp(int x,int a,int v){
		while(x<=n) seg.chp(root[x],a,v,1,tn),x+=lowbit(x);
	}
	void chr(int x,int y,int a,int v){
		chp(x,a,v),chp(y+1,a,-v);
	}
}bit;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;i++) cin>>w[i],dc[++tn]=w[i];
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		add(u,v),add(v,u);
	}
	dfs(1,0);
	for(int i=1;i<=q;i++){
		cin>>qu[i].k>>qu[i].a>>qu[i].b;
		if(!qu[i].k) dc[++tn]=qu[i].b;
	}
	sort(dc+1,dc+1+tn);
	tn=unique(dc+1,dc+1+tn)-dc-1;
	for(int i=1;i<=tn;i++) ma[dc[i]]=i;
	for(int i=1;i<=n;i++) w[i]=ma[w[i]],bit.chr(dfn[i],rr[i],w[i],1);
	for(int i=1;i<=q;i++){
		auto [k,a,b]=qu[i];
		if(!k){
			bit.chr(dfn[a],rr[a],w[a],-1);
			bit.chr(dfn[a],rr[a],w[a]=ma[b],1);
		}else{
			int z=lca(a,b);
			if(dep[a]+dep[b]-2*dep[z]+1<k) cout<<"invalid request!\n";
			else A.init(dfn[a]),B.init(dfn[b]),C.init(dfn[z]),D.init(dfn[fa[z][0]]),
				cout<<dc[seg.kth(k,1,tn)]<<"\n";
		}
	}
	return 0;
}

P3332 [ZJOI2013] K 大数查询

根据题意,我们知道:

  • 维护下标的结构需要支持:区间修改、区间查询。
  • 维护值域的结构需要支持:单点修改、区间 k-th。

这和之前是不一样的。因为此前的题都是对同一个下标进行操作,这样就算需要区间修改,也是在内层结构上进行的;而这道题则是一次性对多个下标进行操作。

由此,写这道题我们大致有 \(2\) 个方向。

内外反套

实际上反转一下数对的顺序,不影响我们解决二维偏序问题。

放在此题,就是用外层的树状数组来维护值域,内层线段树来维护下标。

不过这就需要我们的树状数组支持区间 k-th,这就需要树状数组上倍增

查询第 \(k\) 小的代码如下。

int kth(int k,int a,int b){//即寻找最小的x使得a[1~x]>=k
	int x=0,sum=0;
	for(int i=1<<__lg(n);i;i>>=1){//1<<__lg(n)即n的highbit
		if(x+i>n) continue;
		int f=a[i]+sum;
		if(f<k) x+=i,sum=f;
	}
	return x+1;
}

为了减少常数,内层线段树的区间操作建议使用标记永久化

#1 Code R223706490
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+10,M=5e4+10;
int n,m,root[M],b[M],tn;
unordered_map<int,int> ma;
struct Que{int op,l,r,c;}q[M];
inline int lowbit(int x){return x&-x;}
struct SEG{
	int idx;
	struct node{int lc,rc,sum,tag;}tr[300*M];//MlogMlogN
	#define lc(x) (tr[x].lc)
	#define rc(x) (tr[x].rc)
	#define sum(x) (tr[x].sum)
	#define tag(x) (tr[x].tag)
	int clone(int x){return tr[++idx]=tr[x],idx;}
	void chr(int &x,int a,int b,int v,int l,int r){
		x=clone(x);
		sum(x)+=(b-a+1)*v;
		if(a==l&&b==r) return tag(x)+=v,void();
		int mid=(l+r)>>1;
		if(b<=mid) chr(lc(x),a,b,v,l,mid);
		else if(a>mid) chr(rc(x),a,b,v,mid+1,r);
		else chr(lc(x),a,mid,v,l,mid),chr(rc(x),mid+1,b,v,mid+1,r);
	}
	int query(int x,int a,int b,int l,int r){
		if(a==l&&b==r) return sum(x);
		int mid=(l+r)>>1,sum=(b-a+1)*tag(x);
		if(b<=mid) return sum+query(lc(x),a,b,l,mid);
		else if(a>mid) return sum+query(rc(x),a,b,mid+1,r);
		else return sum+query(lc(x),a,mid,l,mid)+query(rc(x),mid+1,b,mid+1,r);
	}
}seg;
struct BIT{
	void chp(int x,int ia,int ib,int iv){
		for(;x<=tn;x+=lowbit(x)) seg.chr(root[x],ia,ib,iv,1,n);
	}
	int kth(int k,int a,int b){//由于已经从大到小排序,所以实际上是求第k小
		int x=0,sum=0;//即寻找最小的x使得a[1~x]>=k
		for(int i=1<<__lg(tn);i;i>>=1){
			if(x+i>tn) continue;
			int f=seg.query(root[x+i],a,b,1,n)+sum;
			if(f<k) x+=i,sum=f;
		}
		return x+1;
	}
}bit;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>q[i].op>>q[i].l>>q[i].r>>q[i].c;
		if(q[i].op==1) b[++tn]=q[i].c;
	}
	sort(b+1,b+1+tn),tn=unique(b+1,b+1+tn)-b-1;
	reverse(b+1,b+1+tn);//从大到小
	for(int i=1;i<=tn;i++) ma[b[i]]=i;
	for(int i=1;i<=m;i++){
		if(q[i].op==1) bit.chp(ma[q[i].c],q[i].l,q[i].r,1);
		else cout<<b[bit.kth(q[i].c,q[i].l,q[i].r)]<<"\n";
	}
	return 0;
}

外层写线段树也是可以的,毕竟线段树上二分求 k-th 还是很常见的。

#2 Code R223707237
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+10,M=5e4+10;
int n,m,root[M<<1],b[M],tn;//(M<<1)是因为线段树用2倍节点
unordered_map<int,int> ma;
struct Que{int op,l,r,c;}q[M];
struct innerSEG{
	int idx;
	struct node{int lc,rc,sum,tag;}tr[500*M];//2*MlogMlogN,*2是因为线段树用2倍节点
	inline int& lc(int x){return tr[x].lc;}
	inline int& rc(int x){return tr[x].rc;}
	inline int& sum(int x){return tr[x].sum;}
	inline int& tag(int x){return tr[x].tag;}
	int clone(int x){return tr[++idx]=tr[x],idx;}
	void chr(int &x,int a,int b,int v,int l,int r){
		x=clone(x);
		sum(x)+=(b-a+1)*v;
		if(a==l&&b==r) return tag(x)+=v,void();
		int mid=(l+r)>>1;
		if(b<=mid) chr(lc(x),a,b,v,l,mid);
		else if(a>mid) chr(rc(x),a,b,v,mid+1,r);
		else chr(lc(x),a,mid,v,l,mid),chr(rc(x),mid+1,b,v,mid+1,r);
	}
	int query(int x,int a,int b,int l,int r){
		if(a==l&&b==r) return sum(x);
		int mid=(l+r)>>1,sum=(b-a+1)*tag(x);
		if(b<=mid) return sum+query(lc(x),a,b,l,mid);
		else if(a>mid) return sum+query(rc(x),a,b,mid+1,r);
		else return sum+query(lc(x),a,mid,l,mid)+query(rc(x),mid+1,b,mid+1,r);
	}
}in;
struct outerSEG{
	inline int lc(int x){return x<<1;}
	inline int rc(int x){return x<<1|1;}
	void chp(int x,int a,int ia,int ib,int l,int r){
		in.chr(root[x],ia,ib,1,1,n);
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc(x),a,ia,ib,l,mid);
		else chp(rc(x),a,ia,ib,mid+1,r);
	}
	int kth(int x,int k,int ia,int ib,int l,int r){
		if(l==r) return l;
		int mid=(l+r)>>1,z=in.query(root[rc(x)],ia,ib,1,n);
		if(k<=z) return kth(rc(x),k,ia,ib,mid+1,r);
		else return kth(lc(x),k-z,ia,ib,l,mid);
	}
}out;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>q[i].op>>q[i].l>>q[i].r>>q[i].c;
		if(q[i].op==1) b[++tn]=q[i].c;
	}
	sort(b+1,b+1+tn),tn=unique(b+1,b+1+tn)-b-1;
	for(int i=1;i<=tn;i++) ma[b[i]]=i;
	for(int i=1;i<=m;i++){
		if(q[i].op==1) out.chp(1,ma[q[i].c],q[i].l,q[i].r,1,tn);
		else cout<<b[out.kth(1,q[i].c,q[i].l,q[i].r,1,tn)]<<"\n";
	}
	return 0;
}

使用支持区修、区查的外层结构

如果我们还是选择使用外层维护下标,也是可以做的。

虽然说一般的树状数组仅支持“点修 + 区查”,但我仍然可以利用刚才维护差分序列的思路,让我们的树状数组支持“区修 + 区查”。具体来说(仍然参考此文 by RabbitHu):

\(d\)\(a\) 的差分数组,则有:

\[\begin{aligned} \sum\limits_{i=1}^x a[i] &=\sum\limits_{i=1}^x \sum\limits_{j=1}^ i d[j]\\ &=\sum\limits_{i=1}^x (x-i+1)\times d[i]\\ &=(x+1)\sum\limits_{i=1}^x d[i]-\sum\limits_{i=1}^x i\times d[i] \end{aligned}\]

因此在上文维护 \(d[i]\) 的基础上,额外开一个数组维护 \(d[i]\times i\) 即可。即:

void add(ll p, ll x){
	for(int i = p; i \le n; i += i & -i)
		sum1[i] += x, sum2[i] += x \times p;
}
void range_add(ll l, ll r, ll x){
	add(l, x), add(r + 1, -x);
}
ll ask(ll p){
	ll res = 0;
	for(int i = p; i; i -= i & -i)
		res += (p + 1) \times sum1[i] - sum2[i];
	return res;
}
ll range_ask(ll l, ll r){
	return ask(r) - ask(l - 1);
}

\[\]

于是我们就可以愉快地搓树状数组套权值线段树了。

#3 Code R223707459
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+10,M=5e4+10;
int n,m,root[N<<1],b[M],tn;//(N<<1)是因为树状数组要维护两个sum[],见题解
unordered_map<int,int> ma;
inline int lowbit(int x){return x&-x;}
struct Que{int op,l,r,c;}q[M];
struct node{int lc,rc,sum;}tr[300*N];//MlogNlogM
#define lc(x) (tr[x].lc)
#define rc(x) (tr[x].rc)
#define sum(x) (tr[x].sum)
struct group{
	int n,a[80],v[80];//4logN
	int calc(){int ans=0;for(int i=1;i<=n;i++) ans+=v[i]*sum(rc(a[i]));return ans;}
	void L(){for(int i=1;i<=n;i++) a[i]=lc(a[i]);}
	void R(){for(int i=1;i<=n;i++) a[i]=rc(a[i]);}
}A;
struct SEG{
	int idx;
	void chp(int &x,int a,int v,int l,int r){
		if(!x) x=++idx;
		sum(x)+=v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc(x),a,v,l,mid);
		else chp(rc(x),a,v,mid+1,r);
	}
	int kth(int k,int l,int r){
		if(l==r) return l;
		int mid=(l+r)>>1,z=A.calc();
		if(k<=z) return A.R(),kth(k,mid+1,r);
		else return A.L(),kth(k-z,l,mid);
	}
}seg;
struct BIT{
	void chp(int x,int ia,int iv){
		for(int i=x;i<=n;i+=lowbit(i)) seg.chp(root[i],ia,iv,1,tn),seg.chp(root[i+N],ia,iv*x,1,tn);
	}
	void chr(int x,int y,int ia,int iv){
		chp(x,ia,iv),chp(y+1,ia,-iv);
	}
	int query(int x,int y,int ik){
		A.n=0,x--;
		for(int i=y;i;i-=lowbit(i)) A.v[++A.n]=y+1,A.a[A.n]=root[i],A.v[++A.n]=-1,A.a[A.n]=root[i+N];
		for(int i=x;i;i-=lowbit(i)) A.v[++A.n]=-(x+1),A.a[A.n]=root[i],A.v[++A.n]=1,A.a[A.n]=root[i+N];
		return seg.kth(ik,1,tn);
	}
}bit;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>q[i].op>>q[i].l>>q[i].r>>q[i].c;
		if(q[i].op==1) b[++tn]=q[i].c;
	}
	sort(b+1,b+1+tn),tn=unique(b+1,b+1+tn)-b-1;
	for(int i=1;i<=tn;i++) ma[b[i]]=i;
	for(int i=1;i<=m;i++){
		if(q[i].op==1) bit.chr(q[i].l,q[i].r,ma[q[i].c],1);
		else cout<<b[bit.query(q[i].l,q[i].r,q[i].c)]<<"\n";
	}
	return 0;
}

本来还考虑外层换成线段树试试,不过区修有点难以应对,因为操作的不是数而是一棵棵内层线段树,所以标记不能叠加要一个个下放,就算延迟下放也不能降低时间复杂度;标记永久化也无法使用。遂放弃之。


时间复杂度 \(O(m\log m\log n)\)

节点总数 \(m\log m\log n\)


下图是三种代码实现的比较(代码大小不含注释):image

最后一种很显著地快啊……可能是因为大部分操作都丢给树状数组,线段树不需要维护区间操作的原因。

由此也可见,树状数组比线段树还是快了不少的,而且区修 + 区查 + 区 k-th 这些操作树状数组都能做到。除了不能动态开点,所以只能套在外面(否则空间会炸),在维护基础操作方面,绝对碾压线段树好伐!

P3242 [HNOI2015] 接水果

根据题意,水果 \((x,y)\) 能被盘子 \((u,v)\) 接住 \(\iff\) 路径 \((x,y)\) 包含路径 \((u,v)\)

因此考虑 \((x,y)\) 什么满足什么情况时包含 \((u,v)\)

定义 \(L_i\)\(i\) 的 DFS 序,\(R_i\) 为子树 \(i\) 中最大的 DFS 序。

不妨令 \(L_u<L_v\)\(L_x<L_y\)

则进行分类讨论:

  • \(\text{LCA}(u,v)\neq u\)

    显然 \(x\) 在子树 \(u\) 中,\(y\) 在子树 \(v\) 中,即 \(L_x\in[L_u,R_u],L_y\in[L_v,R_v]\)

  • \(\text{LCA}(u,v)=u\)

    \(z\)\(u\rightarrow v\) 路径上第二个节点(如下图),则 \(x,y\) 必有一个在子树 \(v\) 中,另一个在子树 \(z\) 外。

    image

    \(L_x\in[1,L_z),L_y\in[L_v,R_v]\),或 \(L_x\in [L_v,R_v],L_y\in (R_z,n]\)

我们发现,若将路径 \((x,y)\) 映射为二维平面上的点 \((L_x,L_y)\),路径 \((u,v)\) 给出的限制看作二维平面上的 \(1\)\(2\) 个矩形的话,\((L_x,L_y)\) 被矩形覆盖 \(\iff\) 路径 \((x,y)\) 包含路径 \((u,v)\)

而且由于第二种情况的 \(2\) 个矩形是不交的,所以不会重复统计贡献。原问题转化为:查询覆盖 \((L_x,L_y)\) 的所有矩形中,权值的第 \(k\) 大是多少。

我们可以用类似扫描线的算法,先为每个 y 坐标建一棵权值线段树,再从左往右扫描竖向的线段 \((l,r)\) 和待查询的点:

  • 遇到左端点则将编号 \([l,r]\) 线段树的 \(w\)\(+1\)
  • 遇到右端点则将编号 \([l,r]\) 线段树的 \(w\)\(-1\)
  • 遇到待查询的点 \((L_x,L_y)\) 时,直接统计编号 \(L_y\) 的线段树的第 \(k\) 小即可。

操作和上道题类似,不过区间查变成单点查了。我写的是 #3 的实现~

点击查看代码 R224022912
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=4e4+10,P=4e4+10,Q=4e4+10;
int n,p,q,tn,eidx,sidx,head[N],b[P],root[N];
int tim,fa[N][17],dep[N],dfn[N],rig[N],ans[Q];
unordered_map<int,int> ma;
struct edge{int nxt,to;}e[N<<1];
struct Border{int x,l,r,p,v;}bor[P<<2];//描述一条纵向线段
struct Query{int u,v,k,id;}qu[Q];
void add(int u,int v){e[++eidx]={head[u],v},head[u]=eidx;}
inline int lb(int x){return x&-x;}
int lc[300*N],rc[300*N],sum[300*N];
struct group{
	int n,a[20];
	void append(int x){a[++n]=x;}
	int calc(){int ans=0;for(int i=1;i<=n;i++) ans+=sum[lc[a[i]]];return ans;}
	void L(){for(int i=1;i<=n;i++) a[i]=lc[a[i]];}
	void R(){for(int i=1;i<=n;i++) a[i]=rc[a[i]];}
}A;
struct SEG{
	int idx;
	void chp(int &x,int a,int v,int l,int r){
		if(!x) x=++idx;
		sum[x]+=v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc[x],a,v,l,mid);
		else chp(rc[x],a,v,mid+1,r);
	}
	int kth(int k,int l,int r){
		if(l==r) return l;
		int mid=(l+r)>>1,z=A.calc();
		if(k<=z) return A.L(),kth(k,l,mid);
		else return A.R(),kth(k-z,mid+1,r);
	}
}seg;
struct BIT{//区修 点查
	void chp(int x,int ia,int iv){
		while(x<=n) seg.chp(root[x],ia,iv,1,tn),x+=lb(x);
	}
	void chr(int x,int y,int ia,int iv){
		chp(x,ia,iv),chp(y+1,ia,-iv);
	}
	int query(int x,int ik){
		A.n=0;
		while(x) A.append(root[x]),x-=lb(x);
		return seg.kth(ik,1,tn);
	}
}bit;
void dfs(int u){
	dfn[u]=++tim,dep[u]=dep[fa[u][0]]+1;
	for(int i=0;i<16;i++) fa[u][i+1]=fa[fa[u][i]][i];
	for(int i=head[u];i;i=e[i].nxt){
		if(e[i].to==fa[u][0]) continue;
		fa[e[i].to][0]=u,dfs(e[i].to);
	}
	rig[u]=tim;
}
int LCA(int u,int v,int& z){
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=16;~i;i--) if(dep[fa[u][i]]>dep[v]) u=fa[u][i];
	if(dep[u]>dep[v]) z=u,u=fa[u][0];
	if(u==v) return u;
	for(int i=16;~i;i--) if(fa[u][i]!=fa[v][i]) u=fa[u][i],v=fa[v][i];
	return fa[u][0];
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>p>>q;
	for(int i=1,u,v;i<n;i++) cin>>u>>v,add(u,v),add(v,u);
	dfs(1);
	for(int i=1,u,v,w,z;i<=p;i++){
		cin>>u>>v>>w,b[++tn]=w;
		if(dfn[u]>dfn[v]) swap(u,v);
		if(LCA(u,v,z)!=u){
			bor[++sidx]={dfn[u],dfn[v],rig[v],w,1};
			bor[++sidx]={rig[u]+1,dfn[v],rig[v],w,-1};
		}else{
			bor[++sidx]={1,dfn[v],rig[v],w,1};
			bor[++sidx]={dfn[z],dfn[v],rig[v],w,-1};
			bor[++sidx]={dfn[v],rig[z]+1,n,w,1};
			bor[++sidx]={rig[v]+1,rig[z]+1,n,w,-1};
		}
	}
	sort(bor+1,bor+1+sidx,[](Border a,Border b){return a.x<b.x;});
	sort(b+1,b+1+tn),tn=unique(b+1,b+1+tn)-b-1;
	for(int i=1;i<=tn;i++) ma[b[i]]=i;
	bor[sidx+1].x=LLONG_MAX;
	for(int i=1;i<=q;i++){
		cin>>qu[i].u>>qu[i].v>>qu[i].k,qu[i].id=i;
		if(dfn[qu[i].u]>dfn[qu[i].v]) swap(qu[i].u,qu[i].v);
	}
	sort(qu+1,qu+1+q,[](Query a,Query b){return dfn[a.u]<dfn[b.u];});
	for(int i=1,p=0;i<=q;i++){
		auto [u,v,k,num]=qu[i];
		while(bor[p+1].x<=dfn[u]) ++p,bit.chr(bor[p].l,bor[p].r,ma[bor[p].p],bor[p].v);
		ans[num]=b[bit.query(dfn[v],k)];
	}
	for(int i=1;i<=q;i++) cout<<ans[i]<<"\n";
	return 0;
}

P4093 [HEOI2016 / TJOI2016] 序列

树套树优化 dp 的例题(不过这方面其实 CDQ 分治用的比较多)。

\(\max x_{i}\)\(a_{i}\) 能变到的最大值,\(\min n_{i}\)\(a_{i}\) 能变到的最小值,\(f_{i}\) 为以 \(a_{i}\) 为结尾的最长不降子序列的长度。则有转移:

\[f_{i}=1+\max\limits_j f_{j} \]

其中 \(j\) 需要满足:

  • \(j<i\)
  • \(\max x_{j}\le a_{i}\)
  • \(a_{j}\le \min n_{i}\)

套路三维数点。通过按 \(1,2,\dots,n\) 的顺序添加节点干点第一维,然后用外层树状数组维护第二条的 \(\max x\),内层线段树维护第三条的 \(a\)

查询结果即为所有满足该条件的 \(j\) 中,\(f_{j}\) 的最大值。

尽管 \(\max\) 不可减,但是每次的查询在树状数组上都是一个前缀,因此不需要做减法。

顺带一提,树状数组维护 \(\max\) 有一定的局限性,比如只能支持前缀 / 后缀查询,且如果要支持点修,每次修改必须大于原位置的值。

时间复杂度 \(O(n\log^2 n)\)

节点总数 \(n\log^2 n\)

点击查看代码 R223759174
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=1e5+10;
int n,m,a[N],minn[N],maxx[N],root[N],f[N];
inline int lb(int x){return x&-x;}
struct SEG{
	int idx;
	int lc[300*N],rc[300*N],maxx[300*N];//Nlog^2 N
	void sep(int& x,int a,int v,int l,int r){
		if(!x) x=++idx;
		if(l==r) return maxx[x]=max(maxx[x],v),void();
		int mid=(l+r)>>1;
		if(a<=mid) sep(lc[x],a,v,l,mid);
		else sep(rc[x],a,v,mid+1,r);
		maxx[x]=max(maxx[lc[x]],maxx[rc[x]]);
	}
	int query(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return maxx[x];
		int mid=(l+r)>>1,ans=0;
		if(a<=mid) ans=max(ans,query(lc[x],a,b,l,mid));
		if(b>mid) ans=max(ans,query(rc[x],a,b,mid+1,r));
		return ans;
	}
}seg;
struct BIT{
	void sep(int x,int ia,int iv){
		for(;x<=n;x+=lb(x)) seg.sep(root[x],ia,iv,1,n);
	}
	int query(int x,int ia,int ib){
		int ans=0;
		for(;x;x-=lb(x)) ans=max(ans,seg.query(root[x],ia,ib,1,n));
		return ans;
	}
}bit;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i],maxx[i]=minn[i]=a[i];
	while(m--){
		int x,y;
		cin>>x>>y;
		maxx[x]=max(maxx[x],y);
		minn[x]=min(minn[x],y);
	}
	for(int i=1;i<=n;i++){
		f[i]=1+bit.query(a[i],1,minn[i]);
		bit.sep(maxx[i],a[i],f[i]);
	}
	cout<<*max_element(f+1,f+1+n)<<"\n";
	return 0;
}

P3364 Cool loves touli

与上题类似。

先按等级从小到大排序,令每个英雄的后三个属性分别是 \(a_{i},b_{i},c_{i}\)\(f_{i}\) 表示以 \(i\) 结束的选择方案最多包含多少个英雄。则有转移:

\[f_{i}=1+\max\limits_{j} f_{j} \]

其中 \(j\) 需要满足:

  • \(j<i\)
  • \(c_{j}\le a_{i}\)
  • \(b_{j}\le c_{i}\)

和上题一样做就行,如果得 \(60\)你就做对了,因为空间限制不放我们的树套树过 (^^;

点击查看代码 R223752866
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
struct Hero{int l,a,b,c;}s[N];
int n,b[3*N],tn,f[N],root[N];
unordered_map<int,int> ma;
inline int lb(int x){return x&-x;}
struct SEG{
	int idx;
	int lc[400*N],rc[400*N],maxx[400*N];//Nlog^2(3N)
	void sep(int& x,int a,int v,int l,int r){
		if(!x) x=++idx;
		if(l==r) return maxx[x]=max(maxx[x],v),void();
		int mid=(l+r)>>1;
		if(a<=mid) sep(lc[x],a,v,l,mid);
		else sep(rc[x],a,v,mid+1,r);
		maxx[x]=max(maxx[lc[x]],maxx[rc[x]]);
	}
	int query(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return maxx[x];
		int mid=(l+r)>>1,ans=0;
		if(a<=mid) ans=max(ans,query(lc[x],a,b,l,mid));
		if(b>mid) ans=max(ans,query(rc[x],a,b,mid+1,r));
		return ans;
	}
}seg;
struct BIT{
	void sep(int x,int ia,int iv){
		for(;x<=tn;x+=lb(x)) seg.sep(root[x],ia,iv,1,tn);
	}
	int query(int x,int ia,int ib){
		int ans=0;
		for(;x;x-=lb(x)) ans=max(ans,seg.query(root[x],ia,ib,1,tn));
		return ans;
	}
}bit;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>s[i].l>>s[i].a>>s[i].b>>s[i].c;
		b[++tn]=s[i].a,b[++tn]=s[i].b,b[++tn]=s[i].c;
	}
	sort(b+1,b+1+tn),tn=unique(b+1,b+1+tn)-b-1;
	for(int i=1;i<=tn;i++) ma[b[i]]=i;
	sort(s+1,s+1+n,[](Hero a,Hero b){return a.l<b.l;});
	for(int i=1;i<=n;i++){
		auto [ignore,a,b,c]=s[i];
		a=ma[a],b=ma[b],c=ma[c];
		f[i]=1+bit.query(a,1,c);
		bit.sep(c,b,f[i]);
	}
	cout<<*max_element(f+1,f+1+n)<<"\n";
	return 0;
}

P1903 [国家集训队] 数颜色 / 维护队列

本质上是 P1972 [SDOI2009] HH 的项链加了个点修。

套路区间数颜色,仍然记 \(pre_{i}\)\(a_{i}\) 上一次出现的位置,若没有则为 \(0\)。查询时仅需查询满足下面两个条件的 \(i\) 的个数:

  • \(i\in [l,r]\)
  • \(pre_{i}\in [0,l-1]\)

点修直接套用 CF848C Goodbye Souvenir,每次最多修改三个点即可解决。

点击查看代码 R225074108
#include<bits/stdc++.h>
using namespace std;
const int N=133333+10,M=133333+10;
int n,m,tn,a[N],b[N+M],root[N];
unordered_map<int,int> ma;
set<int> p[N+M];
struct Query{char op;int x,y;}q[M];
struct SEG{
	int idx,lc[200*N],rc[200*N],sum[200*N];
	void chp(int& x,int a,int v,int l,int r){
		if(!x) x=++idx;
		sum[x]+=v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc[x],a,v,l,mid);
		else chp(rc[x],a,v,mid+1,r);
	}
	int query(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return sum[x];
		int mid=(l+r)>>1,ans=0;
		if(a<=mid) ans+=query(lc[x],a,b,l,mid);
		if(b>mid) ans+=query(rc[x],a,b,mid+1,r);
		return ans;
	}
}seg;
inline int lb(int x){return x&-x;}
struct BIT{
	void chp(int x,int ia,int iv){
		for(;x<=n;x+=lb(x)) seg.chp(root[x],ia,iv,0,n);
	}
	int query(int x,int ia,int ib){
		int ans=0;
		for(;x;x-=lb(x)) ans+=seg.query(root[x],ia,ib,0,n);
		return ans;
	}
	int query(int x,int y,int ia,int ib){
		return query(y,ia,ib)-query(x-1,ia,ib);
	}
}bit;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i],b[++tn]=a[i];
	for(int i=1;i<=m;i++){
		cin>>q[i].op>>q[i].x>>q[i].y;
		if(q[i].op=='R') b[++tn]=q[i].y;
	}
	sort(b+1,b+1+tn),tn=unique(b+1,b+1+tn)-b-1;
	for(int i=1;i<=tn;i++) ma[b[i]]=i;
	for(int i=1,pre;i<=n;i++){
		a[i]=ma[a[i]];
		pre=p[a[i]].empty()?0:(*p[a[i]].rbegin());
		bit.chp(i,pre,1);
		p[a[i]].insert(i);
	}
	set<int>::iterator it;
	for(int i=1,pre,nxt;i<=m;i++){
		auto [op,x,y]=q[i];
		if(op=='Q'){
			cout<<bit.query(x,y,0,x-1)<<"\n";
		}else{
			y=ma[y];
			p[a[x]].erase(x);
			it=p[y].lower_bound(x+1),pre=0;//1.与新颜色相同的后一位
			if(it!=p[y].begin()) pre=*(--it);
			it=p[y].upper_bound(x);
			if(it!=p[y].end()){
				nxt=*it;
				bit.chp(nxt,x,1);
				bit.chp(nxt,pre,-1);
			}
			it=p[a[x]].lower_bound(x+1),pre=0;//2.与旧颜色相同的后一位
			if(it!=p[a[x]].begin()) pre=*(--it);
			it=p[a[x]].upper_bound(x);
			if(it!=p[a[x]].end()){
				nxt=*it;
				bit.chp(nxt,x,-1);
				bit.chp(nxt,pre,1);
			}
			bit.chp(x,pre,-1);//3.被修改的位置
			a[x]=y,pre=0;
			it=p[y].lower_bound(x+1);
			if(it!=p[y].begin()) pre=*(--it);
			p[y].insert(x);
			bit.chp(x,pre,1);
		}
	}
	return 0;
}

P4690 [Ynoi Easy Round 2016] 镜中的昆虫 ~ LOJ #6201.

Ynoi 一血……?不过要提前说下,洛谷在卡根号空间的同时也把 \(\log^2\) 空间卡掉了,所以树状数组套权值线段树应该是过不掉的。不过你可以在空间限制更宽松的 LOJ 提交,亲测可过~

神题。


本质上是将上题的点修变成区修。

我们不妨沿用上题的思路,就 \(pre\) 数组进行分析。查询和上题相同,我们主要分析区间赋值。

先给结论:对长度为 \(n\) 的序列进行 \(m\) 次区间赋值操作后,\(pre\) 数组的变化次数为 \(O(n+m)\) 级别。

简单证明一下:

如果我们将同色连续段称作一个,那么不难发现,除了块的开头,其他位置的 \(pre\) 值全部都是其下标 \(-1\)

而每次区修一定可以拆成若干个块拼起来(若不能恰好取到左右端点,则将左右端点所在的块分裂),修改完成后这些小的块会被消掉,变成一个大的块。则此过程中发生变化的 \(pre\) 值仅有:

  • 这些块开头的 \(pre\) 值。
  • 所操作的区间右侧,与各个块原颜色相同的第一个位置。

两者同阶,故每次操作 \(pre\) 的变化次数是 \(O(\) 删去的块的个数 \()\),而每次我们最多添加 \(3\) 个块(左右端点所在的块分裂算 \(2\) 个,最后的大块算 \(1\) 个),所以删去的块的个数是 \(O(n+m)\) 数量级的,这样就得证了。


于是我们就可以用 set 来维护每个块,修改 \(pre\) 的时候在树套树上同步更新即可。

点击查看代码 2372872
#include<bits/stdc++.h>
#include<ext/pb_ds/hash_policy.hpp>
#include<ext/pb_ds/assoc_container.hpp>
using namespace std;
using namespace __gnu_pbds;
const int N=1e5+1,M=1e5+1;
int n,m,tn,a[N],b[N+M],pre[N],lst[N+M],st[N+M],top,root[N];
bitset<N+M> in;
struct Que{int l,r,x;}q[M];
gp_hash_table<int,int> ma;
set<int> ra,p[N+M];//ra记录所有块的左端点,p[i]记录颜色为i的所有块的左端点
struct SEG{
	int idx,lc[200*N],rc[200*N],sum[200*N];
	void chp(int& x,int a,int v,int l,int r){
		if(!x) x=++idx;
		sum[x]+=v;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc[x],a,v,l,mid);
		else chp(rc[x],a,v,mid+1,r);
	}
	int query(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return sum[x];
		int mid=(l+r)>>1,ans=0;
		if(a<=mid) ans+=query(lc[x],a,b,l,mid);
		if(b>mid) ans+=query(rc[x],a,b,mid+1,r);
		return ans;
	}
}seg;
inline int lb(int x){return x&-x;}
struct BIT{
	void chp(int x,int ia,int iv){
		for(;x<=n;x+=lb(x)) seg.chp(root[x],ia,iv,0,n);
	}
	int query(int x,int ia,int ib){
		int ans=0;
		for(;x;x-=lb(x)) ans+=seg.query(root[x],ia,ib,0,n);
		return ans;
	}
	int query(int x,int y,int ia,int ib){
		return query(y,ia,ib)-query(x-1,ia,ib);
	}
}bit;
void solve(int l,int r,int x){//将[l,r]置为x
	auto lp=--ra.lower_bound(l+1),rp=ra.upper_bound(r),tmp=lp;//左闭右开
	if(*lp!=l) p[a[*lp]].insert(l),pre[l]=l-1,a[l]=a[*lp],lp=ra.insert(l).first;//分裂左端点所在区间
	if(*rp!=r+1) p[a[*prev(rp)]].insert(r+1),pre[r+1]=r,a[r+1]=a[*prev(rp)],rp=ra.insert(r+1).first;//右端点
	in[st[++top]=x]=1;//st记录修改区间右侧需要更新的颜色
	for(bool f=1;lp!=rp;ra.erase(lp++)){//lp=ra.erase(lp)也可以
		p[a[*lp]].erase(*lp);
		bit.chp(*lp,pre[*lp],-1);
		if(f) f=0,bit.chp(*lp,pre[*lp]=((tmp=p[x].lower_bound(*lp))==p[x].begin()?0:*(ra.upper_bound(*(--tmp)))-1),1);
		else bit.chp(*lp,pre[*lp]=*lp-1,1);//最左边的段需要查找前面的同色位置,其他段修改为i-1即可
		if(!in[a[*lp]]) in[st[++top]=a[*lp]]=1;
		a[*lp]=x;//由于每次访问都是在块的开头,所以更新也只操作块的开头元素即可
	}
	ra.insert(l),p[x].insert(l);
	int c;
	while(top){//处理修改区间右侧的pre
		in[c=st[top--]]=0;
		auto nxt=p[c].upper_bound(r);
		if(nxt!=p[c].end()){
			bit.chp(*nxt,pre[*nxt],-1);
			bit.chp(*nxt,pre[*nxt]=((tmp=p[c].lower_bound(*nxt))==p[c].begin()?0:*(ra.upper_bound(*(--tmp)))-1),1);
		}
	}
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m,tn=n;
	for(int i=1;i<=n;i++) cin>>a[i],b[i]=a[i];
	for(int i=1,o;i<=m;i++){
		cin>>o>>q[i].l>>q[i].r;
		if(2-o) cin>>q[i].x,b[++tn]=q[i].x;
	}
	sort(b+1,b+1+tn),tn=unique(b+1,b+1+tn)-b-1;
	for(int i=1;i<=tn;i++) ma[b[i]]=i;
	for(int i=1;i<=n;i++){
		a[i]=ma[a[i]];
		bit.chp(i,pre[i]=lst[a[i]],1),lst[a[i]]=i;
		if(a[i]!=a[i-1]) ra.insert(i),p[a[i]].insert(i);
	}
	ra.insert(0),ra.insert(n+1);
	for(int i=1;i<=m;i++){
		auto [l,r,x]=q[i];
		if(l>r) continue;
		if(x) solve(l,r,ma[x]);
		else cout<<bit.query(l,r,0,l-1)<<"\n";
	}
	return 0;
}

solve() 是真的难写难调啊~~~ 疑似过于屎山了。

LOJ 上看到了实现出人意料简洁的代码,见 #1748229。虽然维护修改和查询的结构不是树套树而是 CDQ 分治,但是 set 维护的过程是共有的,如果有兴趣可以去学习一下。

小技巧

垃圾回收

防止 MLE,加上这个绝对不亏(如果中途有无用节点的话)。

但是一定要斟酌好删除条件。比如你的删除条件是 sum(x)==0,那你就要留心中途线段树是否可能出现负数从而导致误删。特殊地,若线段树在每次操作完都是正数,而操作过程中要处理减法,那你可以考虑将加法放在减法前面执行。

stack<int> gar;
void del(int &x){gar.push(x),x=0;}
void newnode(int &x){
	if(!gar.empty()) x=gar.top(),gar.pop(),tr[x].init();
	else x=++idx;
}
void chp(int& x,int a,int v,int l,int r){
	if(!x) newnode(x);
	sum[x]+=v;
	if(l<r){
		int mid=(l+r)>>1;
		if(a<=mid) chp(lc[x],a,v,l,mid);
		else chp(rc[x],a,v,mid+1,r);
	}
	if(删除条件...) del(x);
}

标记永久化

支持区间操作的线段树,其常数很大一部分来自标记下放 & 上传。

而且对于主席树上的区修操作,若是下放标记,会影响到之前的版本;若是所有涉及的节点都建新的,又可能爆空间(有结论,单次操作最多涉及 \(4\log n\) 个节点,这样几乎吃不消)。

标记永久化是一种不使用标记下放 / 上传技巧,即修改时将标记保留在修改节点处,并直接处理出该节点的答案,查询时额外累加标记的贡献。

void chr(int &x,int a,int b,int v,int l,int r){
	x=clone(x);
	sum(x)+=(b-a+1)*v;
	if(a==l&&b==r) return tag(x)+=v,void();
	int mid=(l+r)>>1;
	if(b<=mid) chr(lc(x),a,b,v,l,mid);
	else if(a>mid) chr(rc(x),a,b,v,mid+1,r);
	else chr(lc(x),a,mid,v,l,mid),chr(rc(x),mid+1,b,v,mid+1,r);
}
int query(int x,int a,int b,int l,int r){
	if(a==l&&b==r) return sum(x);
	int mid=(l+r)>>1,sum=(b-a+1)*tag(x);
	if(b<=mid) return sum+query(lc(x),a,b,l,mid);
	else if(a>mid) return sum+query(rc(x),a,b,mid+1,r);
	else return sum+query(lc(x),a,mid,l,mid)+query(rc(x),mid+1,b,mid+1,r);
}

注意到两个函数中,\(a,b\) 的值在最后一个 case 会与 \(l,r\) 一起变动。这是因为我们要想让 \([a,b]\) 始终 \(\subset [l,r]\),这样就能直接通过 \(b-a+1\) 来得出 \(|[a,b]\cap[l,r]|\),即当前区间被修改的元素个数,进而更方便地提前计算出此次修改对 sum(x) 的贡献。

模板题:SP11470 TTM - To the moon

点击查看代码 R223037296
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10,M=1e5+10,logN=17;
int n,m,a[N],idx,root[N];
struct SEG{
	int idx;
	struct node{int lc,rc,sum,tag;}tr[(N+M)*logN<<2];
	#define lc(x) (tr[x].lc)
	#define rc(x) (tr[x].rc)
	#define sum(x) (tr[x].sum)
	#define tag(x) (tr[x].tag)
	int clone(int x){return tr[++idx]=tr[x],idx;}
	void build(int &x,int l,int r){
		x=++idx;
		if(l==r) return sum(x)=a[l],void();
		int mid=(l+r)>>1;
		build(lc(x),l,mid),build(rc(x),mid+1,r);
		sum(x)=sum(lc(x))+sum(rc(x));
	}
	void chr(int &x,int a,int b,int v,int l,int r){
		x=clone(x);
		sum(x)+=(b-a+1)*v;
		if(a==l&&b==r) return tag(x)+=v,void();
		int mid=(l+r)>>1;
		if(b<=mid) chr(lc(x),a,b,v,l,mid);
		else if(a>mid) chr(rc(x),a,b,v,mid+1,r);
		else chr(lc(x),a,mid,v,l,mid),chr(rc(x),mid+1,b,v,mid+1,r);
	}
	int query(int x,int a,int b,int l,int r){
		if(a==l&&b==r) return sum(x);
		int mid=(l+r)>>1,sum=(b-a+1)*tag(x);
		if(b<=mid) return sum+query(lc(x),a,b,l,mid);
		else if(a>mid) return sum+query(rc(x),a,b,mid+1,r);
		else return sum+query(lc(x),a,mid,l,mid)+query(rc(x),mid+1,b,mid+1,r);
	}
}tr;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i],a[i]+=a[i-1];
	char op;
	for(int i=1,l,r,x;i<=m;i++){
		cin>>op;
		if(op=='C'){
			cin>>l>>r>>x;
			idx++,root[idx]=root[idx-1];
			tr.chr(root[idx],l,r,x,1,n);
		}else if(op=='Q'){
			cin>>l>>r;
			cout<<a[r]-a[l-1]+tr.query(root[idx],l,r,1,n)<<"\n";
		}else if(op=='H'){
			cin>>l>>r>>x;
			cout<<a[r]-a[l-1]+tr.query(root[x],l,r,1,n)<<"\n";
		}else{
			cin>>x;
			idx=x;
		}
	}
}

内外反套

内外反套,即将外层结构和内层结构维护的维度反过来。这个在上文 P3332 [ZJOI2013] K 大数查询中进行了说明。

内外反套,一般可以将本应树状数组来维护的区间操作,丢给内层线段树,避免较大的码量带来的调试困难(只是,经不完全测试,时间上可能略。略。略。逊一点)。

后缀树状数组

上文 P4093 [HEOI2016 / TJOI2016] 序列中我们利用了“最小值的查询是一个前缀”,从而避开了最值不可减的缺陷。

如果查询是一个后缀,我们则可以使用后缀树状数组,查询时 \(x\)\(1\) 开始递增,修改时 \(x\)\(n\) 开始递减。这样维护的就是后缀的信息了~

这样写还可以在顺序遍历的情况下,不作差地求逆序对。

如何避免漏写 root[] 导致的错误

动态开点线段树添加节点时,我们一般用 root[x] 来存储树状数组第 \(x\) 位对应的线段树的根节点。不过有时候我们可能会忘记加 root[],直接将 x 提供进去(至少我有时会这样 (^^;

为了解决这个问题,除了一遍遍提醒自己以外,还有另外一种方式(是我从 zhoukangyang 大佬的代码中学到的)。

如果根节点数量为 \(n\),那么 \(idx\) 就从 \(n+1\) 开始用,这样我们可以直接用 \(x\) 来代表根节点。不过相应地修改函数不能传引用进去,需要进行一些修改。

void newnode(int &x){if(!x) x=++idx;return idx;}
void chp(int x,int a,int v,int l,int r){
	sum(x)+=v;
	if(l==r) return;
	int mid=(l+r)>>1;
	if(a<=mid) chp(newnode(lc(x)),a,v,l,mid);
	else chp(newnode(rc(x)),a,v,mid+1,r);
}

根据个人习惯来选择就好。

参考文章:

你觉得数据结构可爱嘛?

pid 131967359
posted @ 2025-07-15 17:28  Sinktank  阅读(153)  评论(0)    收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2025 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.