【数据结构】线段树专辑

来源:互联网 发布:广电网络集客业务汇报 编辑:程序博客网 时间:2024/05/22 14:10

线段树是一种高效的数据结构。它可以将一段区间组织为二叉树的结构,比如比如将[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|1const 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|1const 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|1const 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|1const 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 longconst 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被染为23~4仍为1
第三张海报时:墙的3~4被染为31~2仍为2
最终,第一张海报就显示被完全覆盖了,于是输出2,但实际上明显不是这样,正确输出为3

新的离散方法为:在相差大于1的数间加一个数,例如在上面1 4 6 10中间加5算法中实际上14之间,610之间都新增了数的)

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~22314~53,于是输出正确结果3


//POJ2528 贴报纸 离散化#define lson l,m,rt<<1#define rson m+1,r.rt<<1|1const 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;}



原创粉丝点击