一.线段树的实质及基本操作
-
线段树实际上是一棵完全二叉树,将需要维护的数组放在叶子节点上,将子节点的值相关值维护在根节点之中,访问时,只需要查询到能将答案覆盖的最上面的根即可
-
在静态存储而非动态开点的情况下,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=a∑i=lrx+(r−l+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)=a2∑i=lrx2+2ab∑i=lrx+(r−l+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=lr∑j=i+1rvivj=∑i=lr∑j=i+1r(axi+b)(axj+b)=a2∑i=lr∑j=i+1rxixj+ab(r−l)∑i=lrxi+b2(r−l+1)×(r−l)/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+(i−lj+1)2=∑i=lrvi+(i−xj)2=∑i=lrvi+∑i=lri2−2xj∑i=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+n−1]−prei−1 。可以通过区间修改来得到任意一个点开始的前缀和。
例题9.2:线段树维护环的前缀和
- 题目描述: 一个环 n 个数,从任意点 i 开始求 p r e − ( j − i + 1 ) × g o a l pre-(j-i+1)\times goal pre−(j−i+1)×goal,快去维护其信息。
- 问题分析: 从第 i 个点转移至第 i+1 点开始的前缀和, p r e pre pre 中除第 i 点以外,其他都减去 a i − g o a l a_i-goal ai−goal,第 i 个点加上除第 i 个点以外的所有 a i − g o a l a_i-goal ai−goal。
例题10:区间覆盖
- 题目描述: 给定 n 个线段 [ l i , r i ] [l_i,r_i] [li,ri] ,值为 v i v_i vi,按顺序依次使用,覆盖 1 − n 1-n 1−n 。规则如下:当一个线段 [ 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] k−sum[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;
}
五.可持续化线段树
原理实现
-
基本功能: 单点修改产生若干的版本的线段树,查询第 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
}
- 题目描述: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;
}
- 题目描述: 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;
}
- 题目描述: 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;
}
- 题目描述: 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);
}
- 题目描述: 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;
}
- 题目描述: 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;
}
- 题目描述: 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];
}
- 题目描述: 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");
}
}