1. 区间合并
线段树上每一个节点即一个区间,每个区间维护一些信息,区间合并即小区间信息合并得到大区间信息,一个问题能不能使用线段树或者线段树上节点的这样的维护信息能不能解决问题,需要看区间能否合并,在代码中的具体体现就是常写的push_up函数。先给出几个例题引出区间合并,这里的例题以简单的形式叙述。
(1)hdu 1540
问题简述: 一个长度为n的数组,每个数组元素为0或者1,有m次修改,修改有两种类型:将数组元素^1(即0变为1,1变为0);查询与第x个元素相连的连续1的个数(如果第x个元素为0,连续1个个数为0)。
answer:这里直接给出解决方案,使用线段树,线段树的每个区间维护pre与suf,pre的含义(前缀)是该区间从左开始的最长连续1,suf(后缀)的含义是该区间从右开始的最长连1。 这里需要保证大区间的信息能由小区间的信息得来,具体见代码。
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+50;
int pre[N<<2],suf[N<<2];
void build(int k,int l,int r){
pre[k]=suf[k]=r-l+1;
if(r==l)return ;
int mid=l+r>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
}
void push_up(int k,int l,int r){
int ls=k<<1,rs=k<<1|1,mid=l+r>>1;
pre[k]=pre[ls];
if(pre[ls]==mid-l+1)pre[k]+=pre[rs];
suf[k]=suf[rs];
if(suf[rs]==r-mid)suf[k]+=suf[ls];
}
void update(int x,int c,int k,int l,int r){
if(r==l){
pre[k]=suf[k]=c;
return ;
}
int mid=l+r>>1;
if(x<=mid)update(x,c,k<<1,l,mid);
else update(x,c,k<<1|1,mid+1,r);
push_up(k,l,r);
}
int query(int x,int k,int l,int r){
if(r==l)return 0;
if(l+pre[k]-1>=x)return pre[k];
if(r-suf[k]+1<=x)return suf[k];
int mid=l+r>>1,ls=k<<1,rs=k<<1|1;
if(mid-suf[ls]+1<=x&&mid+pre[rs]>=x)return suf[ls]+pre[rs];
if(x<=mid)return query(x,ls,l,mid);
return query(x,rs,mid+1,r);
}
int que[N],idx;
int main(){
int n,m;
while(cin>>n>>m){
build(1,1,n);
idx=-1;
while(m--){
char op;
int x;
cin>>op;
if(op=='D'){
cin>>x;
update(x,0,1,1,n);
que[++idx]=x;
}else if(op=='R'){
update(que[idx--],1,1,1,n);
}else{
cin>>x;
cout<<query(x,1,1,n)<<endl;
}
}
}
}
这里关键点在于两个函数:
①push_up;
②query:查询的思路就是,当在第k个区间时,看第x个数是否在第k个区间的pre中、suf中,以及左儿子的suf+右儿子的pre中,如果在就return,如果不在,就根据x去左儿子上或者右儿子上递归查询。
(2)poj 3667
问题简述:一个数组长度为n,值为0或1,m次操作,操作有两种类型:①区间修改,将[l,r]变为c(c=0或1);②查询:查询最左边的d个连续1的起始位置。
answer:维护信息在(1)的基础上增加了一个ma,表示整个区间的最长连续1的长度,用于判断一个区间是否有d个连续1,避免做无效操作。具体见代码:
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+50;
int pre[N<<2],suf[N<<2],ma[N<<2],tag[N<<2];
void build(int k,int l,int r){
pre[k]=suf[k]=ma[k]=r-l+1;
tag[k]=-1;
if(r==l)return ;
int mid=l+r>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
}
void push_up(int k,int l,int r){
int ls=k<<1,rs=k<<1|1,mid=l+r>>1;
pre[k]=pre[ls];
if(pre[ls]==mid-l+1)pre[k]+=pre[rs];
suf[k]=suf[rs];
if(suf[rs]==r-mid)suf[k]+=suf[ls];
ma[k]=max(max(ma[ls],ma[rs]),suf[ls]+pre[rs]);
}
void push_down(int k,int l,int r){
if(tag[k]>=0){
int ls=k<<1,rs=k<<1|1,mid=l+r>>1;
pre[ls]=suf[ls]=ma[ls]=tag[k]*(mid-l+1);
pre[rs]=suf[rs]=ma[rs]=tag[k]*(r-mid);
tag[ls]=tag[rs]=tag[k];
tag[k]=-1;
}
}
void update(int a,int b,int c,int k,int l,int r){
if(b<l||a>r)return ;
if(a<=l&&b>=r){
pre[k]=suf[k]=ma[k]=c*(r-l+1);
tag[k]=c;
return ;
}
push_down(k,l,r);
int mid=l+r>>1;
update(a,b,c,k<<1,l,mid);
update(a,b,c,k<<1|1,mid+1,r);
push_up(k,l,r);
}
int query(int d,int k,int l,int r){
if(ma[k]<d)return 0;//用于判断整个区间是否有大于等于d的连续1
if(pre[k]>=d)return l;
int ls=k<<1,rs=k<<1|1,mid=l+r>>1;
push_down(k,l,r);
if(ma[ls]>=d)return query(d,ls,l,mid);
if(suf[ls]+pre[rs]>=d)return mid-suf[ls]+1;
return query(d,rs,mid+1,r);
}
int main(){
int n,m;
cin>>n>>m;
build(1,1,n);
while(m--){
int op,x,d;
cin>>op;
if(op==1){
cin>>d;
int res=query(d,1,1,n);
cout<<res<<endl;
if(d)update(res,res+d-1,0,1,1,n);
}else{
cin>>x>>d;
update(x,x+d-1,1,1,1,n);
}
}
}
其中最关键在于查询:查询的思路是从根节点出发,先判断根节点是否有大于等于d的连续1,如果没有,则return,如果有,就判断根节点的pre是否>=d,如果是则找到最左侧;否则此时最左侧可能出现在左儿子,所以看ma[ls]>=d?,如果大于等于,则说明答案在左儿子上,递归到左儿子; 否则再看是否出现在suf[ls]+pre[rs]的位置;否则就必出现在右儿子上。
(3)hdu 3397
问题简述:一个长度为n的数组,元素值为0或1,m次操作,操作有两种:①区间修改:[l,r]改为0或1;②区间查询:查询[l,r]内最长连续1的个数。
answer:这里就主要讲一下查询,首先还是维护pre、suf、ma,在(1)(2)中,说过push_up是将两个小区间合并为大区间(或者说由两个小区间的信息来更新大区间),无论是单点修改或者是区间修改,最终都会向上合并更新大区间的信息。
我在这里想提一个非常重要的理解:线段树的每一个节点都是一个区间,都表示一个范围,因为线段树节点固定,所以每一个区间的范围也固定,更新区间[a,b]时,实际上是主要更新了线段树上k个小区间,这些小区间刚好没有重叠的组成了区间[a,b],然后push_up合并(更新)得到他们上面的区间。查询也是如果,当查询区间[a,b]时,实际上是查询了一些小区间,这些小区间的恰好组成了区间[a,b],而你还需要做的就是将这些小区间合并,在最简单的求区间总和的线段树中,你的代码可能会这么写:
int query(int a,int b,int k,int l,int r){
if(b<l||a>r)return 0;
if(a<=l&&b>=r)return sum[k];
push_down(k,l,r);
int mid=l+r>>1;
return query(a,b,k<<1,l,mid)+query(a,b,k<<1|1,mid+1,r);
}
其中的 query(a,b,k<<1,l,mid)+query(a,b,k<<1|1,mid+1,r)其实就是在区间合并了,将左区间的信息与右区间的信息合并。
所以回到hdu 3397,在查询[a,b]的最长连续1时,你会得到一些小区间的信息,pre、suf、ma。而只有两个小区间的ma是不能得出大区间的ma的,每个小区间你都需要pre、suf、ma(如同push_up中那样,需要左右儿子的pre、suf、ma,才能得出大区间的ma)。所以你在query时,当if(a<=l&&b>=r)满足时,不是返回ma[k],而是pre[k]、suf[k]、ma[k]。也不能像查询sum时那样return query(a,b,k<<1,l,mid)+query(a,b,k<<1|1,mid+1,r),你需要像push_up中那样做,才能有左右区间的信息得出当前区间k的信息。当然这时你可以应该需要一个有pre、suf、ma的结构体,以方便返回。
持续更新中~~~~~~