线段树基础(下)

线段树二分

对序列进行二分的操作,可能使用线段树二分进行优化。

一些序列上最左/最右位置问题可以二分解决,同时需要使用线段树进行查询。时间复杂度通常是 O ( n log ⁡ 2 n ) O(n\log^2n) O(nlog2n),可以尝试使用线段树二分的技巧将其优化为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

具体来说线段树二分有这三步:

  • 若包含且无解,则更新并返回
  • 叶节点返回
  • 按照顺序递归

事实上线段树二分还有更复杂的用法,用来解决其他序列二分问题,例如找到位置 l 1 , l 2 l_1,l_2 l1,l2的最长公共前缀可以使用线段树+哈希实现。

例题1(线段树二分)

题目分析:
固定一个前缀,区间 m a x max max一定单调递增,而区间 m i n min min一定单调递减因此二者必然有唯一的一段相交(或错开):
在这里插入图片描述
我们需要二分出相交的第一个位置和最后一个位置。
由于这样的位置并不一定存在,并且不好写。
我们先求出第一个 m a x ≥ m i n max\geq min maxmin的位置。

线段树二分(findr)

findr1(u,l)表示查询到节点 u u u,查询 [ l , n ] [l,n] [l,n]第一个 m a x ≥ m i n max\geq min maxmin的位置。
过程是这样的:

  • 访问到与 [ l , n ] [l,n] [l,n]有交的节点 u u u处(初始是 r o o t root root
  • [ l u , r u ] ⊆ [ l , n ] [l_u,r_u]\subseteq[l,n] [lu,ru][l,n]并且无解:更新前缀信息后返回无解
  • 如果是叶子,返回这个位置为答案
  • 如果左儿子与 [ l , n ] [l,n] [l,n]有交,先递归进左儿子
  • 如果此时没找到答案,再递归进右儿子
  • 返回答案
int findr1(int u,int l,int&maxx,int&minn) {
	maxx表示[l,t[u].l)的区间max,在离开节点u时更新为[l,t[u].r]的区间max
	minn同理
	
	合法条件为max>=min
	非法即max<min
	if(l<=t[u].l&&max(maxx,t[u].max)<min(minn,t[u].min)) {
		更新前缀信息并返回无解:
		maxx=max(maxx,t[u].max);
		minn=min(minn,t[u].min);
		return -1;
	}
	if(t[u].l==t[u].r) return t[u].l;
	int mid=t[u].l+t[u].r>>1;
	int ans=-1;
	if(l<=mid) ans=findr1(u<<1,l,maxx,minn);
	if(!~ans) ans=findr1(u<<1|1,l,maxx,minn);
	return ans;
}

具体原理是这样的:
如果 u ⊆ [ l , n ] u\subseteq [l,n] u[l,n],并且节点 u u u无解,那么直接用该节点的信息更新前缀信息并返回即可。

如果 u u u相交但不属于 [ l , n ] [l,n] [l,n],说明 l ∈ ( l u , r u ] l\in (l_u,r_u] l(lu,ru],即是黄色的节点之一:
在这里插入图片描述
显然这样的节点最多只有 log ⁡ n \log n logn个,对于这些节点无法直接用此节点的信息更新前缀信息,必须要递归到子节点更新前缀信息。(同时因为不知道这个节点+前缀的信息,也不知道这个节点行不行,因此还要递归到它的儿子节点查找答案,一举两得了)

这样我们可以找到在 l l l右侧满足 m a x ≥ m i n max\geq min maxmin的第一个位置。

我们发现这样递归容易找到第一个合法的位置,但是难以找到最后一个合法的位置。因此对于“最后一个满足 m a x ≥ m i n max\geq min maxmin的位置”是不好处理的。(当然你也可以再编一个板子处理这个问题)
因此我们转化为找到第一个满足 m a x > m i n max>min max>min的位置,再减 1 1 1就可以了。这样就得到了findr2

int findr2(int u,int l,int&maxx,int&minn) {
	唯一的区别就是:
	合法条件为max>min
	非法条件为max<=min
	if(l<=t[u].l&&max(maxx,t[u].max)<=min(minn,t[u].min)) {
		maxx=max(maxx,t[u].max);
		minn=min(minn,t[u].min);
		return -1;
	}
	if(t[u].l==t[u].r) return t[u].l;
	int mid=t[u].l+t[u].r>>1;
	int ans=-1;
	if(l<=mid) ans=findr2(u<<1,l,maxx,minn);
	if(!~ans) ans=findr2(u<<1|1,l,maxx,minn);
	return ans;
}

实现

#include<iostream>
using namespace std;
const int N=2e5;
int a[N+5],b[N+5];
struct node {
	int l,r;
	int min,max;
} t[N<<2];
void push_up(int u) {
	t[u].max=max(t[u<<1].max,t[u<<1|1].max);
	t[u].min=min(t[u<<1].min,t[u<<1|1].min);
}
void build(int u,int l,int r) {
	t[u]= {l,r};
	if(l==r) {
		t[u].min=b[l];
		t[u].max=a[l];
		return;
	}
	int mid=l+r>>1;
	build(u<<1,l,mid);
	build(u<<1|1,mid+1,r);
	push_up(u);
}
int findr1(int u,int l,int&maxx,int&minn) {
	if(l<=t[u].l&&max(maxx,t[u].max)<min(minn,t[u].min)) {
		maxx=max(maxx,t[u].max);
		minn=min(minn,t[u].min);
		return -1;
	}
	if(t[u].l==t[u].r) return t[u].l;
	int mid=t[u].l+t[u].r>>1;
	int ans=-1;
	if(l<=mid) ans=findr1(u<<1,l,maxx,minn);
	if(!~ans) ans=findr1(u<<1|1,l,maxx,minn);
	return ans;
}
int findr2(int u,int l,int&maxx,int&minn) {
	if(l<=t[u].l&&max(maxx,t[u].max)<=min(minn,t[u].min)) {
		maxx=max(maxx,t[u].max);
		minn=min(minn,t[u].min);
		return -1;
	}
	if(t[u].l==t[u].r) return t[u].l;
	int mid=t[u].l+t[u].r>>1;
	int ans=-1;
	if(l<=mid) ans=findr2(u<<1,l,maxx,minn);
	if(!~ans) ans=findr2(u<<1|1,l,maxx,minn);
	return ans;
}
int main() {
	int n;
	cin>>n;
	for(int i=1; i<=n; i++) cin>>a[i];
	for(int i=1; i<=n; i++) cin>>b[i];
	build(1,1,n);
	long long ans=0;
	for(int i=1; i<=n; i++) {
		int minn=1e9,maxx=-1e9;
		int l=findr1(1,i,maxx,minn);
		if(!~l) continue;
		minn=1e9,maxx=-1e9;
		int r=findr2(1,i,maxx,minn);
		if(!~r) r=n+1;
		r--;
		ans+=r-l+1;
		若不存在max=min的位置,则max>=min和max>min的位置重合,则r-l+1=0
	}
	cout<<ans;
}
/*
1
1
1

3
3 3 7
3 9 7
*/

例题2(线段树二分)

把所有颜色加入线段树,然后枚举颜色,把对应颜色从线段树中删除,枚举一条本颜色的线段,然后判掉相交的情况。剩下的部分就是线段树二分。

相当于找到 l l l右侧第一个非 0 0 0的地方, r r r左侧第一个非 0 0 0的地方。
可以维护区间加法懒标记和区间最大值。(或使用标记永久化等)

使用懒标记需要下放懒标记,使用永久标记需要在最后返回答案时计算永久标记对答案的影响。

二分右端点(findr)

findr(u,l)返回 [ l , n ] [l,n] [l,n]中第一个非零的位置,具体过程如下:

  • 进入一个与 [ l , n ] [l,n] [l,n]相交的节点 u u u(初始为根)
  • u u u对应的区间包含于 [ l , n ] [l,n] [l,n] m a x u = 0 max_u=0 maxu=0,则返回无解
    (本题二分过程中不需要维护前缀信息)
  • 下放懒标记
  • 如果左儿子与询问有交就递归左儿子
  • 如果仍然没有答案就递归右儿子
  • 返回答案
int findr(int u,int l){
	if(l<=t[u].l&&!t[u].max) return -1;
	(由于本题二分时不需要维护前缀信息,因此不需要更新前缀信息,直接返回无解)
	if(t[u].l==t[u].r) return t[u].l;
	push_down(u);
	int mid=t[u].l+t[u].r>>1;
	int ans=-1;
	if(l<=mid) ans=findr(u<<1,l);
	if(!~ans) ans=findr(u<<1|1,l);
	return ans;
}

二分左端点(findl)

findl(u,r)返回 [ 1 , r ] [1,r] [1,r]中最后一个非零的位置,具体过程如下:

  • 进入一个与 [ 1 , r ] [1,r] [1,r]相交的节点 u u u(初始为根)
  • u u u对应的区间包含于 [ 1 , r ] [1,r] [1,r] m a x u = 0 max_u=0 maxu=0,则返回无解
  • 下放懒标记
  • 如果儿子与询问有交就递归右儿子
  • 如果仍然没有答案再递归左儿子
  • 返回答案
int findl(int u,int r){
	if(t[u].r<=r&&!t[u].max) return -1;
	if(t[u].l==t[u].r) return t[u].l;
	push_down(u);
	int mid=t[u].l+t[u].r>>1;
	int ans=-1;
	if(mid<r) ans=findl(u<<1|1,r);
	if(!~ans) ans=findl(u<<1,r);
	return ans;
}

实现

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=2e5;
struct node {
	int l,r;
	int max,add;
}t[N<<3];
void build(int u,int l,int r){
	t[u]={l,r};
	if(l==r) return;
	int mid=l+r>>1;
	build(u<<1,l,mid);
	build(u<<1|1,mid+1,r);
}
void push_up(int u){
	t[u].max=max(t[u<<1].max,t[u<<1|1].max);
}
void push_down(int u){
	int l=u<<1,r=u<<1|1;
	t[l].max+=t[u].add;
	t[r].max+=t[u].add;
	t[l].add+=t[u].add;
	t[r].add+=t[u].add;
	t[u].add=0;
}
void push(int u,int l,int r,int val){
	if(l<=t[u].l&&t[u].r<=r) t[u].max+=val,t[u].add+=val;
	else {
		push_down(u);
		int mid=t[u].l+t[u].r>>1;
		if(l<=mid) push(u<<1,l,r,val);
		if(mid<r) push(u<<1|1,l,r,val);
		push_up(u);
	}
}
int find(int u,int l,int r){
	if(l<=t[u].l&&t[u].r<=r) return t[u].max;
	push_down(u);
	int mid=t[u].l+t[u].r>>1;
	int ans=0;
	if(l<=mid) ans=max(ans,find(u<<1,l,r));
	if(mid<r) ans=max(ans,find(u<<1|1,l,r));
	return ans;
}
int findr(int u,int l){
	if(l<=t[u].l&&!t[u].max) return -1;
	if(t[u].l==t[u].r) return t[u].l;
	push_down(u);
	int mid=t[u].l+t[u].r>>1;
	int ans=-1;
	if(l<=mid) ans=findr(u<<1,l);
	if(!~ans) ans=findr(u<<1|1,l);
	return ans;
}
int findl(int u,int r){
	if(t[u].r<=r&&!t[u].max) return -1;
	if(t[u].l==t[u].r) return t[u].l;
	push_down(u);
	int mid=t[u].l+t[u].r>>1;
	int ans=-1;
	if(mid<r) ans=findl(u<<1|1,r);
	if(!~ans) ans=findl(u<<1,r);
	return ans;
}
vector<int> a[N+5];
int x[N+5],y[N+5],c[N+5];
int b[N*2+5];
int ans[N+5];
//void check(int u) {
//	cout<<u<<"("<<t[u].l<<','<<t[u].r<<") "<<t[u].cnt<<' '<<t[u].sum<<endl;
//	if(t[u].l==t[u].r) return ;
//	check(u<<1);
//	check(u<<1|1);
//}
int main() {
	build(1,1,N<<1);
//	build(1,1,4);
	int T;
	cin>>T;
	while(T--) {
		int n;
		cin>>n;
		for(int i=1; i<=n; i++) a[i].resize(0);
		for(int i=1; i<=n; i++) {
			cin>>x[i]>>y[i]>>c[i];
			a[c[i]].push_back(i);
			b[i]=x[i];
			b[i+n]=y[i];
		}
		sort(b+1,b+1+2*n);
		int*t=unique(b+1,b+1+2*n);
		for(int i=1; i<=n; i++)
			x[i]=lower_bound(b+1,t,x[i])-b,
			     y[i]=lower_bound(b+1,t,y[i])-b;
		for(int i=1; i<=n; i++) push(1,x[i],y[i],1);
//		puts("***");
//		check(1);
		for(int i=1; i<=n; i++) {
			for(auto&j:a[i]) push(1,x[j],y[j],-1);
//			puts("***");
//			check(1);
			for(auto&j:a[i]) {
				if(find(1,x[j],y[j])) ans[j]=0;
				else {
					int p=findr(1,y[j]),q=findl(1,x[j]);
					if(~p&&~q) ans[j]=min(b[p]-b[y[j]],b[x[j]]-b[q]);
					else if(~p) ans[j]=b[p]-b[y[j]];
					else if(~q) ans[j]=b[x[j]]-b[q];
					else throw;
				}
			}
			for(auto&j:a[i]) push(1,x[j],y[j],1);
		}
		for(int i=1; i<=n; i++) push(1,x[i],y[i],-1);
//		puts("***");
		for(int i=1; i<=n; i++) cout<<ans[i]<<" \n"[i==n];
	}
}
/*
1
3
1 2 1
3 4 1
5 6 2

3 1 1

1
2
1 2 1
3 4 2



1
2
1 100 2
10 90 1

0 0
*/

实现(永久标记线段树二分)

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=2e5;
struct node {
	int l,r;
	int cnt,sum;
}t[N<<3];
void push_up(int u){
	t[u].sum=t[u].cnt||t[u<<1].sum||t[u<<1|1].sum;
}
void build(int u,int l,int r){
	t[u]={l,r};
	if(l==r) return ;
	int mid=l+r>>1;
	build(u<<1,l,mid);
	build(u<<1|1,mid+1,r);
}
void push(int u,int l,int r,int val) {
	if(l<=t[u].l&&t[u].r<=r) {
		t[u].sum=t[u].cnt+=val;
		if(t[u].l^t[u].r) push_up(u);
	}
	else {
		int mid=t[u].l+t[u].r>>1;
		if(l<=mid) push(u<<1,l,r,val);
		if(mid<r) push(u<<1|1,l,r,val);
		push_up(u);
	}
}
int find(int u,int l,int r) {
	if(l<=t[u].l&&t[u].r<=r) return t[u].sum;
	int mid=t[u].l+t[u].r>>1;
	int ans=0;
	if(l<=mid) ans|=find(u<<1,l,r);
	if(mid<r) ans|=find(u<<1|1,l,r);
	return ans; 
}
int findr(int u,int l){
	if(l<=t[u].l&&!t[u].sum) return -1;
	if(t[u].l==t[u].r) return t[u].l;
	int mid=t[u].l+t[u].r>>1;
	int ans=-1;
	if(l<=mid) ans=findr(u<<1,l);
	if(!~ans) ans=findr(u<<1|1,l);
	if(t[u].cnt) return max(l,t[u].l);
	这里处理永久标记对答案的影响,注意返回max(l,t[u].l)
	(其实这个题因为判了相交所以保证了a[l-1]的位置为0,所以对于这个题来说直接返回t[u].l也行)
	return ans;
}
int findl(int u,int r){
	if(t[u].r<=r&&!t[u].sum) return -1;
	if(t[u].l==t[u].r) return t[u].l;
	int mid=t[u].l+t[u].r>>1;
	int ans=-1;
	if(mid<r) ans=findl(u<<1|1,r);
	if(!~ans) ans=findl(u<<1,r);
	if(t[u].cnt) return min(r,t[u].r);
	return ans;
}
vector<int> a[N+5];
int x[N+5],y[N+5],c[N+5];
int b[N*2+5];
int ans[N+5];
//void check(int u) {
//	cout<<u<<"("<<t[u].l<<','<<t[u].r<<") "<<t[u].cnt<<' '<<t[u].sum<<endl;
//	if(t[u].l==t[u].r) return ;
//	check(u<<1);
//	check(u<<1|1);
//}
int main() {
	build(1,1,N<<1);
//	build(1,1,4);
	int T;
	cin>>T;
	while(T--) {
		int n;
		cin>>n;
		for(int i=1; i<=n; i++) a[i].resize(0);
		for(int i=1; i<=n; i++) {
			cin>>x[i]>>y[i]>>c[i];
			a[c[i]].push_back(i);
			b[i]=x[i];
			b[i+n]=y[i];
		}
		sort(b+1,b+1+2*n);
		int*t=unique(b+1,b+1+2*n);
		for(int i=1; i<=n; i++)
			x[i]=lower_bound(b+1,t,x[i])-b,
			     y[i]=lower_bound(b+1,t,y[i])-b;
		for(int i=1; i<=n; i++) push(1,x[i],y[i],1);
//		puts("***");
//		check(1);
		for(int i=1; i<=n; i++) {
			for(auto&j:a[i]) push(1,x[j],y[j],-1);
//			puts("***");
//			check(1);
			for(auto&j:a[i]) {
				if(find(1,x[j],y[j])) ans[j]=0;
				else {
					int p=findr(1,y[j]),q=findl(1,x[j]);
					if(~p&&~q) ans[j]=min(b[p]-b[y[j]],b[x[j]]-b[q]);
					else if(~p) ans[j]=b[p]-b[y[j]];
					else if(~q) ans[j]=b[x[j]]-b[q];
					else throw;
				}
			}
			for(auto&j:a[i]) push(1,x[j],y[j],1);
		}
		for(int i=1; i<=n; i++) push(1,x[i],y[i],-1);
//		puts("***");
		for(int i=1; i<=n; i++) cout<<ans[i]<<" \n"[i==n];
	}
}
/*
1
3
1 2 1
3 4 1
5 6 2

3 1 1

1
2
1 2 1
3 4 2



1
2
1 100 2
10 90 1

0 0
*/

线段树合并

两颗宽度相同的动态开点线段树合并,事实上就是对标记进行合并,假设一棵树上标记为 u u u,另一棵树上对应位置的标记为 v v v,则新的标记称为 f ( u , v ) f(u,v) f(u,v)

线段树合并原理

merge(u,v)表示 u → v u\rightarrow v uv,把 u u u合并到 v v v上,并返回合并后的根节点编号。

把线段树 u u u合并到线段树 v v v上,则过程是这样的:

  • 如果 v , u v,u v,u都没有左右儿子,则暴力合并并返回。
  • 如果 u , v u,v u,v其中一个没有左儿子,则把存在的那个左儿子直接当做 v v v的左儿子,否则递归进左儿子合并
  • 如果 u , v u,v u,v其中一个没有右儿子,则把存在的那个右儿子直接当做 v v v的右儿子,否则递归进右儿子合并
  • 上传懒标记push_up(v)(这一步相当于合并了 u , v u,v u,v

实现起来是这样的:

  • 检查边界情况
  • 均无左右儿子则暴力合并
  • 递归

代码:

int merge(int u,int v) {
	表示u->v合并,并返回合并后的根节点编号
	if(!u|!v) return u|v;
	if(!t[u].lc&&!t[u].rc&&!t[u].lc&&!t[v].rc){t[v].max.first+=t[u].max.first;return v;} 
	t[v].lc=merge(t[u].lc,t[v].lc);
	t[v].rc=merge(t[u].rc,t[v].rc);
	push_up(v);
	return v;
}

显然线段树合并算法过程中,假设 u u u的一个节点与 v v v的一个节点重合,则 u u u的这个节点会被合并到 v v v上一次,因此总复杂度为 O ( x ) O(x) O(x)。其中 x x x表示两棵树的重合节点数。

我们用两颗树的总结点数来估算 x x x

因此把共有 s u m sum sum个节点的 n n n线段树合并为一颗,先把第 1 1 1颗和第 2 2 2颗合并,然后再把得到的新树和第 3 3 3颗合并,再把得到的新树和第 4 4 4颗合并…,每颗线段树上的节点最多被合并到另一棵树上一次,时间复杂度为 O ( s u m ) O(sum) O(sum)

当然也可以每次新开节点(这样的话还可以正常访问原来的线段树 v v v):

int merge(int u,int v) {
	if(!u|!v) return u|v;
	int x=++tot;
	t[x]=t[v];
	if(!t[u].lc&&!t[u].rc&&!t[v].lc&&!t[v].rc) {t[x].max.first+=t[u].max.first;return x;} 
	t[x].lc=merge(t[u].lc,t[v].lc);
	t[x].rc=merge(t[u].rc,t[v].rc);
	push_up(x);
	return x;
}

线段树合并的空间复杂度

如果合并的过程中不创建新节点的话,线段树合并的线段树节点数量就是原本的动态开点线段树的节点总数。

如果创建新节点的话,线段树合并还额外需要一倍于原来空间的空间。

例题1

题目分析:
首先把路径修改转化为树上差分。
然后我们用 d u , i d_{u,i} du,i表示 u u u节点上目前 i i i的差分数组。然后我们要对这个东西进行树上前缀和。

可以使用线段树合并优化。同时维护最大值。

struct node{
	int l,r,lc,rc;
	pair<int,int>max;
	<最大值,最大值编号> 
};
pair<int,int>max(pair<int,int>x,pair<int,int>y) {
	return x.first==y.first?x.second<y.second?x:y:x.first>y.first?x:y;
}
int merge(int u,int v) {
	u->v合并,并返回编号
	if(!u|!v) return u|v;
	if(!t[u].lc&&!t[u].rc&&!t[u].lc&&!t[v].rc) {t[v].max.first+=t[u].max.first;return v;} 
	t[v].lc=merge(t[u].lc,t[v].lc);
	t[v].rc=merge(t[u].rc,t[v].rc);
	push_up(v);
	return v;
}

空间复杂度分析

若合并时不新开节点,则线段树总节点数=操作数×单次操作创建节点数

单次操作创建节点数为 ⌈ log ⁡ 2 n ⌉ = 17 \lceil\log_2n\rceil=17 log2n=17
由于树上差分,一次修改操作要转化为四次,因此 操作数 = 4 m 操作数=4m 操作数=4m

因此线段树最多节点数为 68 m 68m 68m,由于一般线段树节点从 1 1 1开始编号,因此理论上数组范围至少开到 68 m + 1 68m+1 68m+1
习惯上略微放大一点,例如开到 80 m 80m 80m

若合并时创建新节点,则线段树总节点数为刚才的两倍,即 136 m 136m 136m。因此理论上数组范围至少开到 136 m + 1 136m+1 136m+1
习惯上放大一些,例如开到 160 m 160m 160m

实现(合并不新开节点)

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
const int N=1e5;
struct node{
	int l,r,lc,rc;
	pair<int,int>max;//<最大值,最大值编号> 
}t[N*68+1];
pair<int,int>max(pair<int,int>x,pair<int,int>y) {
	return x.first==y.first?x.second<y.second?x:y:x.first>y.first?x:y;
}
int tot;
int&lc(int u){
	if(t[u].lc) return t[u].lc;
	t[++tot]={t[u].l,t[u].l+t[u].r>>1};
	return t[u].lc=tot;
}
int&rc(int u){
	if(t[u].rc) return t[u].rc;
	t[++tot]={(t[u].l+t[u].r)/2+1,t[u].r};
	return t[u].rc=tot;
}
void push_up(int u){
	t[u].max=max(t[t[u].lc].max,t[t[u].rc].max);
}
void push(int u,int l,int r,int val){
	if(l<=t[u].l&&t[u].r<=r) t[u].max={t[u].max.first+val,l};
	else {
		int mid=t[u].l+t[u].r>>1;
		if(l<=mid) push(lc(u),l,r,val);
		if(mid<r) push(rc(u),l,r,val);
		push_up(u);
	}
}
int merge(int u,int v) {//u->v合并,并返回编号
	if(!u|!v) return u|v;
	if(t[v].l==t[v].r) {t[v].max.first+=t[u].max.first;return v;} 
	t[v].lc=merge(t[u].lc,t[v].lc);
	t[v].rc=merge(t[u].rc,t[v].rc);
	push_up(v);
	return v;
}
vector<int> a[N+5];
int in[N+5];
int f[20][2*N+5],cnt;
int fa[N+5];
int MIN(int x,int y) {
	return in[x]<in[y]?x:y;
}
int LCA(int x,int y) {
	int l=in[x],r=in[y];
	if(l>r) swap(l,r);
	int k=log2(r-l+1);
	return MIN(f[k][l],f[k][r-(1<<k)+1]);
} 
void dfs1(int u) {
	f[0][in[u]=++cnt]=u;
	for(auto&v:a[u])
		if(v^fa[u]&&(fa[v]=u))
			dfs1(v),f[0][++cnt]=u;
}
int ans[N+5];
void dfs2(int u) {
	for(auto&v:a[u])
		if(v^fa[u])
			dfs2(v),merge(v,u);
	if(t[u].max.first>0) 
		ans[u]=t[u].max.second;
}
int main() {
	int n,m;
	cin>>n>>m;
	for(int i=1;i<n;i++) {
		int u,v;
		cin>>u>>v;
		a[u].push_back(v);
		a[v].push_back(u);
	}
	dfs1(1);
	for(int k=1;k<20;k++)
		for(int i=1;i+(1<<k)-1<=cnt;i++)
			f[k][i]=MIN(f[k-1][i],f[k-1][i+(1<<k-1)]);
	for(int i=1;i<=n;i++) 
//		t[++tot]={1,N};
		t[++tot]={1,N};
	while(m--) {
		int x,y,z;
		cin>>x>>y>>z;
		int lca=LCA(x,y);
		push(x,z,z,1);
		push(y,z,z,1);
		push(lca,z,z,-1);
		if(fa[lca]) push(fa[lca],z,z,-1);
	}
	dfs2(1);
	for(int i=1;i<=n;i++) cout<<ans[i]<<endl;
}
/*
3 2
1 2 1 3
2 2 2
3 3 1
*/

实现(合并新开节点)

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
const int N=1e5;
struct node{
	int l,r,lc,rc;
	pair<int,int>max;//<最大值,最大值编号> 
}t[N*136+1];
pair<int,int>max(pair<int,int>x,pair<int,int>y) {
	return x.first==y.first?x.second<y.second?x:y:x.first>y.first?x:y;
}
int tot;
int&lc(int u){
	if(t[u].lc) return t[u].lc;
	t[++tot]={t[u].l,t[u].l+t[u].r>>1};
	return t[u].lc=tot;
}
int&rc(int u){
	if(t[u].rc) return t[u].rc;
	t[++tot]={(t[u].l+t[u].r)/2+1,t[u].r};
	return t[u].rc=tot;
}
void push_up(int u){
	t[u].max=max(t[t[u].lc].max,t[t[u].rc].max);
}
void push(int u,int l,int r,int val){
	if(l<=t[u].l&&t[u].r<=r) t[u].max={t[u].max.first+val,l};
	else {
		int mid=t[u].l+t[u].r>>1;
		if(l<=mid) push(lc(u),l,r,val);
		if(mid<r) push(rc(u),l,r,val);
		push_up(u);
	}
}
int merge(int u,int v) {//u->v合并,并返回编号
	if(!u|!v) return u|v;
	if(t[v].l==t[v].r) {t[v].max.first+=t[u].max.first;return v;} 
	t[v].lc=merge(t[u].lc,t[v].lc);
	t[v].rc=merge(t[u].rc,t[v].rc);
	push_up(v);
	return v;
}
vector<int> a[N+5];
int in[N+5];
int f[20][2*N+5],cnt;
int fa[N+5];
int MIN(int x,int y) {
	return in[x]<in[y]?x:y;
}
int LCA(int x,int y) {
	int l=in[x],r=in[y];
	if(l>r) swap(l,r);
	int k=log2(r-l+1);
	return MIN(f[k][l],f[k][r-(1<<k)+1]);
} 
void dfs1(int u) {
	f[0][in[u]=++cnt]=u;
	for(auto&v:a[u])
		if(v^fa[u]&&(fa[v]=u))
			dfs1(v),f[0][++cnt]=u;
}
int ans[N+5];
void dfs2(int u) {
	for(auto&v:a[u])
		if(v^fa[u])
			dfs2(v),merge(v,u);
	if(t[u].max.first>0) 
		ans[u]=t[u].max.second;
}
int main() {
	int n,m;
	cin>>n>>m;
	for(int i=1;i<n;i++) {
		int u,v;
		cin>>u>>v;
		a[u].push_back(v);
		a[v].push_back(u);
	}
	dfs1(1);
	for(int k=1;k<20;k++)
		for(int i=1;i+(1<<k)-1<=cnt;i++)
			f[k][i]=MIN(f[k-1][i],f[k-1][i+(1<<k-1)]);
	for(int i=1;i<=n;i++) 
//		t[++tot]={1,N};
		t[++tot]={1,N};
	while(m--) {
		int x,y,z;
		cin>>x>>y>>z;
		int lca=LCA(x,y);
		push(x,z,z,1);
		push(y,z,z,1);
		push(lca,z,z,-1);
		if(fa[lca]) push(fa[lca],z,z,-1);
	}
	dfs2(1);
	for(int i=1;i<=n;i++) cout<<ans[i]<<endl;
}
/*
3 2
1 2 1 3
2 2 2
3 3 1
*/

例题2

实现

#include<iostream>
#include<vector>
using namespace std;
const int N=1e5;
struct node {
	int l,r,lc,rc;
	int sum;
} t[N*20];
int tot;
void push_up(int u) {
	t[u].sum=t[t[u].lc].sum+t[t[u].rc].sum;
}
int&lc(int u) {
	if(t[u].lc) return t[u].lc;
	t[++tot]= {t[u].l,t[u].l+t[u].r>>1};
	return t[u].lc=tot;
}
int&rc(int u) {
	if(t[u].rc) return t[u].rc;
	t[++tot]= {(t[u].l+t[u].r)/2+1,t[u].r};
	return t[u].rc=tot;
}
void push(int u,int l,int r,int val) {
	if(l<=t[u].l&&t[u].r<=r) t[u].sum+=val;
	else {
		int mid=t[u].l+t[u].r>>1;
		if(l<=mid) push(lc(u),l,r,val);
		if(mid<r) push(rc(u),l,r,val);
		push_up(u);
	}
}
int find(int u,int l,int r) {
	if(l<=t[u].l&&t[u].r<=r) return t[u].sum;
	int mid=t[u].l+t[u].r>>1;
	int ans=0;
	if(l<=mid&&t[u].lc) ans+=find(lc(u),l,r);
	if(mid<r&&t[u].rc) ans+=find(rc(u),l,r);
	return ans;
}
int merge(int u,int v) {
	if(!u|!v) return u|v;
	if(!t[u].lc&&!t[u].rc&&!t[v].lc&&!t[v].rc) {
		t[v].sum+=t[u].sum;
		return v;
	}
	t[v].lc=merge(t[u].lc,t[v].lc);
	t[v].rc=merge(t[u].rc,t[v].rc);
	push_up(v);
	return v;
}
int dep[N+5];
int n,m;
int que[N+5];
vector<int> ques[N+5];
vector<int> a[N+5];
int ans[N+5];
void dfs1(int u,int fa) {
	dep[u]=dep[fa]+1;
	for(auto&v:a[u])
		if(v^fa)
			dfs1(v,u);
}
void dfs2(int u,int fa) {
	for(auto&v:a[u])
		if(v^fa)
			dfs2(v,u),merge(v,u);
	for(auto&i:ques[u])
		ans[i]=find(u,que[i],n);
}
int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		t[++tot]={1,n};
	for(int i=1,u,v; i<n; i++) {
		cin>>u>>v;
		a[u].push_back(v);
		a[v].push_back(u);
	}
	int tot=0;
	int last=n+1;
	for(int i=1; i<=m; i++) {
		int op,x;
		cin>>op>>x;
		if(op==1) last=x;
		else {
			que[++tot]=last;
			if(last^n+1)
				ques[x].push_back(tot);
		}
	}
	dfs1(1,0);
	for(int i=1;i<=n;i++)
		push(i,dep[i],dep[i],1);
	dfs2(1,0);
	for(int i=1; i<=tot; i++)
//		cout<<"***"<<ans[i]<<endl;
		cout<<ans[i]<<endl;
}
/*
2 2
1 2
1 2
2 1

3 2
1 2
2 3
1 1
2 2
*/

其他题目

P3224
CF600E
P3899
P1600
P5298

线段树分裂

一颗动态开点权值线段树的前 r a n k rank rank个元素和其他元素分开,形成两颗动态开点权值线段树的过程叫做线段树分裂。

线段树分裂原理

split(u,rank)表示把以 u u u为根的权值线段树分裂为排名为 [ 1 , r a n k ] [1,rank] [1,rank]的一部分和 ( r a n k , s u m u ] (rank,sum_u] (rank,sumu]的另一部分。
并且左半部分根节点仍然为 u u u,返回右半部分的根节点编号 v v v

则过程是这样的:

  • u u u没有儿子,则暴力分裂
  • k = s u m l c u k=sum_{lc_u} k=sumlcu,若 r a n k ≤ k rank\leq k rankk,则递归到左儿子
  • 否则递归到右儿子
  • 上传两棵树的懒标记(push_up(u),push_up(v)

实现起来是这样的:

  • 创建新点并复制 l , r l,r l,r信息
  • 均无左右儿子则暴力分裂
  • 按顺序递归

代码:

int split(int u,long long rank) {
	//u->名次[1,rank]
	//v->名次(rank,sum]
	int v=++tot;创建节点v
	t[v]=t[u];	复制u的信息
	t[v].lc=t[v].rc=0;儿子信息不需要复制
	if(!t[u].lc&&!t[u].rc) {t[u].sum=rank;t[v].sum-=rank;return v;}没儿子就返回
	long long k=t[t[u].lc].sum;
	if(rank<=k) t[v].lc=split(lc(u),rank),swap(t[u].rc,t[v].rc);
	注意swap()的意思是,把u的右儿子给v
	else t[v].rc=split(rc(u),rank-k);
	注意一定是split(lc(u)),split(rc(u)),而不是split(t[u].lc),split(t[u].rc)
	因为递归下去之后新创建的节点v还需要复制u的信息,因此u的儿子必须先被创建,这样才能有l,r的信息
	push_up(u);
	push_up(v);
	return v;
}

例题1

时间复杂度分析

线段树合并的总复杂度为 O ( s u m ) O(sum) O(sum) s u m sum sum为程序执行中创建的所有节点数目的总和。

这种方式分裂得到的树,如果原树的外节点只有叶子,那么新树的外节点也只有叶子。换句话说,如果原树只有单点修改,可以认为新树也是由单点修改操作形成的动态开点线段树。

任意操作最多创建 O ( log ⁡ n ) O(\log n) O(logn)个节点。
线段树合并的总时间复杂度为 O ( m log ⁡ n ) O(m\log n) O(mlogn)
或许把节点数看做势能更清楚一点。

线段树分裂的复杂度显然为 O ( log ⁡ n ) O(\log n) O(logn)

算法总复杂度为 O ( ( n + m ) log ⁡ n ) O((n+m)\log n) O((n+m)logn)

空间复杂度分析

  • 初始的建树操作至多创建 2 n 2n 2n个节点
  • 每次单点修改操作至多创建 ⌈ log ⁡ 2 2 × 1 0 5 ⌉ = 20 \lceil\log_2{2\times 10^5}\rceil=20 log22×105=20个节点
  • 线段树合并不创建新节点
  • 线段树分裂对于新生成的一棵树来说,最多创建一条链,即 20 20 20个节点,除此以外原树还有可能创建 1 1 1个节点。因此一次split至多创建 21 21 21个节点(大概分析)
  • 注意到一次0操作最多进行两次split,因此至多创建 42 42 42个节点

因此空间大致开到 2 n + 42 m 2n+42m 2n+42m即可。但是会发现如果要正常存储 l c , r c , s u m lc,rc,sum lc,rc,sum,空间开到这么大大概需要 140 M B 140MB 140MB,当然实际上空间是会小很多的。也可能是我不太会分析吧。
因此我们尽量开大一点,例如开到 25 n 25n 25n

空间回收是没有用的,因为不影响最劣情况。

#include<iostream>
#include<cstdio>
using namespace std;
const int N=2e5;
struct node {
	int l,r,lc,rc;
	long long sum;
} t[N*25];
int tot;
void push_up(int u) {
	t[u].sum=t[t[u].lc].sum+t[t[u].rc].sum;
}
int&lc(int u) {
	if(t[u].lc) return t[u].lc;
	t[++tot]= {t[u].l,t[u].l+t[u].r>>1};
	return t[u].lc=tot;
}
int&rc(int u) {
	if(t[u].rc) return t[u].rc;
	t[++tot]= {(t[u].l+t[u].r)/2+1,t[u].r};
	return t[u].rc=tot;
}
void push(int u,int l,int r,int val) {
	if(l<=t[u].l&&t[u].r<=r) t[u].sum+=val;//只有单点加
	else {
		int mid=t[u].l+t[u].r>>1;
		if(l<=mid) push(lc(u),l,r,val);
		if(mid<r) push(rc(u),l,r,val);
		push_up(u);
	}
}
long long find(int u,int l,int r) {
	if(l<=t[u].l&&t[u].r<=r) return t[u].sum;
	int mid=t[u].l+t[u].r>>1;
	long long ans=0;
	if(l<=mid&&t[u].lc) ans+=find(lc(u),l,r);
	if(mid<r&&t[u].rc) ans+=find(rc(u),l,r);
	return ans;
}
int merge(int u,int v) { //merge:u->v
	if(!u|!v) return u|v;
	if(!t[u].lc&&!t[u].rc&&!t[v].lc&&!t[v].rc) {
		t[v].sum+=t[u].sum;
		return v;
	}
	t[v].lc=merge(t[u].lc,t[v].lc);
	t[v].rc=merge(t[u].rc,t[v].rc);
	push_up(v);
	return v;
}
int split(int u,long long rank) {
	//u->名次[1,rank]
	//v->名次(rank,sum]
	int v=++tot;
	t[v]=t[u];
	t[v].lc=t[v].rc=0;
	if(!t[u].lc&&!t[u].rc) {
		t[u].sum=rank;
		t[v].sum-=rank;
		return v;
	}
	long long k=t[t[u].lc].sum;
	if(rank<=k) t[v].lc=split(lc(u),rank),swap(t[u].rc,t[v].rc);
	else t[v].rc=split(rc(u),rank-k);
	push_up(u);
	push_up(v);
	return v;
}
int findr(int u,long long&rank) {
	if(t[u].sum<rank) {
		rank-=t[u].sum;
		return -1;
	}
	if(t[u].l==t[u].r) return t[u].l;
	int ans=-1;
	if(t[u].lc) ans=findr(lc(u),rank);
	if(!~ans&&t[u].rc) ans=findr(rc(u),rank);
	return ans;
}
void check(int u) {
	printf("%d[%d,%d](%d,%d):%lld\n",u,t[u].l,t[u].r,t[u].lc,t[u].rc,t[u].sum);
	if(t[u].lc) check(t[u].lc);
	if(t[u].rc) check(t[u].rc);
}
int main() {
//	cout<<sizeof t/1e6;
	int n,m;
	cin>>n>>m;
	for(int i=1; i<=N; i++) t[++tot]= {1,n};
	for(long long i=1,x; i<=n; i++) cin>>x,push(1,i,i,x);
	int cnt=1;
	while(m--) {
		int op;
		cin>>op;
		if(op==0) {
			int x,y,z,l,r;
			cin>>x>>l>>r;
			long long k1=find(x,1,r),
				k2=find(x,l,r);
			注意排名有可能会爆int,一定要注意开long long
			z=split(x,k1);
			y=split(x,k1-k2);
			t[++cnt]=t[y];
			merge(z,x);
		}
		if(op==1) {
			int p,t;
			cin>>p>>t;
			merge(t,p);
		}
		if(op==2) {
			int p,x,q;
			cin>>p>>x>>q;
			push(p,q,q,x);
		}
		if(op==3) {
			int p,x,y;
			cin>>p>>x>>y;
			cout<<find(p,x,y)<<endl;
		}
		if(op==4) {
//			throw;
			int p;
			long long k;
			cin>>p>>k;
			if(t[p].sum<k) cout<<-1<<endl;
			else cout<<findr(p,k)<<endl;
		}
		if(op==5) {
			int x;
			cin>>x;
			check(x); 
		}
	}
}
/*
3 10000
1 1 1
0 1 2 2
3 1 2 2


5 10000
1 1 1 1 1
0 1 2 4
2 2 1 4
3 2 2 4
1 1 2
4 1 3

3 100000
3 0 3 
0 1 1 1
1 2 1
0 2 3 3
3 2 3 3

5 10000
2 2 0 3 0 
0 1 5 5
0 2 3 4
0 2 3 4
1 4 1
0 3 4 4
1 3 4
3 2 3 4
1 5 2
3 3 5 5
3 3 1 4

*/

例题2

题目分析:
本题离线可以使用二分技巧来完成单点询问。

考虑到一个区间被排序后会形成一个有序序列,可以简单的用权值线段树来维护。
并且区间排序是可以颜色段均摊的,因此可以使用颜色段均摊+权值线段树分裂合并来维护。

时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)

时间复杂度分析

颜色段均摊的时间复杂度:
一次修改只可能创建 O ( 1 ) O(1) O(1)个颜色段,总颜色段数: O ( n + n ) O(n+n) O(n+n)

  • 使用缩点平衡树维护颜色段的复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

一次修改至多可能分裂 2 2 2个颜色段(与 l , r l,r l,r相交的两个颜色段),总颜色段分裂数: O ( m ) O(m) O(m)
-线段树分裂的总复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

记势能值为初始的所有动态开点权值线段树的相交节点数,初始为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

  • 分裂操作不增加势能值
  • 合并操作使用 O ( 1 ) O(1) O(1)的代价消除 O ( 1 ) O(1) O(1)的势能
  • 合并总复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)

总复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)

可持久化线段树

可持久化线段树,即支持访问历史版本的线段树。
例如对线段树 T 0 T_0 T0进行 m m m次修改操作,分别得到: T 0 , T 1 , T 2 , . . . , T m T_0,T_1,T_2,...,T_m T0,T1,T2,...,Tm,现在要支持访问 T i ∈ [ 0 , m ] T_{i\in[0,m]} Ti[0,m]

可持久化线段树原理

对于线段树 T T T,对其进行一次操作 X X X,得到线段树 T ′ T' T
X X X为单点修改,则 T T T T ′ T' T只有一条链上的节点可能不同
X X X为区间修改,则 T T T T ′ T' T只有约 4 log ⁡ 2 n 4\log_2 n 4log2n个节点可能不同

T ′ T' T对于可能不同的节点新开,其余节点借用 T T T的节点。

在这里插入图片描述
可持久化线段树每个版本都有自己的根。
从每个历史版本的根开始递归,都好像是访问一颗满线段树,但事实上一个版本上只有 O ( log ⁡ n ) O(\log n) O(logn)个节点是新创建的,其余的节点都是借用的历史版本。

对可持久化线段树进行修改操作的代码略有不同。对可持久化线段树进行查询操作与对普通的线段树进行查询没有任何区别。

显然可持久化线段树不能下放懒标记,因此如果要支持区间修改,就必须要标记永久化。

主席树指的是可持久化权值线段树,由黄嘉泰(HJT)提出。
由于线段树的区间树和权值树没有区别,因此可持久化线段树一般使用主席树。

处理继承历史版本的方法一般是这样的:

  • 初次建树时,建立一个满的线段树
  • 修改操作同时维护历史版本的对应节点和当前版本的对应节点
  • 把历史版本的信息复制到当前节点上
  • 修改在当前节点的哪个儿子上,当前节点就新建那个儿子然后递归。没有修改的儿子借用历史版本
void build(int u,int l,int r){通常主席树的写法需要在初始进行一次建树
	t[u]={l,r};
	if(l==r) return;
	int mid=l+r>>1;
	build(t[u].lc=++tot,l,mid);
	build(t[u].rc=++tot,mid+1,r);
}
这里的lc,rc函数不再有创建节点的功能了,因为新建节点仅在push函数中,递归进去之后会复制历史版本的信息,不需要手动设置t[u].l,t[u].r这些信息了。
int&lc(int u){
	return t[u].lc;
}
int&rc(int u){
	return t[u].rc;
}
void push(int h,int u,int l,int r,int val){
	t[u]=t[h];复制历史信息
	if(l<=t[u].l&&t[u].r<=r) t[u].val=val;
	else {
		int mid=t[u].l+t[u].r>>1;
		if(l<=mid) push(lc(h),lc(u)=++tot,l,r,val);
		if(mid<r) push(rc(h),rc(u)=++tot,l,r,val);
		//push_up(u);
	}
}

时空复杂度分析

对主席树进行一次修改/操作的时间复杂度显然为 O ( log ⁡ n ) O(\log n) O(logn)

假设值域大小为 n n n,主席树初始建树有 2 n − 1 2n-1 2n1个节点,此后每一次修改操作,如果是单点修改,则至多会创建 ⌈ log ⁡ 2 n ⌉ = log ⁡ 2 n + 1 \lceil\log_2 n\rceil=\log_2n+1 log2n=log2n+1个节点,因此总节点数为 2 n − 1 + m ( log ⁡ 2 n + 1 ) 2n-1+m(\log_2n+1) 2n1+m(log2n+1)

如果 n , m n,m n,m数值上完全相等而不是同阶),则可以认为总节点数为 n ( log ⁡ 2 n + 3 ) − 1 n(\log_2n+3)-1 n(log2n+3)1,由于通常从 1 1 1开始存储节点,因此数组大小开到 n ( log ⁡ 2 n + 3 ) n(\log_2n+3) n(log2n+3)即可。

如果是区间修改,则一次修改操作可能创建 4 ⌈ log ⁡ 2 n ⌉ 4\lceil\log_2n\rceil 4log2n个节点。总节点数为 2 n − 1 + 4 m ( log ⁡ 2 n + 1 ) 2n-1+4m(\log_2n+1) 2n1+4m(log2n+1)

适当开大一些。

例题1

本题略微卡常,需要进行读写优化
实现:

#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e6;
int a[N+5];
struct node{
	int l,r,lc,rc;
	int val;
}t[N*25];
理论上空间开到23N
int tot;
int&lc(int u){
	return t[u].lc;
}
int&rc(int u){
	return t[u].rc;
}
void build(int u,int l,int r){
	t[u]={l,r,0,0,a[l]};
	if(l==r) return;
	int mid=l+r>>1;
	build(t[u].lc=++tot,l,mid);
	build(t[u].rc=++tot,mid+1,r);
}
void push(int h,int u,int l,int r,int val){
	t[u]=t[h];
	if(l<=t[u].l&&t[u].r<=r) t[u].val=val;
	else {
		int mid=t[u].l+t[u].r>>1;
		if(l<=mid) push(lc(h),lc(u)=++tot,l,r,val);
		if(mid<r) push(rc(h),rc(u)=++tot,l,r,val);
		//push_up(u);
	}
}
int find(int u,int l,int r){
	if(l<=t[u].l&&t[u].r<=r) return t[u].val;
	int mid=t[u].l+t[u].r>>1;
	if(l<=mid) return find(lc(u),l,r);
	if(mid<r) return find(rc(u),l,r);
	throw;
}
int root[N+5];
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	build(root[0]=++tot,1,n);
	for(int i=1;i<=m;i++) {
		int v,op;
		scanf("%d%d",&v,&op);
		if(op==1) {
			int loc,val;
			scanf("%d%d",&loc,&val);
			push(root[v],root[i]=++tot,loc,loc,val);
		}
		else {
			int loc;
			scanf("%d",&loc);
//			puts("***");
			printf("%d\n",find(root[v],loc,loc));
			t[root[i]=++tot]=t[root[v]];
		}
	}
}

例题2

静态区间第 k k k问题。

如果我们能够知道一个区间的值域线段树的情况,那么我们就可以在值域线段树上二分来获得第 k k k小。

我们能够用主席树维护出每个前缀的值域线段树,也就是说,我们用第 i i i个历史版本,表示 [ 1 , i ] [1,i] [1,i]的值域线段树。那我们就可以利用线段树二分轻易完成在线查询每个前缀的第 k k k小。

进一步的,如果我们能够把两个前缀时刻 l − 1 , r l-1,r l1,r的桶数组的桶数组对应位置相减,维护出相减之后的值域线段树,就相当于是区间 [ l , r ] [l,r] [l,r]的值域线段树,然后我们就可以在新的值域线段树上二分,来得到 [ l , r ] [l,r] [l,r]的区间第 k k k小。

但是直接对权值线段树相减是非常不优的,我们注意到,线段树二分时只可能会访问到 O ( log ⁡ n ) O(\log n) O(logn)的线段树节点,因此我们可以等到真的要访问一个相减之后的线段树节点时,才计算它的值,这样复杂度就被压缩为了 O ( log ⁡ n ) O(\log n) O(logn)

把下标作为第一维,把值域作为第二维,其实我们可以认为这是在对序列进行扫描线。

实现

#include<iostream>
#include<algorithm>
using namespace std;
const int N=2e5;
struct node{
	int l,r,lc,rc;
	int sum;
}t[N*25];
int tot;
void push_up(int u){
	t[u].sum=t[t[u].lc].sum+t[t[u].rc].sum;
}
int&lc(int u){
	return t[u].lc;
}
int&rc(int u){
	return t[u].rc;
}
void build(int u,int l,int r){
	t[u]={l,r};
	if(l==r) return;
	int mid=l+r>>1;
	build(lc(u)=++tot,l,mid);
	build(rc(u)=++tot,mid+1,r);
}
void push(int h,int u,int l,int r,int val){
	t[u]=t[h];
	if(l<=t[u].l&&t[u].r<=r) t[u].sum+=val;
	else{
		int mid=t[u].l+t[u].r>>1;
		if(l<=mid) push(lc(h),lc(u)=++tot,l,r,val);
		if(mid<r) push(rc(h),rc(u)=++tot,l,r,val);
		push_up(u);
	}
}
int findr(int h,int u,int&rank){
	if(t[u].sum-t[h].sum<rank) {
		rank-=t[u].sum-t[h].sum;
		return -1;
	}
	if(t[u].l==t[u].r) return t[u].l;
	int ans=findr(lc(h),lc(u),rank);
	if(!~ans) ans=findr(rc(h),rc(u),rank);
	return ans;
} 
int root[N+5];
int a[N+5],b[N+5];
int main() {
	int n,m;
	cin>>n>>m;
	build(root[0]=++tot,1,n);
	for(int i=1;i<=n;i++) cin>>a[i],b[i]=a[i];
	sort(b+1,b+1+n);
	int*t=unique(b+1,b+1+n);
	for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,t,a[i])-b;
	for(int i=1;i<=n;i++) push(root[i-1],root[i]=++tot,a[i],a[i],1);
	while(m--){
		int l,r,k;
		cin>>l>>r>>k;
		cout<<b[findr(root[l-1],root[r],k)]<<endl;
	}
}

后记

于是皆大欢喜。

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值