线段树,线段树合并,主席树

本文深入探讨线段树的多种实现,包括基本操作、区间加法、区间乘法、区间最值等,以及权值线段树、动态开点线段树和主席树的构建与应用。通过实例分析和代码展示,阐述线段树在区间查询和修改上的高效性,以及在解决复杂问题时的灵活性。
摘要由CSDN通过智能技术生成

一.线段树的实质及基本操作

  • 线段树实际上是一棵完全二叉树,将需要维护的数组放在叶子节点上,将子节点的值相关值维护在根节点之中,访问时,只需要查询到能将答案覆盖的最上面的根即可

  • 在静态存储而非动态开点的情况下,i 节点的子节点是 2i 和 2i+1 ,i 节点的父节点是 i/2,则 i 节点要保存 2i,2i+1的相关值,这里相关值以和为例:设tree[now]表示以now为根的子树点权和,l,r是需要维护的数组的下标

  • ps:线段树需要四倍空间

基本操作如下:

1.建树

void build(int now,int l,int r){
	if(l==r){
		tree[now]=a[l];
		return;
	}
	int mid=(l+r)/2;
	build(now*2,l,mid);
	build(now*2+1,mid+1,r);
	tree[now]=tree[now*2]+tree[now*2+1];
}

2.单点修改

void one_update(int now,int l,int r,int x,int k){
	if(l==r){
		tree[now]+=k;
		return;
	}
	int mid=(l+r)/2;
	if(x<=mid)one_update(now*2,l,mid,x,k);
	else one_update(now*2,mid+1,r,x,k);
	tree[now]=tree[now*2]+tre[now*2+1];
} 

3.区间查询

int range_query(int now,int l,int r,int ql,int qr) {
	if(ql<=l&&r<=qr)return tree[now];
	int mid=(l+r)/2,ans=0;
    if(ql<=mid)ans+=range_query(now*2,l,mid,ql,qr);
    if(qr>mid)ans+=range_query(now*2+1,mid+1,r,ql,qr);
    return ans;
}

4.区间修改+区间查询(带懒标记)

void pushup(int now){
	tree[now]=tree[now*2]+tree[now*2+1];
}
void pushdown(int now,int l,int r,int mid){
	if(lazy[now]){
		tree[now*2]+=(mid-l+1)*lazy[now];
		tree[now*2+1]+=(r-mid)*lazy[now];
		lazy[now*2]+=lazy[now];
		lazy[now*2+1]+=lazy[now];
		lazy[now]=0;
	}
}
void range_update(int now,int l,int r,int ql,int qr,int k){
	if(ql<=l&&r<=qr){
		tree[now]+=(r-l+1)*k;
		lazy[now]+=k;
		return;
	}
	int mid=(l+r)/2;
	pushdown(now,l,r,mid);
    if(ql<=mid)range_update(now*2,l,mid,ql,qr,k);
    if(qr>mid)range_update(now*2+1,mid+1,r,ql,qr,k);
    pushup(now);
}
int range_query(int now,int l,int r,int ql,int qr){
	if(ql<=l&&r<=qr)return tree[now];
	int mid=(l+r)/2,ans=0;
	pushdown(now,l,r,mid);
	if(ql<=mid)ans+=range_query(now*2,l,mid,ql,qr);
	if(qr>mid)ans+=range_query(now*2+1,mid+1,r,ql,qr);
    return ans; 
}

6.求区间最值(可修改的区间最值)

  • 再开一个数组维护区间最值即可。由于区间 +k 会导致区间所有值 +k ,而此时最大值也刚好 +k。
  • 方法:pushup时取左右子树的max,pushdown时将 lazy 值也加上,区间修改时最大值 +k 。
void pushup(ll now) {
	tree[now]=tree[now*2]+tree[now*2+1];
	maxv[now]=maxv(maxv[now*2],maxv[now*2+1]);
}
void pushdown(ll now,ll l,ll r,ll mid) {
	if(lazy[now]) {
		tree[now*2]+=lazy[now]*(mid-l+1);
		tree[now*2+1]+=lazy[now]*(r-mid);
		lazy[now*2]+=lazy[now];
		lazy[now*2+1]+=lazy[now];
		maxv[now*2]+=lazy[now];
		maxv[now*2+1]+=lazy[now];
		lazy[now]=0;
		pushup(now);
	}
}
ll querymaxv(ll now,ll l,ll r,ll ql,ll qr) {
	if(ql<=l&&r<=qr)return maxv[now];
	ll ans=-1e10;
	ll mid=(l+r)/2;
	pushdown(now,l,r,mid);
	if(mid>=ql)ans=max(ans,querymaxv(now*2,l,mid,ql,qr));
	if(mid<qr)ans=max(ans,querymaxv(now*2+1,mid+1,r,ql,qr));
	pushup(now);
	return ans;
}
void update(ll now,ll l,ll r,ll ql,ll qr,ll k) {
	if(ql<=l&&r<=qr) {
		lazy[now]+=k;
		tree[now]+=k*(r-l+1);
		maxv[now]+=k;
		return ;
	}
	ll mid=(l+r)/2;
	pushdown(now,l,r,mid);
	if(mid>=ql)update(now*2,l,mid,ql,qr,k);
	if(mid<qr)update(now*2+1,mid+1,r,ql,qr,k);
	pushup(now);
}

7.区间乘法

  • 区间的每个数都乘于k相当于他们的和乘于k,故让 t r e e [ n o w ] × tree[now]\times tree[now]×k,同时 l a z y [ n o w ] × k lazy[now]\times k lazy[now]×k
void init() {
	for(int i=1; i<=n*4; i++)lazy[i]=1;
}
void pushup(int now) {
	tree[now]=tree[now*2]+tree[now*2+1];
}
void pushdown(int now,int l,int r,int mid) {
	if(lazy[now]>1) {
		tree[now*2]*=lazy[now];
		tree[now*2+1]*=lazy[now];
		lazy[now*2]*=lazy[now];
		lazy[now*2+1]*=lazy[now];
		lazy[now]=1;
	}
}
void update(int now,int l,int r,int ql,int qr,int k) {
	if(ql<=l&&r<=qr) {
		tree[now]*=k;
		lazy[now]*=k;
		return;
	}
	int mid=(l+r)/2;
	pushdown(now,l,r,mid);
	if(ql<=mid)update_chen(now*2,l,mid,ql,qr,k);
	if(qr>mid)update_chen(now*2+1,mid+1,r,ql,qr,k);
	pushup(now);
}

8.区间 gcd

  • pushup 部分不一样而已,其他同理。

二.例题学习

例题1:区间加乘法,维护区间和,区间平方和

  • 问题分析:
  • 既然有乘又有加,那么则需要两个 tag 了,设乘法 tag1 为 a ,加法 tag2 为 b
  • 考虑 pushdown :
  • v i = a x + b v_i=ax+b vi=ax+b v i = A ( a x + b ) + B = A a x + A b + B v_i=A(ax+b)+B=Aax+Ab+B vi=A(ax+b)+B=Aax+Ab+B,即 t a g 1 = A a , t a g 2 = A b + B tag1=Aa,tag2=Ab+B tag1=Aa,tag2=Ab+B
  • 考虑 cal_lazy :
  • ∑ i = l r v i = ∑ i = l r a x + b = a ∑ i = l r x + ( r − l + 1 ) × b \sum_{i=l}^r v_i=\sum_{i=l}^r ax+b=a\sum_{i=l}^rx+(r-l+1)\times b i=lrvi=i=lrax+b=ai=lrx+(rl+1)×b
  • ∑ i = l r v i 2 = ∑ i = l r ( a x + b ) 2 = ∑ i = l r ( a 2 x 2 + 2 a b x + b 2 ) = a 2 ∑ i = l r x 2 + 2 a b ∑ i = l r x + ( r − l + 1 ) × b 2 \sum_{i=l}^r v_i^2=\sum_{i=l}^r (ax+b)^2=\sum_{i=l}^r (a^2x^2+2abx+b^2)=a^2\sum_{i=l}^r x^2+2ab\sum_{i=l}^r x+(r-l+1)\times b^2 i=lrvi2=i=lr(ax+b)2=i=lr(a2x2+2abx+b2)=a2i=lrx2+2abi=lrx+(rl+1)×b2
int sum1[N*4],sum2[N*4],a[N],tag1[N*4],tag2[N*4],n,p,m;

void pushup(int now) {
	sum1[now]=sum1[now*2]+sum1[now*2+1];
	sum2[now]=sum2[now*2]+sum2[now*2+1];
}
void pushdown(int now,int l,int r,int mid) {
	if(tag1[now]!=1||tag2!=0) {
		sum2[now*2]=tag1[now]*tag1[now]*sum2[now*2]+2*tag1[now]*tag2[now]*sum1[now*2]+(mid-l+1)*tag2[now]*tag2[now];
		sum2[now*2+1]=tag1[now]*tag1[now]*sum2[now*2+1]+2*tag1[now]*tag2[now]*sum1[now*2+1]+(r-mid)*tag2[now]*tag2[now];
		sum1[now*2]=sum1[now*2]*tag1[now]+tag2[now]*(mid-l+1);
		sum1[now*2+1]=sum1[now*2+1]*tag1[now]+tag2[now]*(r-mid);
		
		tag1[now*2]=tag1[now]*tag1[now*2];
		tag1[now*2+1]=tag1[now]*tag1[now*2+1];
		tag2[now*2]=tag1[now]*tag2[now*2]+tag2[now];
		tag2[now*2+1]=tag1[now]*tag2[now*2+1]+tag2[now];
		tag1[now]=1;
		tag2[now]=0;
	}
}
void build(int now,int l,int r) {
	tag1[now]=1;
	tag2[now]=0;
	if(l==r) {
		sum1[now]=a[l];
		sum1[now]=a[l]*a[l];
		return;
	}
	int mid=(l+r)/2;
	build(now*2,l,mid);
	build(now*2+1,mid+1,r);
	pushup(now);
}
void update_jia(int now,int l,int r,int ql,int qr,int k) {
	if(ql<=l&&r<=qr) {
		sum2[now]+=2*k*sum1[now]+(r-l+1)*k*k;
		sum1[now]+=k*(r-l+1);
		tag2[now]+=k;
		return;
	}
	int mid=(l+r)/2;
	pushdown(now,l,r,mid);
	if(ql<=mid)update_jia(now*2,l,mid,ql,qr,k);
	if(qr>mid)update_jia(now*2+1,mid+1,r,ql,qr,k);
	pushup(now);
}
void update_chen(int now,int l,int r,int ql,int qr,int k) {
	if(ql<=l&&r<=qr) {
		sum1[now]*=k;
		sum2[now]*=k;
		tag1[now]*=k;
		tag2[now]*=k;
		return;
	}
	int mid=(l+r)/2;
	pushdown(now,l,r,mid);
	if(ql<=mid)update_chen(now*2,l,mid,ql,qr,k);
	if(qr>mid)update_chen(now*2+1,mid+1,r,ql,qr,k);
	pushup(now);
}

例题2:区间加乘法,维护区间两两乘积和

  • 问题分析: pushdown 与上题类似。
  • cal_lazy:
  • ∑ i = l r ∑ j = i + 1 r v i v j = ∑ i = l r ∑ j = i + 1 r ( a x i + b ) ( a x j + b ) = a 2 ∑ i = l r ∑ j = i + 1 r x i x j + a b ( r − l ) ∑ i = l r x i + b 2 ( r − l + 1 ) × ( r − l ) / 2 \sum_{i=l}^r\sum_{j=i+1}^r v_iv_j=\sum_{i=l}^r\sum_{j=i+1}^r (ax_i+b)(ax_j+b)=a^2\sum_{i=l}^r\sum_{j=i+1}^r x_ix_j+ab(r-l)\sum_{i=l}^r x_i+b^2(r-l+1)\times (r-l)/2 i=lrj=i+1rvivj=i=lrj=i+1r(axi+b)(axj+b)=a2i=lrj=i+1rxixj+ab(rl)i=lrxi+b2(rl+1)×(rl)/2

例题3:区间加平方数列,维护区间和

  • 问题分析:
  • 错误思路:
  • 设 tag 为从区间 [l,r] 左端点开始 :+ tag2+(tag+1)2+(tag+2)2
  • pushdown: t a g [ l s o n ] + = t a g [ n o w ] , t a g [ r s o n ] + = t a g [ n o w ] + m i d tag[lson]+=tag[now],tag[rson]+=tag[now]+mid tag[lson]+=tag[now],tag[rson]+=tag[now]+mid
  • cal_lazy: s u m [ ] sum[] sum[] 用前 n 项和做差算。
  • 但是 wa 了,注意,tag 是不能直接累加的, t a g 1 2 + t a g 2 2 ≠ ( t a g 1 + t a g 2 ) 2 tag_1^2+tag_2^2\not = (tag_1+tag_2)^2 tag12+tag22=(tag1+tag2)2
  • 正确思路:
  • ∑ i = l r v i + ( i − l j + 1 ) 2 = ∑ i = l r v i + ( i − x j ) 2 = ∑ i = l r v i + ∑ i = l r i 2 − 2 x j ∑ i = l r i + ∑ i = l r x j 2 \sum_{i=l}^r v_i+(i-l_j+1)^2=\sum_{i=l}^r v_i+(i-x_j)^2=\sum_{i=l}^r v_i+\sum_{i=l}^r i^2-2x_j\sum_{i=l}^r i +\sum_{i=l}^rx_j^2 i=lrvi+(ilj+1)2=i=lrvi+(ixj)2=i=lrvi+i=lri22xji=lri+i=lrxj2
  • v i = 0 v_i=0 vi=0,每次操作就相当于加了一次后面的三个项,其实就是维护区间加而已
  • tag2 记第一项的次数,tag1 记 x j x_j xj 的和,tag0 记 x j 2 x_j^2 xj2 的和
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+100;
const long long mod=1e9+7;
long long sum2[N*4],sum1[N*4],sum[N*4],tag1[N*4],tag2[N*4],tag0[N*4];

void pushup(int now) {
	sum2[now]=(sum2[now*2]+sum2[now*2+1])%mod;
	sum1[now]=(sum1[now*2]+sum1[now*2+1])%mod;
	sum[now]=(sum[now*2]+sum[now*2+1])%mod;
}
void pushdown(int now,int l,int r,int mid) {
	if(tag2[now]){
		sum[now*2]=(sum[now*2]+sum2[now*2]*tag2[now])%mod;
		sum[now*2+1]=(sum[now*2+1]+sum2[now*2+1]*tag2[now])%mod;
		tag2[now*2]+=tag2[now];
		tag2[now*2+1]+=tag2[now];
		tag2[now]=0;
	}
	if(tag1[now]){
		sum[now*2]=(sum[now*2]-2*tag1[now]*sum1[now*2])%mod; 
		sum[now*2+1]=(sum[now*2+1]-2*tag1[now]*sum1[now*2+1])%mod; 
		tag1[now*2]=(tag1[now*2]+tag1[now])%mod;
		tag1[now*2+1]=(tag1[now*2+1]+tag1[now])%mod;
		tag1[now]=0;
	}
	if(tag0[now]){
		sum[now*2]=(sum[now*2]+(mid-l+1)*tag0[now])%mod;
		sum[now*2+1]=(sum[now*2+1]+(r-mid)*tag0[now])%mod;
		tag0[now*2]=(tag0[now*2]+tag0[now])%mod;
		tag0[now*2+1]=(tag0[now*2+1]+tag0[now])%mod;
		tag0[now]=0;
	}
}
void build(int now,int l,int r){
	if(l==r){
		sum2[now]=1ll*l*l%mod;
		sum1[now]=l%mod;
		return;
	}
	int mid=(l+r)/2;
	build(now*2,l,mid);
	build(now*2+1,mid+1,r);
	pushup(now);
}
void update(int now,int l,int r,int ql,int qr) {
	if(ql<=l&&r<=qr) {
		long long x=(ql-1);
		tag2[now]++;
		tag1[now]=(tag1[now]+x)%mod;
		tag0[now]=(tag0[now]+x*x)%mod;
		sum[now]=(sum[now]+sum2[now]+x*x*(r-l+1)-2*x*sum1[now])%mod;
		return;
	}
	int mid=(l+r)/2;
	pushdown(now,l,r,mid);
	if(ql<=mid)update(now*2,l,mid,ql,qr);
	if(qr>mid)update(now*2+1,mid+1,r,ql,qr);
	pushup(now);
}
long long query(int now,int l,int r,int ql,int qr) {
	if(ql<=l&&r<=qr)return sum[now];
	int mid=(l+r)/2;
	pushdown(now,l,r,mid);
	long long ans=0;
	if(ql<=mid)ans+=query(now*2,l,mid,ql,qr);
	if(qr>mid)ans+=query(now*2+1,mid+1,r,ql,qr);
	pushup(now);
	return ans%mod;
}
int main() {
	int n,m,op,l,r;
	scanf("%d%d",&n,&m);
	build(1,1,n);
	for(int i=1; i<=m; i++) {
		scanf("%d%d%d",&op,&l,&r);
		if(op==1)update(1,1,n,l,r);
		else cout<<(query(1,1,n,l,r)%mod+mod)%mod<<'\n';
	}
	return 0;
}

例题4:区间减法,维护最小值

  • 题目描述: 有 n 天,每天有 ai 个空教室。有 m 个订单,第 i 个订单为第 li 天到第 ri 天每天需要 di 个教室。如果所有订单均可成功完成,输出0;否则输出第 1 个不满足的订单。
  • 问题分析: 把 n 天开成一棵 n 个结点的线段树,订单看成区间减法,然后维护最小值。判断最小值是否小于 0 即可。

例题5:区间最大值,以及最大值的个数

例题7:查询第一个 > = a i >=a_i >=ai 的位置,并修改

  • 题目描述: n 堆鸡蛋,每堆鸡蛋 ai 个。n 个篮子,每个篮子最多放 m 个鸡蛋,最多放 k 堆鸡蛋。依次拿起一堆鸡蛋,从编号小的篮子开始找,找到第一个放得下的篮子,并放入。求 n 堆鸡蛋分别放入了哪个篮子中。 n < = 2 e 5 , m < = 1 e 9 , k < = 1 e 6 n<=2e5,m<=1e9,k<=1e6 n<=2e5,m<=1e9,k<=1e6
  • 问题分析: 线段树维护区间剩余的最大值,然后二分的思路:左子树最大值 > = a i >=a_i >=ai , 就去左子树找;否则就去右子树找。然后单调修改即可,对于 k 堆都放完的,值赋为 0
void pushup(int now) {
	tr[now].v=max(tr[now*2].v,tr[now*2+1].v);
}
void build(int now,int l,int r) {
	if(l==r) {
		tr[now].v=m;
		tr[now].cnt=k;
		return;
	}
	int mid=(l+r)/2;
	build(now*2,l,mid);
	build(now*2+1,mid+1,r);
	pushup(now);
}
void update(int now,int l,int r,int x,int a) {
	if(l==r) {
		tr[now].v-=a;
		tr[now].cnt--;
		if(tr[now].cnt<=0)tr[now].v=0;
		return;
	}
	int mid=(l+r)/2;
	if(x<=mid)update(now*2,l,mid,x,a);
	else update(now*2+1,mid+1,r,x,a);
	pushup(now);
}
int query(int now,int l,int r,int a) {
	if(l==r)return l;
	int mid=(l+r)/2;
	if(tr[now*2].v>=a)return query(now*2,l,mid,a);
	else return query(now*2+1,mid+1,r,a);
}
int main() {
	int a;
	cin>>n>>m>>k;
	build(1,1,n);
	for(int i=1; i<=n; i++) {
		scanf("%d",&a);
		if(a>tr[1].v)printf("-1\n");
		else {
			int t=query(1,1,n,a);
			printf("%d\n",t);
			update(1,1,n,t,a);
		}
	}
}

例题8:线段树二分:查询区间第一个满足某条件的点

  • 原理: 找到查询区间后,优先判断左子区间是否有满足的点,如果有就去左子区间找;如果没有就去判断右子区间是否有满足的点。

目标1: 找到区间第一个 > = a i >=a_i >=ai 的点,并 + k

void pushup(int now){
	tr[now]=max(tr[now*2],tr[now*2+1])} 
int query(int now,int l,int r,int ql,int qr,int k) {
	if(ql<=l&&r<=qr) {
		if(l==r) {
			tr[now]+=k;
			return 1;
		}
		int mid=(l+r)/2,flag=0;
		if(tr[now*2]>=k&&!flag)flag=query(now*2,l,mid,ql,qr,k);
		if(tr[now*2+1]>=k&&!flag)flag=query(now*2+1,mid+1,r,ql,qr,k);
		pushup(now); 
		return flag;
	}
	int mid=(l+r)/2,flag=0;
	if(ql<=mid&&tr[now*2]>=k&&!flag)flag=query(now*2,l,mid,ql,qr,k);
	if(qr>mid&&tr[now*2+1]>=k&&!flag)flag=query(now*2+1,mid+1,r,ql,qr,k);
	pushup(now);
	return flag;
}

目标2: 找到区间第一个 > = a i >=a_i >=ai 并且加次数不超过 n u m i num_i numi的点, + k

void pushup(int now){
	tr[now]=max(tr[now*2],tr[now*2+1]); 
} 
int query(int now,int l,int r,int ql,int qr,int k) {
	if(ql<=l&&r<=qr) {
		if(l==r) {
			tr[now]+=k;
			num[l]--;
			if(num[l]==0)tr[now]=-1;
			return 1;
		}
		int mid=(l+r)/2,flag=0;
		if(tr[now*2]>=k&&!flag)flag=query(now*2,l,mid,ql,qr,k);
		if(tr[now*2+1]>=k&&!flag)flag=query(now*2+1,mid+1,r,ql,qr,k);
		pushup(now); 
		return flag;
	}
	int mid=(l+r)/2,flag=0;
	if(ql<=mid&&tr[now*2]>=k&&!flag)flag=query(now*2,l,mid,ql,qr,k);
	if(qr>mid&&tr[now*2+1]>=k&&!flag)flag=query(now*2+1,mid+1,r,ql,qr,k);
	pushup(now);
	return flag;
}

例题9.1:线段树维护环的前缀和

  • 题目描述: 一个环 n 个数,求任意点 i 开始求 p r e pre pre,快速维护 p r e pre pre 的信息
  • 问题分析: 倍长,进行一次前缀和,得到 p r e pre pre 数组。从第 i 个点开始的前缀和为 p r e pre pre 数组的 [ i , i + n − 1 ] − p r e i − 1 [i,i+n-1]-pre_{i-1} [i,i+n1]prei1 。可以通过区间修改来得到任意一个点开始的前缀和。

例题9.2:线段树维护环的前缀和

  • 题目描述: 一个环 n 个数,从任意点 i 开始求 p r e − ( j − i + 1 ) × g o a l pre-(j-i+1)\times goal pre(ji+1)×goal,快去维护其信息。
  • 问题分析: 从第 i 个点转移至第 i+1 点开始的前缀和, p r e pre pre 中除第 i 点以外,其他都减去 a i − g o a l a_i-goal aigoal,第 i 个点加上除第 i 个点以外的所有 a i − g o a l a_i-goal aigoal

例题10:区间覆盖

  • 题目描述: 给定 n 个线段 [ l i , r i ] [l_i,r_i] [li,ri] ,值为 v i v_i vi,按顺序依次使用,覆盖 1 − n 1-n 1n 。规则如下:当一个线段 [ l i , r i ] [l_i,r_i] [li,ri] 出现,设 j ∈ [ l i , r i ] j\in[l_i,r_i] j[li,ri] ,若 a j = 0 a_j=0 aj=0 (未赋值),则 a j = v i a_j=v_i aj=vi;否则 a j a_j aj 值不变。
  • 问题分析: 如果区间 m i n = 0 min=0 min=0 ,则区间内有未赋值点,进入修改即可。
void pushup(int now){
	tr[now]=min(tr[now*2],tr[now*2+1]); 
} 
void query(int now,int l,int r,int ql,int qr,int k) {
	if(ql<=l&&r<=qr) {
		if(l==r){
			if(tr[now]==0)tr[now]=k;
			return;
		}
		int mid=(l+r)/2;
		if(tr[now*2]==0)query(now*2,l,mid,ql,qr,k);
		if(tr[now*2+1]==0)query(now*2+1,mid+1,r,ql,qr,k);
		pushup(now); 
	}
	int mid=(l+r)/2;
	if(ql<=mid&&tr[now*2]==0)query(now*2,l,mid,ql,qr,k);
	if(qr>mid&&tr[now*2+1]==0)query(now*2+1,mid+1,r,ql,qr,k);
	pushup(now);
}

例题11:带修改的最大子序列和

  • 题目描述: 最初 n n n 个数 a i a_i ai m m m 次操作。操作1:单点修改。操作2:查询最大子序列和。
  • 问题分析: 线段树维护区间 [ l , r ] [l,r] [l,r] 的三个值。 l x [ l , r ] lx[l,r] lx[l,r] 表示从区间左端点 l l l 开始的最大前缀子序列和, r x [ l , r ] rx[l,r] rx[l,r] 表示从区间右端点 r r r 开始的最大后缀子序列和, m x [ l , r ] mx[l,r] mx[l,r] 表示区间 [ l , r ] [l,r] [l,r] 的最大子序列和。
  • d p dp dp p u s h u p pushup pushup 合并
  • l x [ l , r ] = m a x ( l x [ l , m i d ] , s u m [ l , m i d ] + l x [ m i d + 1 , r ] ) lx[l,r]=max(lx[l,mid],sum[l,mid]+lx[mid+1,r]) lx[l,r]=max(lx[l,mid],sum[l,mid]+lx[mid+1,r])
  • r x [ l , r ] = m a x ( r x [ m i d + 1 , r ] , s u m [ m i d + 1 , r ] + r x [ l , m i d ] ) rx[l,r]=max(rx[mid+1,r],sum[mid+1,r]+rx[l,mid]) rx[l,r]=max(rx[mid+1,r],sum[mid+1,r]+rx[l,mid])
  • m x [ l , r ] = m a x ( m x [ l , m i d ] , m x [ m i d + 1 , r ] , r x [ l , m i d ] + l x [ m i d + 1 , r ] mx[l,r]=max(mx[l,mid],mx[mid+1,r],rx[l,mid]+lx[mid+1,r] mx[l,r]=max(mx[l,mid],mx[mid+1,r],rx[l,mid]+lx[mid+1,r]
void pushup(int now,int l,int r,int mid){
	lx[now]=max(lx[now*2],sum[now*2]+lx[now*2+1]);
	rx[now]=max(rx[now*2+1],sum[now*2+1]+rx[now*2]);
	mx[now]=max{max(mx[now*2],mx[now*2+1]),rx[now*2]+lx[now*2+1]};
}

例题12:维护区间离散化后与原来不同的个数

三.权值线段树

  • 一般线段树是维护原数组 a i a_i ai ,而权值线段树是值为 i i i 的一些相关信息,相当于维护值域。例如:维护一个桶,我们化值域为数组区间大小,节点的权值,就是该数组下标数字所存在的个数。
  • 【注意】值域过大时,可能还要采取离散化的方式

例题1:权值线段树查询第 k 小

  • 问题分析:递归找区间第 k 小,假如左区间的权值和小于等于 k,那么区间第 k 大一定在左区间内,我们递归到左区间去找区间第 k 大;假如左区间的权值和大于 k,那么区间第 k 大一定在右区间内,我们递归到右区间区找区间第 k − s u m [ l , r ] k-sum[l,r] ksum[l,r] 大。当 l==r 时,显然 l 必然时区间第 k 大的数。

Code

int query(int now,int l,int r,int k){
	if(l==r)return tree[now].v;
	int mid=(l+r)/2;
	if(k<=mid)return query(tree[now].l,l,mid,k);
	else return query(tree[now].r,mid+1,r,k-tree[tree[now].l].v);
}

四.动态开点线段树

  • 在后面的许多操作中,静态存储已经无法满足问题需求,因此需要动态开点。
  • 其实就是线段树上的每个结点的左右孩子不再是 2 n o w 2now 2now 2 n o w + 1 2now+1 2now+1 。我们通过递归子树返回根的来告知其左右孩子的编号。
  • 因此我们一个结构体,结构体成员有 :l,r 存储左右孩子编号,其他存储其他数据(区间和,区间最值等);
  • 当需要到达一个没有被赋予编号的点,我们让其等于 + + c n t ++cnt ++cnt

形式下面

int build(int now,int l,int r){
	now=++cnt;
	if(l==r){
		tree[now].v=a[l];
		return now;
	}
	int mid=(l+r)/2;
	tree[now].l=build(tree[now].l,l,mid);
	tree[now].r=build(tree[now].r,mid+1,r);
	pushup(now);
	return now;
} 
int change(int now,int l,int r,int x,int k) {
	if(now==0) now=++cnt;
	if(l==r) {
		tree[now].v+=k;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid) tree[now].l=change(tree[now].l,l,mid,x,k);
	else tree[now].r=change(tree[now].r,mid+1,r,x,k);
	pushup(now);
	return now;
}

五.可持续化线段树

原理实现

P3919主席树模板题

  • 基本功能: 单点修改产生若干的版本的线段树,查询第 t 个版本的线段树的内容

  • 很显然,不能直接对每个版本都去建一棵全新的线段树,因为时间和空间都不允许。仔细思考修改一个节点线段树发生什么变化???

在这里插入图片描述

可以看出,事实上每次修改,只有 l o g n logn logn 个节点改变了值,对于“第二颗线段树”,我们可以去只增加这 l o g n logn logn 个节点,并与上一颗线段树共用其余的所有节点

一秒懂图

在这里插入图片描述
动态开点建树

int build(int now,int l,int r){
	now=++cnt;
	if(l==r){
		tr[now]=a[l];
		return now;
	}
	int mid=(l+r)/2;
	tr[now].l=build(tr[now].l,l,mid);
	tr[now].r=build(tr[now].r,mid+1,r);
}
int main(){
	root[0]=build(1,1,n);
} 

单点修改更新版本:更新从根节点往下走,其一边与上一棵线段树共用,另一边存储新增的 l o g n logn logn 个结点。用 t o p top top 表示有多少个版本,并用 r o o t root root 数组存储每个版本的根节点。

int clone(int now){
	tr[++cnt]=tr[now];
	return cnt;
}
int update(int now,int l,int r,int x,int k){
	now=clone(now);
	if(l==r){
		tr[now].v+=k;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid)tr[now].l=update(tr[now].l,l,mid,x,k);
	else tr[now].r=update(tr[now].r,mid+1,r,x,k);
	return now;
}
int main(){
   	root[++top]=update(root[t],1,n,x,k);
    return 0;
}

查询: 与基本的线段树一致,从想要查的版本的根结点进入即可。
版本复制: r o o t [ + + t o p ] = r o o t [ t ] root[++top]=root[t] root[++top]=root[t]

可持续化模板题代码

#include <bits/stdc++.h>
using namespace std;
const int N=1e7+10;
struct ppp{
	int l,r,v;
}tr[N*4];
int top;//版本数
int cnt=0;//动态开点
int root[N];//每个版本的根 
int a[N];

int clone(int now){
	cnt++;
	tr[cnt]=tr[now];
	return cnt;
}
int build(int now,int l,int r){
	now=++cnt;
	if(l==r){
		tr[now].v=a[l];
		return now;
	}
	int mid=(l+r)/2;
	tr[now].l=build(tr[now].l,l,mid);
	tr[now].r=build(tr[now].r,mid+1,r);
	return now; 
}
int update(int now,int l,int r,int x,int k){
	now=clone(now);
	if(l==r){
		tr[now].v=k;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid)tr[now].l=update(tr[now].l,l,mid,x,k);
	else tr[now].r=update(tr[now].r,mid+1,r,x,k);
	return now;
}
int query(int now,int l,int r,int x){
	if(l==r)return tr[now].v;
	int mid=(l+r)/2;
	if(x<=mid)return query(tr[now].l,l,mid,x);
	else return query(tr[now].r,mid+1,r,x);
}
int main()
{
	int n,q,op,x,k;
	int t;
	cin>>n>>q;
	for(int i=1;i<=n;i++)cin>>a[i];
	root[0]=build(0,1,n);
	for(int i=1;i<=q;i++){
		scanf("%d%d",&t,&op);
		if(op==1){
			scanf("%d%d",&x,&k);
			root[++top]=update(root[t],1,n,x,k);
		}
		else {
			scanf("%d",&x);
			cout<<query(root[t],1,n,x)<<endl; 
			root[++top]=root[t];
		}
	}
    return 0;
}

例题1:查询区间第 k 大(单点修改构建主席树)

例题链接

  • 题目描述: 查询若干次区间第 k 大
  • 问题分析: 查询整个区间的区间第 k 大可以用权值线段树做。但是时间和空间都不允许。发现查询权值线段树的区间第 k 大是满足前缀和相减的,如果我们建立出区间的所有前缀的权值线段树,就可以实现查询区间第 k 大了。而对于前缀 i 和前缀 i+1 ,其实就单点修改了一下,只有 logn 个结点的不同。那么我们就可以 nlogn 的建出所有前缀的权值线段树。然后在线段树上二分即可。
  • 【补充】区间第 k k k 大其实还可以用莫队+平衡树/权值线段树写
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct ppp{
	int l,r,v;
}tr[N*105];
int cnt,root[N],n,q,a[N];
void pushup(int now){
	tr[now].v=tr[tr[now].l].v+tr[tr[now].r].v;
}
int build(int now,int l,int r){
	now=++cnt;
	if(l==r){
		tr[now].v=0;
		return now;
	}
	int mid=(l+r)/2;
	tr[now].l=build(tr[now].l,l,mid);
	tr[now].r=build(tr[now].r,mid+1,r);
	pushup(now);
	return now;
}
int clone(int now){
	tr[++cnt]=tr[now];
	return cnt;
}
int update(int now,int l,int r,int x,int k){
	now=clone(now);
	if(l==r){
		tr[now].v+=k;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid)tr[now].l=update(tr[now].l,l,mid,x,k);
	else tr[now].r=update(tr[now].r,mid+1,r,x,k);
	pushup(now);
	return now;
}
int query(int l,int r,int nowx,int nowy,int k){
	if(l==r)return l;
	int sum=tr[tr[nowy].l].v-tr[tr[nowx].l].v;
	int mid=(l+r)/2;
	if(k<=sum)return query(l,mid,tr[nowx].l,tr[nowy].l,k);
	else return query(mid+1,r,tr[nowx].r,tr[nowy].r,k-sum);
}
void solve(){
	root[0]=build(0,1,n);
	for(int i=1;i<=n;i++)root[i]=update(root[i-1],1,n,a[i],1);
	int l,r,k;
	for(int i=1;i<=q;i++){
		cin>>x>>y>>k;
		cout<<a[query(1,n,root[l-1],root[r],k)]<<endl; 
	}
}
int main(){
    cin>>n>>q;
    for(int i=1;i<=n;i++)cin>>a[i];
	solve();
    return 0;
}

例题2:查询单点第 k 大(区间修改构建主席树)

例题链接

  • 题目描述: n 个人,m 次给予,每次让第 l i l_i li r i r_i ri 中的每个人都的得到一个值 k i k_i ki。q 次查询,查询第 x i x_i xi 个人手中值的前 k 小之和。

例题3:求区间中最少使用多少个元素可以使得和大于等于 h

  • 问题分析: 显然是优先选择大的数。假设不是区间而是整个数组,对整个数组建权值线段树,那么问题就变成了在权值线段树上往大的二分。考虑区间,发现我们需要查询的问题也是支持前缀相减的。主席树又来了。
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+100;
struct T{
	int v,l,r,s;
}tr[N*40];
int n,R=1000,a[N],root[N],cnt;
void build(int &now,int l,int r){
	if(now==0)now=++cnt;
	if(l==r)return;
	int mid=(l+r)/2;
	build(tr[now].l,l,mid);
	build(tr[now].r,mid+1,r); 
}
int clone(int now){
	tr[++cnt]=tr[now];
	return cnt;
}
int update(int now,int l,int r,int x,int k){
	now=clone(now);
	if(l==r){
		tr[now].v+=k;
		tr[now].s+=k*l; 
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid)tr[now].l=update(tr[now].l,l,mid,x,k);
	else tr[now].r=update(tr[now].r,mid+1,r,x,k);
	tr[now].v=tr[tr[now].l].v+tr[tr[now].r].v;
	tr[now].s=tr[tr[now].l].s+tr[tr[now].r].s;
	return now;
}
int query(int nowx,int nowy,int l,int r,int h){
	if(l==r)return h/l+(h%l==0?0:1);
	int mid=(l+r)/2,sum=tr[tr[nowy].r].s-tr[tr[nowx].r].s;
	if(sum>=h)return query(tr[nowx].r,tr[nowy].r,mid+1,r,h);
	else return tr[tr[nowy].r].v-tr[tr[nowx].r].v+query(tr[nowx].l,tr[nowy].l,l,mid,h-sum);
}
int main(){
	int n,q,l,r,h;
	cin>>n>>q;
	for(int i=1;i<=n;i++)cin>>a[i];
	build(root[0],1,R);
	for(int i=1;i<=n;i++)root[i]=update(root[i-1],1,R,a[i],1);
	for(int i=1;i<=q;i++){
		cin>>l>>r>>h;
		if(tr[root[r]].s-tr[root[l-1]].s<h)cout<<-1<<endl;
		else cout<<query(root[l-1],root[r],1,R,h)<<endl;
	}
	return 0;
}

例题4:树上路径第 k 小

例题链接

  • 问题分析: x , y x,y x,y 的路径 ( x , y ) (x,y) (x,y) 可以表示为: ( 1 , x ) + ( 1 , y ) − ( 1 , l c a ) − ( 1 , f a [ l c a ] ) (1,x)+(1,y)-(1,lca)-(1,fa[lca]) (1,x)+(1,y)(1,lca)(1,fa[lca])。可以预处理出树上所有前缀的主席树,然后用前缀求第 k 小。与区间上基本一致。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;

struct E{
	int u,v,next;
}e[N*2];
int vex[N],tot;
struct T {
	int v,l,r;
} tr[N*40];
int cnt,root[N],dep[N],f[N][25],R,a[N],b[N];

void add(int u,int v){
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].next=vex[u];
	vex[u]=tot;
}
int build(int now,int l,int r){
	if(now==0)now=++cnt;
	if(l==r)return now;
	int mid=(l+r)/2;
	tr[now].l=build(tr[now].l,l,mid);
	tr[now].r=build(tr[now].r,mid+1,r);
	return now;
}
int clone(int now){
	tr[++cnt]=tr[now];
	return cnt;
}
int update(int now,int l,int r,int x,int  k){
	now=clone(now);
	if(l==r){
		tr[now].v+=k;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid)tr[now].l=update(tr[now].l,l,mid,x,k);
	else tr[now].r=update(tr[now].r,mid+1,r,x,k);
	tr[now].v=tr[tr[now].l].v+tr[tr[now].r].v;
	return now;
}
int query(int nowflca,int nowlca,int nowx,int nowy,int l,int r,int k){
	if(l==r)return l;
	int mid=(l+r)/2,sum=tr[tr[nowx].l].v+tr[tr[nowy].l].v-tr[tr[nowlca].l].v-tr[tr[nowflca].l].v;
	if(k<=sum)return query(tr[nowflca].l,tr[nowlca].l,tr[nowx].l,tr[nowy].l,l,mid,k);
	else return query(tr[nowflca].r,tr[nowlca].r,tr[nowx].r,tr[nowy].r,mid+1,r,k-sum);
}
void dfs(int u,int fa) {
	root[u]=update(root[fa],1,R,a[u],1);
	dep[u]=dep[fa]+1;
	f[u][0]=fa;
	for(int i=1; i<=20; i++)f[u][i]=f[f[u][i-1]][i-1];
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
	}
}
int LCA(int x,int y) {
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20; i>=0; i--) {
		if(dep[f[x][i]]<dep[y])continue;
		x=f[x][i];
	}
	if(x==y)return x;
	for(int i=20; i>=0; i--) {
		if(f[x][i]==f[y][i])continue;
		x=f[x][i],y=f[y][i];
	}
	return f[x][0];
}
int main() {
	int n,m,u,v,k;
	cin>>n>>m;
	for(int i=1; i<=n; i++)cin>>a[i],b[++R]=a[i];
	sort(b+1,b+R+1);
	R=unique(b+1,b+R+1)-b-1;
	for(int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+R+1,a[i])-b;
	
	for(int i=1; i<n; i++) {
		cin>>u>>v;
		add(u,v);
		add(v,u);
	}
	root[0]=build(root[0],1,R);
	dfs(1,0);
	
	for(int i=1;i<=m;i++){
		cin>>u>>v>>k;
		int lca=LCA(u,v);
		cout<<b[query(root[f[lca][0]],root[lca],root[u],root[v],1,R,k)]<<endl;
	}
}

例题5:树上路径第 k 小,森林连边合并

例题链接

  • 问题分析: 合并部分启发式即可。连边完直接从树较小的地方开始 d f s dfs dfs ,重构主席树即可。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;

struct E {
	int u,v,next;
} e[N*2];
int vex[N],tot;
struct T {
	int v,l,r;
} tr[N*40];
int cnt,root[N],dep[N],f[N][25],R,bel[N],a[N],b[N],size[N];

void add(int u,int v) {
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].next=vex[u];
	vex[u]=tot;
}
int build(int now,int l,int r) {
	if(now==0)now=++cnt;
	if(l==r)return now;
	int mid=(l+r)/2;
	tr[now].l=build(tr[now].l,l,mid);
	tr[now].r=build(tr[now].r,mid+1,r);
	return now;
}
int clone(int now) {
	tr[++cnt]=tr[now];
	return cnt;
}
int update(int now,int l,int r,int x,int  k) {
	now=clone(now);
	if(l==r) {
		tr[now].v+=k;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid)tr[now].l=update(tr[now].l,l,mid,x,k);
	else tr[now].r=update(tr[now].r,mid+1,r,x,k);
	tr[now].v=tr[tr[now].l].v+tr[tr[now].r].v;
	return now;
}
int query(int nowflca,int nowlca,int nowx,int nowy,int l,int r,int k) {
	if(l==r)return l;
	int mid=(l+r)/2,sum=tr[tr[nowx].l].v+tr[tr[nowy].l].v-tr[tr[nowlca].l].v-tr[tr[nowflca].l].v;
	if(k<=sum)return query(tr[nowflca].l,tr[nowlca].l,tr[nowx].l,tr[nowy].l,l,mid,k);
	else return query(tr[nowflca].r,tr[nowlca].r,tr[nowx].r,tr[nowy].r,mid+1,r,k-sum);
}
void pre_dfs(int u,int fa,int belong) {
	root[u]=update(root[fa],1,R,a[u],1);
	dep[u]=dep[fa]+1;
	bel[u]=belong;
	size[u]=1;
	f[u][0]=fa;
	for(int i=1; i<=20; i++)f[u][i]=f[f[u][i-1]][i-1];
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa)continue;
		pre_dfs(v,u,belong);
		size[u]+=size[v];
	}
}
void dfs(int u,int fa,int belong) {
	//cout<<u<<" ";
	root[u]=update(root[fa],1,R,a[u],1);
	dep[u]=dep[fa]+1;
	bel[u]=belong;
	f[u][0]=fa;
	for(int i=1; i<=20; i++)f[u][i]=f[f[u][i-1]][i-1];
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u,belong);
	}
}
int LCA(int x,int y) {
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20; i>=0; i--) {
		if(dep[f[x][i]]<dep[y])continue;
		x=f[x][i];
	}
	if(x==y)return x;
	for(int i=20; i>=0; i--) {
		if(f[x][i]==f[y][i])continue;
		x=f[x][i],y=f[y][i];
	}
	return f[x][0];
}
int main() {
	char op;
	int n,m,u,v,k,q,T;
	cin>>T;
	while(T--) {
		cin>>n>>m>>q;
		tot=cnt=R=0;
		for(int i=1; i<=n; i++)root[i]=bel[i]=vex[i]=0;


		for(int i=1; i<=n; i++)cin>>a[i],b[++R]=a[i];
		sort(b+1,b+R+1);
		R=unique(b+1,b+R+1)-b-1;
		for(int i=1; i<=n; i++)a[i]=lower_bound(b+1,b+R+1,a[i])-b;

		for(int i=1; i<=m; i++) {
			cin>>u>>v;
			add(u,v);
			add(v,u);
		}
		root[0]=build(root[0],1,R);
		for(int i=1; i<=n; i++) {
			if(bel[i]==0)pre_dfs(i,0,i);
		}

		int last=0;
		for(int i=1; i<=q; i++) {
			cin>>op;
			if(op=='Q') {
				cin>>u>>v>>k;
				u=u^last,v=v^last,k=k^last;
				int lca=LCA(u,v);
				last=b[query(root[f[lca][0]],root[lca],root[u],root[v],1,R,k)];
				cout<<last<<'\n';
			} else {
				cin>>u>>v;
				u=u^last,v=v^last;
				int x=bel[u],y=bel[v];
				add(u,v);
				add(v,u);
				if(size[x]>size[y]) {
					dfs(v,u,x);
					size[x]+=size[y];
				}else{
					dfs(u,v,y);
					size[y]+=size[x];
				}

			}
		}
	}

}

六.线段树合并

方法学习

  • 线段树一般是针对权值线段树,即合并两个桶。
  • 假如我们真的去合并两个桶,a[i]+=b[i],时间复杂度是O(n)的。
  • 而我们去合并两个权值线段树,只需要O(logn)即可。

方法如下:

  • 递归两个桶,合并每个子区间,假设递归到的区间在两个桶中的根分别为a,b。
  • 1.如果a或b为空,即其中一个并未分配到编号,也就是说这个子树没有改动过任何的值,那么合并后的子树就用那棵不空的。
  • 2.如果到了叶子,即l==r,将权值加到a上,并返回a来作为合并后的子树即可。
  • 3.如果没到叶子,那么就继续递归合并左右子区间,并将合并后的子树作为a的左右子树,并返回a来作为合并后的子树。

合并模板如下

int merge(int a,int b,int l,int r) {
	if(!a) return b;
	if(!b) return a;
	if(l==r) {
		tr[a].v+=tr[b].v;
		return a;
	}
	int mid=(l+r)/2;
	tr[a].l=merge(tr[a].l,tr[b].l,l,mid);
	tr[a].r=merge(tr[a].r,tr[b].r,mid+1,r);
	pushup(a);
	return a;
}

【注意】使用该方法进行线段树合并之后,被合并的线段树 b 的结构已经毁了。

被合并的线段树b还在的线段树合并方法

int merge(int a,int b,int l,int r) {
	if(!a) return b;
	if(!b) return a;
	if(l==r) {
		int now=++cnt;
		tr[now].v=tr[a].v+tr[b].v;
		return now;
	}
	int mid=(l+r)/2,now=++cnt;
	tr[now].l=merge(tr[a].l,tr[b].l,l,mid);
	tr[now].r=merge(tr[a].r,tr[b].r,mid+1,r);
	pushup(now);
	return now;
}

例题分析

树上线段树合并

大致的模板如下:

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10;
const int M=7e5+10;
const int MAXN=1e7;
struct E {
	long long u,v,next;
} e[M];
struct T {
	long long l,r,v;
} tree[MAXN];

long long vex[N],root[N];
long long n,cnt,tot;

void add(int u,int v) {
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].next=vex[u];
	vex[u]=tot;
}
void pushup(int now) {
tree[now].v=tree[tree[now].l].v+tree[tree[now].r].v;
}
int change(int now,int l,int r,int x,int k) {
	if(now==0) now=++cnt;
	if(l==r) {
		tree[now].v+=k;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid) tree[now].l=change(tree[now].l,l,mid,x,k);
	else tree[now].r=change(tree[now].r,mid+1,r,x,k);
	pushup(now);
	return now;
}
int merge(int a,int b,int l,int r) {
	if(!a) return b;
	if(!b) return a;
	if(l==r) {
		tree[a].v+=tree[b].v;
		return a;
	}
	int mid=(l+r)/2;
	tree[a].l=merge(tree[a].l,tree[b].l,l,mid);
	tree[a].r=merge(tree[a].r,tree[b].r,mid+1,r);
	pushup(a);
	return a;
}
void dfs(int u,int fa) {
	for(int i=vex[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
		root[u]=merge(root[u],root[v],1,R);
	}
	//计算ans
}

例题1

  • 题目描述:n个点的树,m次操作,每次操作选定x,y,并让xy上的路径的每个点都得到z类型的一个粮食。最后输出每个点粮食最多的类型编号。
  • 问题分析: 假如只有一个类型的救济粮,我们怎么做?根据树上差分的思想,假设x,y路径上都要+1,我们会让差分数组:d[x]++,d[y]++,d[lca]–,d[fa[lca]]–,然后递归前缀和。那么有多种类型的救济粮呢?让每个结点都生成一个权值线段树,来表示每个类型有多少个,同样根据差分思想修改。而前缀和部分我们需要合并两个桶,!!!不就是合并两个线段树吗???
const int N=1e5+10;
const int M=3e5+10;
const int MAXN=5e6;
struct E {
	int u,v,next;
} e[M];
struct T{
	int l,r,v,id;
}tree[MAXN];

int vex[N],f[N][25],dep[N],x[N],y[N],z[N],ans[N],root[N];
int n,m,cnt,R,tot;

void add(int u,int v){
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].next=vex[u];
	vex[u]=tot;
}
void dfs(int u,int fa) {
	f[u][0]=fa;
	dep[u]=dep[fa]+1;
	for(int i=1; i<=20; i++)f[u][i]=f[f[u][i-1]][i-1];
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
	}
}
int LCA(int x,int y) {
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20; i>=0; i--) {
		if(dep[f[x][i]]<dep[y])continue;;
		x=f[x][i];
	}
	if(x==y)return x;
	for(int i=20; i>=0; i--) {
		if(f[x][i]==f[y][i])continue;
		x=f[x][i];
		y=f[y][i];
	}
	return f[x][0];
}
void pushup(int now){
	if(tree[tree[now].l].v>=tree[tree[now].r].v){
		tree[now].v=tree[tree[now].l].v;
		tree[now].id=tree[tree[now].l].id;
	}
	else{
		tree[now].v=tree[tree[now].r].v;
		tree[now].id=tree[tree[now].r].id;
	}
}
int change(int now,int l,int r,int x,int k) {
	if(now==0) now=++cnt;
	if(l==r) {
		tree[now].v+=k;
		tree[now].id=l;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid) tree[now].l=change(tree[now].l,l,mid,x,k);
	else tree[now].r=change(tree[now].r,mid+1,r,x,k);
	pushup(now);
	return now;
}
int merge(int a,int b,int l,int r) {
	if(!a) return b;
	if(!b) return a;
	if(l==r) {
		tree[a].v+=tree[b].v;
		return a;
	}
	int mid=(l+r)/2;
	tree[a].l=merge(tree[a].l,tree[b].l,l,mid);
	tree[a].r=merge(tree[a].r,tree[b].r,mid+1,r);
	pushup(a);
	return a;
}
void Count(int u,int fa) {
	for(int i=vex[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa)continue;
		Count(v,u);
		root[u]=merge(root[u],root[v],1,R);
	}
	if(tree[root[u]].v)ans[u]=tree[root[u]].id;
}
int main() {
	int u,v,w;
	cin>>n>>m;
	for(int i=1; i<n; i++) {
		cin>>u>>v;
		add(u,v);
		add(v,u);
	}
	dfs(1,0);
	for(int i=1; i<=m; i++) {
		cin>>x[i]>>y[i]>>z[i];
		R=max(R,z[i]);
	}
	for(int i=1; i<=m; i++) {
		int lca=LCA(x[i],y[i]);
		root[x[i]]=change(root[x[i]],1,R,z[i],1);
		root[y[i]]=change(root[y[i]],1,R,z[i],1);
		root[lca]=change(root[lca],1,R,z[i],-1);
		if(f[lca][0]!=0)root[f[lca][0]]=change(root[f[lca][0]],1,R,z[i],-1);
	}
	Count(1,0);
	for(int i=1; i<=n; i++)cout<<ans[i]<<endl;
	return 0;
}

例题2

  • 题目描述: n个点的树,每个点的权值为a[i],对于每个点,求其子树的所有点中权值大于当前点的数量。
  • 问题分析: 可以用权值线段树来记录子树中哪些值,并计相应的桶++,然后查询比 a[u] 大的点有多少个即可。求权值线段树的部分:递归合并儿子的权值线段树作为自己的权值线段树即可。注意值较大,还需离散化一下。
const int N=1e5+10;
const int M=3e5+10;
const int MAXN=5e6;
struct E {
	int u,v,next;
} e[M];
struct T{
	int l,r,v;
}tree[MAXN];

int vex[N],f[N][25],root[N],a[N],b[N],ans[N];
int n,cnt,tot;

void add(int u,int v){
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].next=vex[u];
	vex[u]=tot;
}
void pushup(int now){
	tree[now].v=tree[tree[now].l].v+tree[tree[now].r].v;
}
int change(int now,int l,int r,int x,int k) {
	if(now==0) now=++cnt;
	if(l==r) {
		tree[now].v+=k;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid) tree[now].l=change(tree[now].l,l,mid,x,k);
	else tree[now].r=change(tree[now].r,mid+1,r,x,k);
	pushup(now);
	return now;
}
int query(int now,int l,int r,int ql,int qr) {
	if(now==0)return 0;
	if(ql<=l&&r<=qr)return tree[now].v;;
	int mid=(l+r)/2,ans=0;
	if(ql<=mid)ans+=query(tree[now].l,l,mid,ql,qr);
	if(qr>mid)ans+=query(tree[now].r,mid+1,r,ql,qr);
	return ans;
}
int merge(int a,int b,int l,int r) {
	if(!a) return b;
	if(!b) return a;
	if(l==r) {
		tree[a].v+=tree[b].v;
		return a;
	}
	int mid=(l+r)/2;
	tree[a].l=merge(tree[a].l,tree[b].l,l,mid);
	tree[a].r=merge(tree[a].r,tree[b].r,mid+1,r);
	pushup(a);
	return a;
}
void dfs(int u,int fa) {
	for(int i=vex[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
		root[u]=merge(root[u],root[v],1,n);
	}
	root[u]=change(root[u],1,n,a[u],1);
	ans[u]=query(root[u],1,n,a[u]+1,n);
}
int main() {
	cin>>n;
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		b[i]=a[i];
	}
	sort(b+1,b+n+1);
	int t=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+n+1,a[i])-b;
	
	int u;
	for(int i=2;i<=n;i++){
		cin>>u;
		add(i,u);
		add(u,i);
	} 
	dfs(1,0);
	for(int i=1;i<=n;i++)cout<<ans[i]<<endl;
	return 0;
}

题目3

  • 题目描述: n个点的一棵树,每个点都有一个颜色c[i],对于每个点,求该点为根的子树中,主导颜色的和。其中:主导颜色为颜色数量最多的颜色,有多个颜色数量最多的颜色时,均为主导颜色。
  • 问题分析: 递归维护权值线段树即可,那么查询问题就变为了:求区间最大值之和。

主代码如下:

void pushup(int now){
	if(tree[tree[now].l].maxnum>tree[tree[now].r].maxnum){
		tree[now].maxnum=tree[tree[now].l].maxnum;
		tree[now].summaxv=tree[tree[now].l].summaxv;
	}
	else if(tree[tree[now].l].maxnum<tree[tree[now].r].maxnum){
		tree[now].maxnum=tree[tree[now].r].maxnum;
		tree[now].summaxv=tree[tree[now].r].summaxv;
	}
	else {
		tree[now].maxnum=tree[tree[now].l].maxnum;
		tree[now].summaxv=tree[tree[now].l].summaxv+tree[tree[now].r].summaxv;
	}
}
int change(int now,int l,int r,int x,int k) {
	if(now==0) now=++cnt;
	if(l==r) {
		tree[now].maxnum+=k;
		tree[now].summaxv=l;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid) tree[now].l=change(tree[now].l,l,mid,x,k);
	else tree[now].r=change(tree[now].r,mid+1,r,x,k);
	pushup(now);
	return now;
}
int merge(int a,int b,int l,int r) {
	if(!a) return b;
	if(!b) return a;
	if(l==r) {
		tree[a].maxnum+=tree[b].maxnum;
		tree[a].summaxv=l;
		return a;
	}
	int mid=(l+r)/2;
	tree[a].l=merge(tree[a].l,tree[b].l,l,mid);
	tree[a].r=merge(tree[a].r,tree[b].r,mid+1,r);
	pushup(a);
	return a;
}
void dfs(int u,int fa) {
	for(int i=vex[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
		root[u]=merge(root[u],root[v],1,R);
	}
	root[u]=change(root[u],1,R,a[u],1);
	ans[u]=tree[root[u]].summaxv;
}

例题4

  • 题目描述: n个点的一个二叉树,每个点都有一个权值a[i],权值构成一个1-n的排列。你可以任意交换任意一个点的左右子树,求先序遍历二叉树下的最小逆序对数量。
  • 问题分析: 对于第 i 个点而言,交换这两棵子树产生的逆序对只与两棵子树之间的逆序对有关,与子树内的逆序对数无关。因此我们只需要贪心的选择交不交换两个子树即可。假设子树分别为a,b,其各有一个权值线段树,如何统计交换还是不交换下的逆序对数量。
  • 继续分析: 根据值域考虑,递归merge两个权值线段树a,b的时候,假设到了某区间,我们先不考虑这个左右区间值域内的,只考虑跨左右区间值域的,如果a的左右区间上有x0,x1个元素,b的左右区间上有y0,y1个元素,显然不交换会产生x1y0个逆序对,交换会产生x0y1个逆序对。我们在递归时累加即可求出总的交换与不交换产生的逆序对。
void pushup(int now){
	tree[now].v=tree[tree[now].l].v+tree[tree[now].r].v;
}
int change(int now,int l,int r,int x,int k) {
	if(now==0) now=++cnt;
	if(l==r) {
		tree[now].v+=k
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid) tree[now].l=change(tree[now].l,l,mid,x,k);
	else tree[now].r=change(tree[now].r,mid+1,r,x,k);
	pushup(now);
	return now;
}
int merge(int a,int b,int l,int r,int &yes,int &no) {
	if(!a) return b;
	if(!b) return a;
	if(l==r) {
		tree[a].v+=tree[b].v;
		return a;
	}
	yes+=tree[tree[a].l].v*tree[tree[b].r].v;
	no+=tree[tree[a].r].v*tree[tree[b].l].v;
	
	int mid=(l+r)/2;
	tree[a].l=merge(tree[a].l,tree[b].l,l,mid,yes);
	tree[a].r=merge(tree[a].r,tree[b].r,mid+1,r,no);
	pushup(a);
	return a;
}
void dfs(int u) {
	dfs(a[u].l);
	dfs(a[u].r);
	int yes=0,no=0;
	root[u]=merge(root[a[u].l],root[a[u].r],1,n,yes,no);
	ans+=min(yes,no);
	root[u]=change(root[u],1,n,a[u],1); 
}

例题5

  • 题目描述: n个点的一棵树,q个询问,每次询问查询结点u的k祖祖先的k级儿子数量(不包括自己)。
  • 问题分析: u的k级祖先可以用倍增求到,假设为u,那么就变成了求u的k级儿子,我们将u结点的询问放入对应u的vector中,然后递归到的时候处理。首先我们预处理出所有结点的dep,那么查询 u 的 k 级祖先就相当于查询 u 的权值线段树中 dep[u]+k 的权值。
void pushup(int now) {
	tree[now].v=tree[tree[now].l].v+tree[tree[now].r].v;
}
int change(int now,int l,int r,int x,int k) {
	if(now==0) now=++cnt;
	if(l==r) {
		tree[now].v+=k;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid) tree[now].l=change(tree[now].l,l,mid,x,k);
	else tree[now].r=change(tree[now].r,mid+1,r,x,k);
	pushup(now);
	return now;
}
int query(int now,int l,int r,int x) {
	if(now==0) now=++cnt;
	if(l==r) {
		return tree[now].v;
	}
	int mid=(l+r)/2;
	if(x<=mid)return query(tree[now].l,l,mid,x);
	else return query(tree[now].r,mid+1,r,x);
}
int merge(int a,int b,int l,int r) {
	if(!a) return b;
	if(!b) return a;
	if(l==r) {
		tree[a].v+=tree[b].v;
		return a;
	}
	int mid=(l+r)/2;
	tree[a].l=merge(tree[a].l,tree[b].l,l,mid);
	tree[a].r=merge(tree[a].r,tree[b].r,mid+1,r);
	pushup(a);
	return a;
}
void solve(int u,int fa) {
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa)continue;
		solve(v,u);
		root[u]=merge(root[u],root[v],1,2*n);
	}
	for(int i=0;i<q[u].size();i++){
		ans[id[u][i]]=query(root[u],1,2*n,dep[u]+q[u][i])-1;
	} 
	root[u]=change(root[u],1,2*n,dep[u],1);
}
void dfs(int u,int fa) {
	f[u][0]=fa;
	dep[u]=dep[fa]+1;
	for(int i=1; i<=20; i++)f[u][i]=f[f[u][i-1]][i-1];
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
	}
}
int lca(int x,int t) {
	for(int i=21; i>=1; i--) {
		if((1<<(i-1))>t)continue;
		x=f[x][i-1];
		t-=(1<<(i-1));
	}
	return x;
}
int main() {
	int m,u,k;
	cin>>n>>m;
	for(int i=2; i<=n; i++) {
		cin>>u;
		add(i,u);
		add(u,i);
	}
	dfs(1,0);
	for(int i=1; i<=m; i++) {
		cin>>u>>k;
		q[lca(u,k)].push_back(k);
		id[lca(u,k)].push_back(i);
	}
	for(int i=1;i<=n;i++)root[i]=++cnt;
	solve(1,0); 
	for(int i=1; i<=m; i++)cout<<ans[i]<<" ";
	return 0;
}

例题5

  • 题目描述: n个点构成一棵树,m个询问,每次询问给定一个a,k,求这样一个有序对的个数(a与b在树上的距离不超过k,a和b都是c的祖先)。
  • 问题分析: 可以看出,a,b,c一定在树的从上到下的一条链上。1: 如果 a 是 b 的祖先那么 b 只能在 [dep[a]+1,dep[a]+k] 中,而对于每个 b 所在的位置,c 可以在 b 的子树中(除b本身),即 c 有size[b]-1种选择。我们可以用权值线段树维护:在 dep[u] 位置加上 size[u]-1 的权值。2: 如果 b 是 a 的祖先,即 b 在 a 的正上方,有 min(k,dep[a]-1)种选择,而 c 可以在 a 的子树中(除a本身),即 c 有size[a]-1种选择。同样可以用权值线段树维护。最后递归合并子树的权值线段树即可。
int merge(int a,int b,int l,int r) {
	if(!a) return b;
	if(!b) return a;
	if(l==r) {
		int now=++cnt;
		tree[now].v=tree[a].v+tree[b].v;
		return now;
	}
	int mid=(l+r)/2,now=++cnt;
	tree[now].l=merge(tree[a].l,tree[b].l,l,mid);
	tree[now].r=merge(tree[a].r,tree[b].r,mid+1,r);
	pushup(now);
	return now;
}
void dfs(int u,int fa) {
	dep[u]=dep[fa]+1;
	size[u]=1;
	for(int i=vex[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
		size[u]+=size[v];
		root[u]=merge(root[u],root[v],1,n);
	}
	root[u]=change(root[u],1,n,dep[u],size[u]-1);
}
int main(){
	long long q,u,v,k,a;
	cin>>n>>q;
	for(int i=1;i<n;i++){
		scanf("%lld%lld",&u,&v);
		add(u,v);
		add(v,u);
	}
	dfs(1,0);
	for(int i=1;i<=q;i++){
		scanf("%lld%lld",&a,&k);
		printf("%lld\n",1ll*query(root[a],1,n,dep[a]+1,dep[a]+k)+1ll*min(k,dep[a]-1)*(size[a]-1));
	}
	return 0;
}

题目6

  • 题目描述: n个点的一棵树,对于每个点 i ,求一个最小k,使得 i 的子树中与点 i 距离等于 k 的点最多。
  • 问题分析: 对于每个点 u,都用权值线段树维护它的子树中的点的每个dep的权值,那么答案就是:求区间最大值的下标-dep[u]。递归时进行子树的线段树合并即可。
void pushup(int now) {
	if(tree[tree[now].l].v>=tree[tree[now].r].v) {
		tree[now].id=tree[tree[now].l].id;
		tree[now].v=tree[tree[now].l].v;
	} else {
		tree[now].id=tree[tree[now].r].id;
		tree[now].v=tree[tree[now].r].v;
	}
}
int change(int now,int l,int r,int x,int k) {
	if(now==0) now=++cnt;
	if(l==r) {
		tree[now].v+=k;
		tree[now].id=l;
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid) tree[now].l=change(tree[now].l,l,mid,x,k);
	else tree[now].r=change(tree[now].r,mid+1,r,x,k);
	pushup(now);
	return now;
}
int merge(int a,int b,int l,int r) {
	if(!a) return b;
	if(!b) return a;
	if(l==r) {
		tree[a].v+=tree[b].v;
		return a;
	}
	int mid=(l+r)/2;
	tree[a].l=merge(tree[a].l,tree[b].l,l,mid);
	tree[a].r=merge(tree[a].r,tree[b].r,mid+1,r);
	pushup(a);
	return a;
}
void dfs(int u,int fa) {
	dep[u]=dep[fa]+1;
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
		root[u]=merge(root[u],root[v],1,n);
	}
	root[u]=change(root[u],1,n,dep[u],1);
	ans[u]=tree[root[u]].id-dep[u];
}

题目7

  • 题目描述: n个点构成一棵树,每个点上有一个字母(a-z),每次询问 a, b查询以 a 为根的子树内深度为 b 的结点上的字母重新排列之后是否能构成回文串。
  • 问题分析: 如何判断是否能构成回文串?个数为奇数的字母<=1即可。权值线段树维护dep,用权值来表示深度为dep下的所有字母是否能构成回文串。权值只是数字,如何来描述字母状态呢?使用二进制,用二进制的第 i 位表示第 i 个字母数量的奇偶性,那么每多个字母 i ,v^(1<<i),合并的时候异或即可。最后数1的个数就ok。
int change(int now,int l,int r,int x,char k) {
	if(now==0) now=++cnt;
	if(l==r) {
		int t=k-'a';
		tree[now].v=tree[now].v^(1<<t);
		return now;
	}
	int mid=(l+r)/2;
	if(x<=mid) tree[now].l=change(tree[now].l,l,mid,x,k);
	else tree[now].r=change(tree[now].r,mid+1,r,x,k);
	pushup(now);
	return now;
}
int query(int now,int l,int r,int x) {
	if(l==r) {
		int num=0;
		for(int i=0;i<26;i++){
			if((tree[now].v&(1<<i))!=0)num++;
		}
		if(num<=1)return 1;
		else return 0;
	}
	int mid=(l+r)/2;
	if(x<=mid)return query(tree[now].l,l,mid,x);
	else return query(tree[now].r,mid+1,r,x);
}
int merge(int a,int b,int l,int r) {
	if(!a) return b;
	if(!b) return a;
	if(l==r) {
		tree[a].v^=tree[b].v;
		return a;
	}
	int mid=(l+r)/2;
	tree[a].l=merge(tree[a].l,tree[b].l,l,mid);
	tree[a].r=merge(tree[a].r,tree[b].r,mid+1,r);
	pushup(a);
	return a;
}
void dfs(int u,int fa) {
	dep[u]=dep[fa]+1;
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
		root[u]=merge(root[u],root[v],1,n);
	}
	root[u]=change(root[u],1,n,dep[u],s[u]);
	
	for(int i=0;i<q[u].size();i++){
		ans[id[u][i]]=query(root[u],1,n,q[u][i]);
	}
}
int main() {
	int m,u,a,b;
	cin>>n>>m;
	for(int i=2; i<=n; i++){
		scanf("%d",&u);
		add(u,i);
		add(i,u);
	}
	cin>>s+1;
	for(int i=1;i<=m;i++){
		scanf("%d%d",&a,&b); 
		q[a].push_back(b);
		id[a].push_back(i);
	}
	dfs(1,0);
	for(int i=1;i<=m;i++){
		if(ans[i]==1)printf("Yes\n");
		else printf("No\n");
	}
}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值