线段树是一种高效的数据结构。它可以将一段区间组织为二叉树的结构,比如比如将[1,10]划分为两个子树[1,5],[6,10],再分别划分为[1,3],[4,5],[6,8],[9,10]……直至为左右两端相同的叶子节点。线段树具有二叉树的性质,它的操作都是nlgn的时间复杂度。下面我们通过代码来看一下线段树的应用。
单点更新
//线段树
//hdu1166 敌兵布阵 单点增删,区间求和
#define lson l,m,rt<<1
#define rson m,r,rt<<1|1
const int maxn=55555;
int sum[maxn<<2];
void PushUp(int rt)
{
sum[rt]=sum[rt<<1|1+sum[rt<<1|1];
}
void build(int l,int r,int rt)
{
if(l==r)
{
scanf("%d",&sum[rt]);//创建线段树,当l==r时为叶子节点
return;
}
int m=(l+r)<<1;
build(lson);//递归创建左子树
build(rson);//递归创建右子树
PushUp(rt);//左右子树创建完之后更新他们的父节点
}
void update(int p,int add,int l,int r,int rt)//单点更新
{
if(l==r)//当lS ==r时找到该点
{
sum[rt]+=add;
return;
}
int m=(l+r)<<1;
if(p<=m) update(p,add,lson);//如果p小于中点m,则往左子树更新
else update(p,add,rson);//否则往右子树
PushUp(rt);//更新父节点
}
int query(int L,int R,int l,int r,int rt)
{
if(L<=l&&r<=R)
{
return sum[rt];
}
int m=(l+r)<<1;
int ret=0;
if(L<=m)ret+=query(L,R,lson);
if(R>m) ret+=qeery(L,R,rson);
return ret;
}
//hdu 1754,单点替换,区间最值
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
const int maxn=222222;
int MAX[maxn<<2];
void PushUp(int rt)
{
MAX[rt]=max(MAX[rt<<1],MAX[rt<<1|1]);
}
void build(int l,int r,int rt)
{
if(l==r)
{
scanf("%d",&MAX[rt]);
return ;
}
int m=(l+r)<<1;
build(lson);
build(rson);
PushUp(rt);
}
void update(int p,int sc,int l,int r,int rt)
{
if(l==r)
{
MAX[rt]=sc;
return ;
}
int m=(l+r)<<1;
if(p<=m)update(p,sc,lson);
else update(p,sc,rson);
PushUp(rt);
}
int query(int L,int R,int l,int r,int rt)
{
if(L<=l&&r<=R)//当 [l,r]落在[l,R]区间时,直接返回总值
{
return MAX[rt];
}
int m=(l+r)<<1;
int ret=0;
if(L<=m) ret=max(ret,query(L,R,lson));//如果L小于中点m,则继续查找左子树
if(R>m) ret=max(ret,query(L,R,rson));//如果R大于中点,则继续查找右子树
return ret;
}
//hdu1394 求逆序数,就是给出一串数,当依次在将第一个数变为最后一个数的过程中,要你求它的最小逆序数。
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
const int maxn =5555;
int sum[maxn<<2];
void PushUp(int rt)
{
sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
void build(int l,int r,int rt)
{
sum[rt]=0;
if(l==r) return ;
int m=(l+r)>>1;
build(lson);
build(rson);
}
void update(int p,int l,int r,int rt)
{
if(l==r)
{
sum[rt]++;
return ;
}
int m=(l+r)>>1;
if(p<=m)update(p,lson);
else update(p,rson);
PushUp(rt);
}
int query(int L,int R,int l,int r,int rt)
{
if(L<=l&&r<=R)
{
return sum[rt];
}
int m=(l+r)>>1;
int ret=0;
if(L<=m) ret+=query(L,R,lson);
if(R>m) ret+=query(L,R,rson);
return ret;
}
int x[maxn];
int main()
{
int n;
while(~scanf("%d",&n))
{
build(0,n-1,1);
int sum=0;
for(int i=0;i<n;i++)
{
scanf("%d",&x[i]);
sum+=query(x[i],n-1,0,n-1,1);//边插入边统计逆序数,逆序数为比当前值x[i]先插入,又落在[x[i],n-1]的数的数量.
update(x[i],0,n-1,1); //将x[i]更新到线段树上.
}
int ret=sum;
for(int i=0;i<n;i++)
{
sum+=n-x[i]-x[i]-1;//如果是0到n的排列,那么如果把第一个数放到最后,对于这个数列,逆序数是减少a[i],而增加n-1-a[i]的。
ret=min(ret,sum);
}
printf("%d\n",ret);
}
return 0;
}
成段更新
Lazy思想:成段更新的时候,有时候会遇到这样一种情况,比如我对1到10这个区间全部加上10,当找到[1,10]这个节点时,正常来说它所有的子节点全部都要加上10。假如这时候我又来一个操作:对1到10这个区间全部减去10,那么我们又要对[1,10]这个区间的所有节点减去10。如果有大量这样的操作,那么算法效率将大大降低。Lazy思想就在这里体现作用了。对[1,10]整体加10,当我们找到[1,10]这个节点时,我们在它身上设置一个标记,表示它整个区间加10,但并不更新它的子树。这时[1,10]减10的操作来的时候,我们只要是把标记抵消了,没有进行多余的操作。但是采用了lazy思想,当我们想要更新左右子树之前,就必须先查看当前的节点是否存在 这样的标记,把它往下一层传,这样才能保证不出错的情况下,效率也得到了提高。
HDU1698
给你一些牌子(铜,银,金)分别用1,2,3表示,一开始这些都是铜牌。现在对这些区间区间进行操作,比如说将1到5的牌子涂成银牌等等。最后统计这些牌子的总价值。
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
const int maxn=11111;
int col[maxn<<2];
int sum[maxn<<2];
void PushUp(int rt)
{
sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
void PushDown(int rt,int m)//基于Lazy思想的向下更新操作
{
if(col[rt])//如果当前节点被标记
{
col[rt<<1]=col[rt<<1|1]=col[rt];//更新左右子树的标记
sum[rt<<1]=(m-(m>>1))*col[rt];//更新左右子树的值
sum[rt<<1|1]=m>>1*col[rt];
col[rt]=0;//完成后将当前结点的标记清空。
}
}
void build(int l,int r,int rt)
{
col[rt]=0;
sum[rt]=1;
if(l==r)return ;
int m=(l+r)>>1;
build(lson);
build(rson);
PushUp(rt);
}
void update(int L,int R,int c,int l,int r,int rt)
{
if(L<=l&&r<=R)//当[l,r]落到[l,R]区域时,标记该区域,更新该区域的值,但不对子节点进行操作。
{
col[rt]=c;
sum[rt]=c*(r-l+1);
return ;
}
PushDown(rt,r-l+1);//每次更新当前结点前,都要查看当前结点有无标记
int m=(l+r)>>1;
if(L<=m)update(L,R,c,lson);
if(R>m)update(L,R,c,rson);
PushUp(rt);//更新完左右子树之后,更新父节点的标记
}
//poj3468题意:给你N个数,Q个操作,操作有两种,‘Q a b ’是询问a~b这段数的和,‘C a b c’是把a~b这段数都加上c。update成段增减 query 区间求和
#define lson l,m,rt<<1
#define rson m+1,r.rt<<1|1
#define LL long long
const int maxn=11111;
LL add[maxn<<2];
LL sum[maxn<<2];
void PushUp(int rt)
{
sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
void PushDown(int rt,int m)
{
if(add[rt])
{
add[rt<<1]+=add[rt];
add[rt<<1|1]+=add[rt];
sum[rt<<1]+=add[rt]*(m-(m>>1));
sum[rt<<1|1]+=add[rt]*(m>>1);
add[rt]=0;
}
}
void build(int l,int r,int rt)
{
add[rt]=0;
if(l==r)
{
scanf("%lld",&sum[rt]);
return ;
}
int m=(l+r)>>1;
build(lson);
build(rson);
PushUp(rt);
}
void update(int L,int R,int c,int l,int r,int rt)
{
if(L<=l&&r<=R)
{
add[rt]+=c;
sum[rt]+=(LL)c*(r-l+1);
return ;
}
PushDown(rt,r-l+1);
int m=(l+r)>>1;
if(L<=m) update(L,R,c,lson);
if(R>m) update(L,R,rson);
PushUp(rt);
}
LL query(int L,int R,int l,int r)
{
if(L<=l&&r<=R)
{
return sum[rt];
}
PushDown(rt,r-l+1);
int m=(l+r)>>1;
LL ret=0;
if(L<=m)ret+=query(L,R,lson);
if(R>m)ret+=query(L,R,rson);
return ret;
}
离散化:使用离散化可以对线段树的长度进行压缩,将一个很长的区间映射到一个较小的区间之中。比如我们有3组线段,[1,1000],[2,2000],[3,3000]。正常情况下我们需要将[1,3000]组织为线段树。但是通过离散化,我们令x[1]=1,x[2]=2,x[3]=1000,x[4]=1000,x[5]=2000,x[6]=3000(已排序),更新[1,1000]也就是将区间[1,4]更新,以此类推,只需要将[1,6]组织为线段树,大大节省了空间。
我们看一下poj2528这道题。
题意:n(n<=10000)个人依次贴海报,给出每张海报所贴的范围li,ri(1<=li<=ri<=10000000)。求出最后还能看见多少张海报。
这道题并不能简单地使用离散化,下面借用一下别人的例子。
如三张海报为:1~10 1~4 6~10
离散化时 X[ 1 ] =1, X[ 2 ] = 4, X[ 3 ] = 6, X[ 4 ] = 10
第一张海报时:墙的1~4被染为1;
第二张海报时:墙的1~2被染为2,3~4仍为1;
第三张海报时:墙的3~4被染为3,1~2仍为2。
最终,第一张海报就显示被完全覆盖了,于是输出2,但实际上明显不是这样,正确输出为3。
新的离散方法为:在相差大于1的数间加一个数,例如在上面1 4 6 10中间加5(算法中实际上1,4之间,6,10之间都新增了数的)
X[ 1 ] = 1, X[ 2 ] = 4, X[ 3 ] = 5, X[ 4 ] = 6, X[ 5 ] =10
这样之后,第一次是1~5被染成1;第二次1~2被染成2;第三次4~5被染成3
最终,1~2为2,3为1,4~5为3,于是输出正确结果3。
//POJ2528 贴报纸 离散化
#define lson l,m,rt<<1
#define rson m+1,r.rt<<1|1
const int maxn=11111;
bool hash[maxn];
int li[maxn],ri[maxn];
int X[maxn*3];
int col[maxn<<4];
int cnt;
void PushDown(int rt)
{
if(col[rt]!=-1)
{
col[rt<<1]=col[rt<<1|1]=col[rt];
cik[rt]=-1;
}
}
void update(int L,int R,int l,int r,int rt)
{
if(L<=l&&r<=R)
{
col[rt]=c;
return ;
}
PushDown(rt);
int m=(l+r)>>1;
if(L<=m) update(L,R,c,lson);
if(m<R) update(L,R,c,rson);
}
void query(int l,int r,int rt)
{
if(col[rt]!=-1)
{
if(!hash[col[rt]]) cnt++;
hash[col[rt]]=true;
return ;
}
if(l==r) return ;
int m=(l+r)>>1;
query(lson);
query(rson);
}
int Bin(int key,int n,int X[])//二分查找
{
int l=0;,r=n-1;
while(l<=r)
{
int m=(l+r)>>1;
if(X[m]==key) return m;
if(X[m]<key) l=m+1;
else r=m-1;
}
return -1;
}
int main()
{
int T,n;
scanf("%d,",&T);
while(T--)
{
scanf("%d",&n);
int nn=0;
for(int i=0;i<n;i++)//将区间离散化,将一个很大的区间映射到一个较小的区间之中
{
scanf("%d%d",&li[i],&ri[i]);
X[nn++]=li[i];
X[nn++]=ri[i];
}
sort(X,X+nn);//从小到大排序
int m=1;
for(int i=1;i<nn;i++)//去除区间重复值
{
if(X[i]!=X[i-1])X[m++]=[i];
}
for(int i=m-1;i>0;i--)//在相差大于1的数间加一个数
{
if(X[i]!=X[i-1]+1)X[m++]=X[i-1]+1;
}
sort(X,X+m);//再次排序
memset(col,-1,sizeof(col));
for(int i=0;i<n;i++)
{
int l=Bin(li[i],m,X);//用二分查找方法找到报纸的左右端点所在下标
int r=Bin(ri[i],m,X);
update(l,r,i,0,m,1);//更新线段树
}
cnt=0;
memset(hash,false,sizeof(hash));
query(0,m,1);
printf("%d\n",cnt);
}
return 0;
}
区间合并
特点:update区间替换 query询问满足条件的最左断点 ,需要在PushUp的时候对左右儿子的区间进行合并
poj3667 题意 1 a:询问是不是有连续长度为a的空房间,有的话住进最左边 2 a b:将[a,a+b-1]的房间清空
#define lson l,m,rt<<1
#define rson m+1,rt<<1|1
const int maxn=55555;
int lsum[maxn<<2],rsum[maxn<<2],msum[maxn<<2];//lsum区间左边数连续空房间的数目,rsum从区间右边数连续空房间的数目,msum该区间中连续空房间的总数目
int cover[maxn<<2];
void PushDown(int rt,int m)
{
if(cover[rt]!=-1)
{
cover[rt<<1]=cover[rt<<1|1]=cover[rt];
msum[rt<<1]=lsum[rt<<1]=rsum[rt<<1]=cover[rt]?0:m-(m>>1);
msum[rt<<1|1]=lsum[rt<<1|1]=rsum[rt<<1|1]=cover[rt]?0:(m>>1);
cover[rt]=-1;
}
}
void PushUp(int rt,int m)
{
lsum[rt]=lsum[rt<<1];//当前节点的lsum为左子树的lsum
rsum[rt]=rsum[rt<<1|1];//当前结点的rsum为右子树的rsum
if(lsum[rt]==m-(m>>1))lsum[rt]+=lsum[rt<<1|1];//如果当前结点的lsum等于左子树的区间长度,则再加上右子树的 lsum
if(rsum[rt]==(m>>1))rsum[rt]+=rsum[rt>>1];//如果当前结点的rsum 等于右子树的区间长度,则再加上左子树的rsum
msum[rt]=max(lsum[rt<<1|1]+rsum[rt<<1],max(msum[rt<<1],msum[rt<<1|1]));//当前结点的连续空房间总数目为三者的最大值
}
void build(int l,int r,int rt)
{
msum[rt]=lsum[rt]=rsum[rt]=r-l+1;
cover[rt]=-1;
if(l==r) return ;
int m=(l+r)>>1;
build(lson);
build(rson);
}
void update(int L,int R,int c,int l,int r,int rt)
{
if(L<=l&&r<=R)
{
msum[rt]=lsum[rt]=rsum[rt]=c?0:r-l+1;
cover[rt]=c;
return ;
}
PushDown(rt,r-l+1);
int m=(l+r)>>1;
if(L<=m) update(L,R,c,lson);
if(m<R) update(L,R,c,rson);
PushUp(rt,r-l+1);
}
int query(int w,int l,int r,int rt)
{
if(l==r) return l;
PushDown(rt,r-l+1);
int m=(l+r)>>1;
if(msum[rt<<1]>=w)return query(w,lson);
else if(rsum[rt<<1]+lsum[rt<<1|1]>=w) return m-rsum[rt<<1]+1;
return query(w,rson);
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
build(1,n,1);
while(m--)
{
int op,a,b;
scanf("%d",&op);
if(op==1)
{
scanf("%d",&a);
if(msum[1]<a) puts("0");
else
{
int p=query(a,1,n,1);
printf("%d\n",p);
update(p,p+a-1,1,1,n,1);
}
}
else
{
scanf("%d%d",&a,&b);
update(a,a+b-1,0,1,n,1);
}
}
return 0;
}