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

前

树状数组套权值线段树,是众多树套树的一种(下文树套树默认指树状数组套权值线段树),可以在单次查询 / 点修均 \(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  阅读(520)  评论(0)    收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2025 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.