浅谈线段树 (例题:[USACO08FEB]酒店Hotel)By cellur925
今天我们说说线段树。
我个人还是非常欣赏这种数据结构的。(逃)因为它足够优美,有递归结构,有左子树和右子树,还有二分的思想。
emm这个文章打算自用,就不写那些基本的操作了...
1° 简单的懒标记(仅含加法)
当我们进行区间修改(比如同时加上一个数)时,我们现在也许暂时不用它,可以当需要用的时候再改。这个时候我们就需要做个标记,这个标记就是懒标记,$lazy$。如果在后续的指令中需要从p向下递归,我们这时候检查它是否有标记。若有,就按照标记更新两个子节点,同时为子节点增加标记,清除p的标记。
比如最简单的区间修改(加法)
void spread(int p) {
if(t[p].l==t[p].r) return ; t[p*2].val+=t[p].lazy*(t[p*2].r-t[p*2].l+1); t[p*2+1].val+=t[p].lazy*(t[p*2+1].r-t[p*2+1].l+1); //标记释放威力 t[p*2].lazy+=t[p].lazy; t[p*2+1].lazy+=t[p].lazy; //标记下传 t[p].lazy=0; //清空自身标记 }
也就是说,其实标记是为自己的儿子们准备的,而自己已经修改了。当自己的儿子接手了标记的衣钵,父亲也就不需要存在标记的。
- 我的习惯:$spread$常写,函数内判断条件苛刻。
- 一个习惯的change函数写法:
-
-
void change(int p,int l,int r,int op) {//op==1 I have rooms! //op==2 I lose rooms! spread(p); //标记下放 if(t[p].l==l&&t[p].r==r)//边界判断 { if(op==1) t[p].lmax=t[p].rmax=t[p].sum=t[p].r-t[p].l+1; else t[p].lmax=t[p].rmax=t[p].sum=0; t[p].lazy=op;//这里也有懒标记更新 return ; } int mid=(t[p].l+t[p].r)>>1; if(l>mid) change(p*2+1,l,r,op); else if(r<=mid) change(p*2,l,r,op);//标记判断 else change(p*2,l,mid,op),change(p*2+1,mid+1,r,op);
//更新 renew(p); }
-
-
例题1 【模板】线段树 1
裸的懒标记应用。
1 #include<cstdio> 2 #include<algorithm> 3 #define maxn 100090 4 5 using namespace std; 6 typedef long long ll; 7 8 int n,m; 9 int a[maxn]; 10 struct SegmentTree{ 11 int l,r; 12 ll lazy,val; 13 }t[maxn*4]; 14 15 void build(int p,int l,int r) 16 { 17 t[p].l=l,t[p].r=r; 18 if(l==r) 19 { 20 t[p].val=a[l]; 21 return ; 22 } 23 int mid=(l+r)>>1; 24 build(p*2,l,mid); 25 build(p*2+1,mid+1,r); 26 t[p].val=t[p*2].val+t[p*2+1].val; 27 } 28 29 void spread(int p) 30 { 31 if(t[p].l==t[p].r) return ; 32 t[p*2].val+=t[p].lazy*(t[p*2].r-t[p*2].l+1); 33 t[p*2+1].val+=t[p].lazy*(t[p*2+1].r-t[p*2+1].l+1); 34 t[p*2].lazy+=t[p].lazy; 35 t[p*2+1].lazy+=t[p].lazy; 36 t[p].lazy=0; 37 } 38 39 void change(int p,int l,int r,int k) 40 { 41 spread(p); 42 if(t[p].l==l&&t[p].r==r) 43 { 44 t[p].val+=k*(r-l+1); 45 t[p].lazy+=k; 46 return ; 47 } 48 int mid=(t[p].l+t[p].r)>>1; 49 if(l>mid) change(p*2+1,l,r,k); 50 else if(r<=mid) change(p*2,l,r,k); 51 else change(p*2,l,mid,k),change(p*2+1,mid+1,r,k); 52 t[p].val=t[p*2].val+t[p*2+1].val; 53 } 54 55 ll ask(int p,int l,int r) 56 { 57 spread(p); 58 if(t[p].l==l&&t[p].r==r) return t[p].val; 59 int mid=(t[p].l+t[p].r)>>1; 60 if(l>mid) return ask(p*2+1,l,r); 61 else if(r<=mid) return ask(p*2,l,r); 62 else return ask(p*2,l,mid)+ask(p*2+1,mid+1,r); 63 } 64 65 int main() 66 { 67 scanf("%d%d",&n,&m); 68 for(int i=1;i<=n;i++) 69 scanf("%d",&a[i]); 70 build(1,1,n); 71 for(int i=1;i<=m;i++) 72 { 73 int opt=0; 74 scanf("%d",&opt); 75 if(opt==1) 76 { 77 int x=0,y=0,k=0; 78 scanf("%d%d%d",&x,&y,&k); 79 change(1,x,y,k); 80 } 81 else if(opt==2) 82 { 83 int x=0,y=0; 84 scanf("%d%d",&x,&y); 85 printf("%lld\n",ask(1,x,y)); 86 } 87 } 88 return 0; 89 }
By hzwer
题解
线段树
每个节点记录该段最长连续长度
为了合并还要记录坐标开始的连续长度,右边开始的连续长度
这里用到了线段树中另一个常见的思想。平常我们用线段树大多都是在维护一个值,而遇到一些复杂的信息需要维护时,我们就很难纯粹地加加减减,那么我们不妨换一种思路,多维护一些信息。最早应用这个思想的是最大子段和的维护,详情戳。(当时我还在$tsoi$讲过内qwq)就是多维护了$lmax$,$rmax$。这样父亲线段的最值可以由左儿子的$val$、右儿子的$val$、左儿子的$rmax$加右儿子的$lmax$更新维护来。
那么回到本题:一句话题意就是在维护,最大连续空房。综合之前分析,知道如何用懒标记还有知道需要维护哪些信息后,这道题就比较简单了。
$Code$
1 #include<cstdio> 2 #include<algorithm> 3 #define maxn 50090 4 5 using namespace std; 6 7 int n,m; 8 struct SegmentTree{ 9 int l,r; 10 int lazy; 11 int rmax,lmax,sum; 12 }t[maxn*4]; 13 14 void build(int p,int l,int r) 15 { 16 t[p].l=l,t[p].r=r; 17 t[p].lmax=t[p].rmax=t[p].sum=r-l+1; 18 if(l==r) return ; 19 int mid=(l+r)>>1; 20 build(p*2,l,mid); 21 build(p*2+1,mid+1,r); 22 } 23 24 void spread(int p) 25 { 26 if(t[p].l==t[p].r) return ; 27 if(t[p].lazy==2) 28 { 29 t[p*2].lazy=t[p*2+1].lazy=2; 30 t[p*2].lmax=t[p*2].rmax=t[p*2+1].lmax=t[p*2+1].rmax=0; 31 t[p*2].sum=t[p*2+1].sum=0; 32 } 33 else if(t[p].lazy==1) 34 { 35 t[p*2].lazy=t[p*2+1].lazy=1; 36 t[p*2].lmax=t[p*2].rmax=t[p*2].sum=t[p*2].r-t[p*2].l+1; 37 t[p*2+1].lmax=t[p*2+1].rmax=t[p*2+1].sum=t[p*2+1].r-t[p*2+1].l+1; 38 } 39 t[p].lazy=0; 40 } 41 42 void renew(int p) 43 { 44 if(t[p*2].sum==t[p*2].r-t[p*2].l+1) 45 t[p].lmax=t[p*2].r-t[p*2].l+1+t[p*2+1].lmax; 46 else t[p].lmax=t[p*2].lmax; 47 if(t[p*2+1].sum==t[p*2+1].r-t[p*2+1].l+1) 48 t[p].rmax=t[p*2+1].r-t[p*2+1].l+1+t[p*2].rmax; 49 else t[p].rmax=t[p*2+1].rmax; 50 t[p].sum=max(max(t[p*2].sum,t[p*2+1].sum),t[p*2].rmax+t[p*2+1].lmax); 51 } 52 53 void change(int p,int l,int r,int op) 54 {//op==1 I have rooms! 55 //op==2 I lose rooms! 56 spread(p); 57 if(t[p].l==l&&t[p].r==r) 58 { 59 if(op==1) t[p].lmax=t[p].rmax=t[p].sum=t[p].r-t[p].l+1; 60 else t[p].lmax=t[p].rmax=t[p].sum=0; 61 t[p].lazy=op; 62 return ; 63 } 64 int mid=(t[p].l+t[p].r)>>1; 65 if(l>mid) change(p*2+1,l,r,op); 66 else if(r<=mid) change(p*2,l,r,op); 67 else change(p*2,l,mid,op),change(p*2+1,mid+1,r,op); 68 renew(p); 69 } 70 71 int ask(int p,int len) 72 { 73 spread(p); 74 int mid=(t[p].l+t[p].r)>>1; 75 if(t[p].l==t[p].r) return t[p].l;//找到真正精确的地方了 76 if(t[p*2].sum>=len) return ask(p*2,len); 77 //左面就已经有足够空房 继续向下找更小更精细的 78 else if(t[p*2].rmax+t[p*2+1].lmax>=len) return mid-t[p*2].rmax+1; 79 //跨越边界的部分有足够空房 80 else return ask(p*2+1,len); 81 //否则只能去右子树找连续空房 82 } 83 84 int main() 85 { 86 scanf("%d%d",&n,&m); 87 build(1,1,n); 88 for(int i=1;i<=m;i++) 89 { 90 int opt=0; 91 scanf("%d",&opt); 92 if(opt==1) 93 { 94 int x=0; 95 scanf("%d",&x); 96 if(t[1].sum<x){printf("0\n");continue;} 97 int tmp=ask(1,x); 98 printf("%d\n",tmp); 99 change(1,tmp,tmp+x-1,2); 100 } 101 else if(opt==2) 102 { 103 int x=0,y=0; 104 scanf("%d%d",&x,&y); 105 change(1,x,x+y-1,1); 106 } 107 } 108 return 0; 109 }
Update:话说最近做了不少(?)线段树,有一种感觉十分友好,就是那种操作一定数量后操作失效的(如开方),那么我们可以记录一个区间最大值来检验是否还需要操作,思想很妙。
还有:线段树这种用左儿子+右儿子+左右儿子交界来更新答案的这种思想,最初是在维护最大子段和看到的。
Update:同时维护区间乘法&区间加法?再加一个懒标记记录乘法!要注意的是区间乘法修改时加法懒标记也要乘上修改值,$update$时加法懒标记也要乘上修改值,也就是加法一直在听着乘法的话。
1 #include<cstdio> 2 #include<algorithm> 3 #define maxn 100090 4 5 using namespace std; 6 typedef long long ll; 7 8 int n,m; 9 int seq[maxn]; 10 ll moder; 11 struct SegmentTree{ 12 int l,r; 13 ll lazy1,lazy2,sum; 14 }t[maxn*4]; 15 16 void build(int p,int l,int r) 17 { 18 t[p].l=l,t[p].r=r,t[p].lazy1=1; 19 if(l==r) 20 { 21 t[p].sum=seq[l]; 22 return ; 23 } 24 int mid=(l+r)>>1; 25 build(p<<1,l,mid); 26 build(p<<1|1,mid+1,r); 27 t[p].sum=(t[p<<1].sum+t[p<<1|1].sum)%moder; 28 } 29 30 void update(int p) 31 { 32 if(!t[p].lazy2&&t[p].lazy1==1) return ; 33 if(t[p].l==t[p].r) return ; 34 ll add=t[p].lazy2,mul=t[p].lazy1; 35 (t[p<<1].lazy1*=mul)%=moder; 36 (t[p<<1|1].lazy1*=mul)%=moder; 37 (t[p<<1].lazy2*=mul)%=moder; 38 (t[p<<1|1].lazy2*=mul)%=moder; 39 (t[p<<1].lazy2+=add)%=moder; 40 (t[p<<1|1].lazy2+=add)%=moder; 41 t[p<<1].sum=(mul*t[p<<1].sum%moder+1ll*add*(t[p<<1].r-t[p<<1].l+1)%moder)%moder; 42 t[p<<1|1].sum=(mul*t[p<<1|1].sum%moder+1ll*add*(t[p<<1|1].r-t[p<<1|1].l+1)%moder)%moder; 43 t[p].lazy1=1; 44 t[p].lazy2=0; 45 } 46 47 void change(int p,int l,int r,ll k,int op) 48 { 49 update(p); 50 if(t[p].l==l&&t[p].r==r) 51 { 52 if(op==1) (t[p].sum*=k)%=moder,(t[p].lazy1*=k)%=moder,(t[p].lazy2*=k)%moder; 53 else (t[p].sum+=k*(r-l+1))%=moder,(t[p].lazy2+=k)%moder; 54 return ; 55 } 56 int mid=(t[p].l+t[p].r)>>1; 57 if(l>mid) change(p<<1|1,l,r,k,op); 58 else if(r<=mid) change(p<<1,l,r,k,op); 59 else change(p<<1,l,mid,k,op),change(p<<1|1,mid+1,r,k,op); 60 t[p].sum=(t[p<<1].sum+t[p<<1|1].sum)%moder; 61 } 62 63 ll ask(int p,int l,int r) 64 { 65 update(p); 66 if(t[p].l==l&&t[p].r==r) return t[p].sum; 67 int mid=(t[p].l+t[p].r)>>1; 68 if(l>mid) return ask(p<<1|1,l,r); 69 else if(r<=mid) return ask(p<<1,l,r); 70 else return (ask(p<<1,l,mid)%moder+ask(p<<1|1,mid+1,r)%moder)%moder; 71 } 72 73 int main() 74 { 75 scanf("%d%d%lld",&n,&m,&moder); 76 for(int i=1;i<=n;i++) scanf("%d",&seq[i]); 77 build(1,1,n); 78 for(int i=1;i<=m;i++) 79 { 80 int op=0,x=0,y=0;ll k=0; 81 scanf("%d",&op); 82 if(op==1) 83 { 84 scanf("%d%d%lld",&x,&y,&k); 85 change(1,x,y,k,1); 86 } 87 else if(op==2) 88 { 89 scanf("%d%d%lld",&x,&y,&k); 90 change(1,x,y,k,2); 91 } 92 else if(op==3) 93 { 94 scanf("%d%d",&x,&y); 95 printf("%lld\n",ask(1,x,y)%moder); 96 } 97 } 98 return 0; 99 }