线段树基础(下)

news2024/11/28 14:39:13

线段树二分

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

一些序列上最左/最右位置问题可以二分解决,同时需要使用线段树进行查询。时间复杂度通常是 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)

实现起来是这样的:

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

代码:

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

后记

于是皆大欢喜。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1356819.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器学习(四) -- 模型评估(2)

系列文章目录 机器学习&#xff08;一&#xff09; -- 概述 机器学习&#xff08;二&#xff09; -- 数据预处理&#xff08;1-3&#xff09; 机器学习&#xff08;三&#xff09; -- 特征工程&#xff08;1-2&#xff09; 机器学习&#xff08;四&#xff09; -- 模型评估…

B端产品经理学习-对用户进行需求挖掘

目录&#xff1a; 用户需求挖掘的方法 举例&#xff1a;汽车销售系统的用户访谈-前期准备 用户调研提纲 预约用户做访谈 用户访谈注意点 我们对于干系人做完调研之后需要对用户进行调研&#xff1b;在C端产品常见的用户调研方式外&#xff0c;对B端产品仍然适用的 用户需…

6.1810: Operating System Engineering 2023 <Lab6: Multithreading>

一、本节任务 二、要点 2.1 锁&#xff08;Locking&#xff09; 在多 CPU 或者单 CPU 多线程并发的场景中&#xff0c;对临界资源&#xff08;或者说共享资源&#xff09;的访问如果不加以限制&#xff0c;可能会引发一些严重的问题&#xff0c;比如当两个线程同时对一个共享…

Python初探:从零开始的编程奇妙之旅

一、Python是什么 Python是一门多用途的高级编程语言&#xff0c;以其简洁、易读的语法而脱颖而出。在深度学习领域&#xff0c;Python扮演着至关重要的角色。其丰富的科学计算库&#xff08;如NumPy、Pandas、Matplotlib&#xff09;和强大的深度学习框架&#xff08;如Tenso…

jmeter参数化的三种方式

1.用户定义变量 使用变量&#xff1a; ${变量名} 这个变量是全局变量&#xff0c;也就是在下面子节点中都可以使用&#xff1b; 使用场景&#xff1a;两个账号分别有不同的权限&#xff0c;A经办&#xff0c;B审核。等。。。 2.CSV数据文件设置 3.函数

案例071:基于微信小程序的汽车预约维修系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

jupyter更改默认路径到其它的目录或者到其它的盘 比如D盘

1.打开终端 输入jupyter notebook --generate-config 如下 2.在C:\Users\mb5958\.jupyter路径下 3.用记事本打开它&#xff0c;搜索directory 4.在你想要的路径下新建一个文件夹&#xff0c;如‘D:\jupyterFile’&#xff0c;然后将路径名放在c.NotebookApp.notebook_dir"…

卷麻了,00后测试用例写的比我还好,简直无地自容...........

经常看到无论是刚入职场的新人&#xff0c;还是工作了一段时间的老人&#xff0c;都会对编写测试用例感到困扰&#xff1f;例如&#xff1a; 如何编写测试用例&#xff1f; 作为一个测试新人&#xff0c;刚开始接触测试&#xff0c;对于怎么写测试用例很是头疼&#xff0c;无法…

【PCB专题】Allegro封装更新焊盘

在PCB封装的绘制中&#xff0c;有时会出现需要更新焊盘的情况。比如在制作封装的过程中发现焊盘做的不对而使用PAD_Designer重新更新了焊盘。 那在PCB中如何更新已经修改过的焊盘呢&#xff1f; 打开封装&#xff0c;选择Tools->Padstack->Refresh... 选择Refresh all …

让 sdk 包静默升级的 SAO 操作,你见过几种?

拓展阅读 让 sdk 包静默升级的 SAO 操作&#xff0c;你见过几种&#xff1f; 业务背景 有时候为业务方提供了基础的 sdk 包&#xff0c;为了保证稳定性&#xff0c;一般都是 release 包。 但是每一次升级都非常痛苦&#xff0c;也不可能写一个一步到位的 jar 包&#xff0c…

javascript 常见工具函数(三)

21.克隆数组的几种方法&#xff1a; &#xff08;1&#xff09;slice方法&#xff1a; let arr [1,2,3,4] let arr1 arr.slice() //或者是 let arr1 arr.slice(0) arr[0] 6 console.log(arr) // [6, 2, 3, 4] console.log(arr1) // [1, 2, 3, 4] &#xff08;2&…

Android Jetpack学习系列——Navigation

写在前面 Google在2018年就推出了Jetpack组件库&#xff0c;但是直到今天我才给重视起来&#xff0c;这真的不得不说是一件让人遗憾的事。过去几年的空闲时间里&#xff0c;我一直在尝试做一套自己的组件库&#xff0c;帮助自己快速开发&#xff0c;虽然也听说过Jetpack&#…

Stable Diffusion模型概述

Stable Diffusion 1. Stable Diffusion能做什么&#xff1f;2. 扩散模型2.1 正向扩散2.2 反向扩散 3. 训练如何进行3.1 反向扩散3.2 Stable Diffusion模型3.3 潜在扩散模型3.4 变分自动编码器3.5 图像分辨率3.6 图像放大 4. 为什么潜在空间是可能的&#xff1f;4.1 在潜在空间中…

【智慧地球】星图地球 | 星图地球超算数据工场

当前空天信息处理涉及并发并行的大量计算问题&#xff0c;需要高性能计算、智能计算联合调度&#xff0c;以此来实现多算力融合&#xff1b;而我国算力产业规模快速增长&#xff0c;超算算力资源正需要以任务驱动来统筹。 基于此&#xff0c;中科星图与郑州中心展开紧密合作&a…

Qt学习_17_一些关于QTableWidget的记录

1 QTableWidget::clear() 程序异常退出 近日&#xff0c;项目中使用到QTableWidget&#xff0c;遇到一个问题&#xff0c;项目需要清空这个表格&#xff0c;但是无论调用clear()&#xff0c;clearContents()&#xff0c;程序都报&#xff1a;程序异常退出。 而且项目程序还比较…

OpenVINS学习5——VioManager.cpp/h学习与注释

前言 之前又看到说VioManager.cpp/h是OpenVINS中的核心程序&#xff0c;这次就看看这里面都写了啥&#xff0c;整体架构什么样&#xff0c;有哪些函数功能。具体介绍&#xff1a; VioManager类 整体分析 VioManager类包含 MSCKF 工作所需的状态和其他算法。我们将测量结果输…

二维码地址门牌管理系统:物业管理的未来趋势

文章目录 前言一、数字化管理与便捷服务二、身份认证与安全保障三、业主便利与贴心服务四、未来发展趋势 前言 在数字化时代&#xff0c;物业管理面临着不断增加的挑战。为了提高管理效率、服务业主&#xff0c;二维码门牌管理系统应运而生。本文将探讨这一新型管理方式&#…

【OpenBMC】的内部README 模板

OpenBMC 本项目的AST2500分支核心代码的机型是ast2500-default&#xff0c;克隆代码后进入编译环境的命令为&#xff1a; source setup ast2500-default 一、源码下载、配置以及编译 重要&#xff1a;请参阅confluence 详细步骤 二、代码使用方法 目前所有自定义修改的代码…

虚拟机添加显示屏

1、关闭虚拟机&#xff0c;虚拟机在为关机的情况下&#xff0c;虚拟机设置->显示器->监视器 都是灰色的&#xff0c;不能设置&#xff1b; 2、虚拟机设置->显示器->监视器 “监视器数量” 设置为2 “拉伸模式” 不要勾选 点确定 3、点击 查看->循环使用多个…

蜥蜴目标检测数据集VOC格式1400张

蜥蜴&#xff0c;一种爬行动物&#xff0c;以其独特的形态和习性&#xff0c;成为了人们关注的焦点。 蜥蜴的外观多样&#xff0c;体型大小不一。它们通常拥有长条的身体、四肢和尾巴&#xff0c;鳞片覆盖全身&#xff0c;这使得它们能够在各种环境中轻松移动。大多数蜥蜴拥有…