关于线段树基础
首先明白什么是线段树:
线段树是一棵二叉树,每个节点表示序列上的一段区间,其中根节点表示区间[1,n]
从根节点开始,只要区间长度不为1,就将区间划分为两半,并分给两个子结点
如下图,就是n=8的线段树:
当节点表示区间[l,r],当l≠r时,左孩子表示[l,(l+r)/2],右孩子表示[(l+r)/2+1,r]
当l=r时,该节点为叶子节点
线段树的基本性质:
性质1.总节点数为2n-1
性质2.线段树并不一定是一棵完全二叉树,最后一层可能为空,且空结点的个数可能能达到2n个,因此最好开一棵4n的数组来避免LE
性质3.层数约为,即每个叶节点到根节点的距离约为个节点组合而成性质
性质4.任何区间都可以用不超过2个结点组成
线段树工作原理:
我们先在线段树的每个节点都储存该区间的总和。假设起始都为0
对于操作1,令A[x]加上y,我们先从根节点出发,找到[x,x],比如x=3
此时,路径上每个区间都包含x
接下来,将这些区间储存的总和都加上y,比如y=4
这就是单点修改的工作原理,跟树状数组有些相似之处,即修改一个点会对其祖先有同样效果的影响,即要将其所有的祖先结点都通过修改的方式加上同样的效果来保证影响的后效性。
问题2:询问区间[x,y]的总和
就是寻找这个区间包含的最长且完整的、没有交集的若干区间。
下面是线段树的模板:
代码如下:
1、数组写法
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 5 #define ll long long 6 7 inline int get()//快读 8 { 9 char c; 10 int sign=1; 11 while((c=getchar())<'0'||c>'9') 12 { 13 if(c=='-') 14 { 15 sign=-1; 16 } 17 } 18 int res=c-'0'; 19 while((c=getchar())>='0'&&c<='9') 20 { 21 res=res*10+c-'0'; 22 } 23 return res*sign; 24 } 25 26 const int N=1e5+5; 27 int n,m,a[N]; 28 int add[N*4]; 29 ll sum[N*4]; 30 31 void build(int k,int l,int r)//建树 32 { 33 if(l==r)//区间长度为1,是叶节点,给它赋值 34 { 35 sum[k]=a[l]; 36 return; 37 } 38 int mid=l+r>>1; 39 build(k<<1,l,mid);//建他的左儿子 40 build(k<<1|1,mid+1,r);//建他的右儿子 41 sum[k]=sum[k<<1]+sum[k<<1|1];//他的区间和等于他两个儿子的和 42 } 43 44 int Add(int k,int l,int r,int v)//区间加并更新值 45 { 46 add[k]+=v;//给懒标记更新值 47 sum[k]+=(ll)v*(r-l+1);//给区间和更新值 48 } 49 50 void pushdown(int k,int l,int r,int mid)//进行懒标记的下放 51 { 52 if(add[k]==0) 53 { 54 return ;//如果懒标记是0就直接跳过,不用在乎他 55 } 56 Add(k<<1,l,mid,add[k]);//懒标记让他的两个儿子更新值 57 Add(k<<1|1,mid+1,r,add[k]); 58 add[k]=0;//最后要将懒标记赋值0,以便下次懒标记更新值时不会受到上一次标记的影响 59 } 60 61 ll query(int k,int l,int r,int x,int y)//询问区间和,其中l和r代表的是当前访问的区间端点,x和y代表的是想要求和的区间端点 62 { 63 if(l>=x&&r<=y) return sum[k];//如果当前访问的区间在求和区间以内,就将它的值返回给res 64 int mid=l+r>>1; 65 ll res=0; 66 pushdown(k,l,r,mid);//先进行标记下放,确保当前访问的区间是最新的 67 if(x<=mid) res+=query(k<<1,l,mid,x,y); 68 if(y>mid)res+=query(k<<1|1,mid+1,r,x,y); 69 return res; 70 } 71 72 int modify(int k,int l,int r,int x,int y,int v)//给区间的每个元素加值得操作 73 { 74 if(l>=x&&r<=y) return Add(k,l,r,v);//就是给它这个区间做标记 75 int mid=l+r>>1; 76 pushdown(k,l,r,mid); 77 if(x<=mid) modify(k<<1,l,mid,x,y,v); 78 if(y>mid) modify(k<<1|1,mid+1,r,x,y,v); 79 sum[k]=sum[k<<1]+sum[k<<1|1]; 80 } 81 82 int main() 83 { 84 n=get(),m=get(); 85 for(int i=1;i<=n;++i) 86 { 87 a[i]=get(); 88 } 89 build(1,1,n); 90 ll a1,b,c,d,e,f; 91 while(m--) 92 { 93 scanf("%lld",&a1); 94 switch(a1) 95 { 96 case 1:{ 97 scanf("%lld%lld%lld",&b,&c,&d); 98 modify(1,1,n,b,c,d); 99 break; 100 } 101 case 2:{ 102 scanf("%lld%lld",&e,&f); 103 printf("%lld\n",query(1,1,n,e,f)); 104 break; 105 } 106 } 107 } 108 return 0; 109 }
2、
结构体写法:
1 #include<cstdio> 2 #define ll long long 3 4 using namespace std; 5 6 const int N=1e5+10; 7 ll n,m,z,l,r,j,s[N]; 8 9 struct node{ 10 ll sum,add; 11 }tree[4*N]; 12 13 ll read(){ 14 int f=1,x=0; 15 char ch=getchar(); 16 while(ch>'9'||ch<'0'){ 17 if(ch=='-'){ 18 f=-1; 19 } 20 ch=getchar(); 21 } 22 while(ch<='9'&&ch>='0'){ 23 x=x*10+ch-'0'; 24 ch=getchar(); 25 } 26 return f*x; 27 } 28 29 void build(ll k,ll l,ll r) 30 { 31 if(l==r) 32 { 33 tree[k].sum=s[l]; 34 return; 35 } 36 ll mid=l+r>>1; 37 build(k<<1,l,mid); 38 build(k<<1|1,mid+1,r); 39 tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum; 40 } 41 42 void add(ll k,ll l,ll r,ll j) 43 { 44 tree[k].add+=j; 45 tree[k].sum+=(r-l+1)*j; 46 } 47 48 void pushdown(ll k,ll l,ll r,ll mid) 49 { 50 if(tree[k].add==0) 51 { 52 return; 53 } 54 add(k<<1,l,mid,tree[k].add); 55 add(k<<1|1,mid+1,r,tree[k].add); 56 tree[k].add=0; 57 return; 58 } 59 60 ll query(ll k,ll l,ll r,ll x,ll y) 61 { 62 ll res=0; 63 if(l>=x&&r<=y) 64 { 65 return tree[k].sum; 66 } 67 ll mid=l+r>>1; 68 pushdown(k,l,r,mid); 69 if(x<=mid) res+=query(k<<1,l,mid,x,y); 70 if(y>mid) res+=query(k<<1|1,mid+1,r,x,y); 71 return res; 72 } 73 74 void modify(ll k,ll l,ll r,ll x,ll y,ll j){ 75 if(l>=x&&r<=y) 76 { 77 add(k,l,r,j); 78 return; 79 } 80 ll mid=l+r>>1; 81 pushdown(k,l,r,mid); 82 if(x<=mid) modify(k<<1,l,mid,x,y,j); 83 if(y>mid) modify(k<<1|1,mid+1,r,x,y,j); 84 tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum; 85 } 86 87 int main() 88 { 89 n=read(),m=read(); 90 for(ll i=1;i<=n;i++) 91 { 92 s[i]=read(); 93 } 94 build(1,1,n); 95 while(m--) 96 { 97 z=read(); 98 if(z==1) 99 { 100 l=read(),r=read(),j=read(); 101 modify(1,1,n,l,r,j); 102 } 103 else 104 { 105 l=read(),r=read(); 106 printf("%lld\n",query(1,1,n,l,r)); 107 } 108 } 109 return 0; 110 }
(这篇代码作者为Audrey_Hall,让我们向它致敬!respect!)
以上就是线段树的基本操作了,上面代码会存疑的地方大多都有注释,最后只解释有关懒标记的操作:
在没有懒标记的时候,之前修改的位置都要立即更新,所以无形之中复杂度就高了很多,刺死就突出了懒标记的作用:
懒标记,顾名思义就是很懒的标记,每当我进行区间加的操作时,它起到了一个给目标区间记录的作用,即区间不会立即修改,而是将每次的操作记录并累计下来,只有当访问的元素包括此元素的时候才会将此元素依照对应的懒标记进行单点修改,也就从原来的直接降成,复杂度降了很多诶!
线段树二模板:
代码如下:
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 5 #define ll long long 6 7 using namespace std; 8 9 const int N=1e5+10; 10 ll n,m,l,r,mod=571373,a[N]; 11 12 struct node 13 { 14 ll value; 15 ll add,mul; 16 }tree[4*N]; 17 18 inline ll get() 19 { 20 char c; 21 int sign=1; 22 while((c=getchar())<'0'||c>'9') 23 { 24 if(c=='-') 25 { 26 sign=-1; 27 } 28 } 29 ll res=c-'0'; 30 while((c=getchar())>='0'&&c<='9') 31 { 32 res=res*10+c-'0'; 33 } 34 return res*sign; 35 } 36 37 void build(ll k,ll l,ll r) 38 { 39 tree[k].mul=1; 40 if(l==r) 41 { 42 tree[k].value=a[r]; 43 return; 44 } 45 ll mid=l+r>>1; 46 build(k<<1,l,mid); 47 build(k<<1|1,mid+1,r); 48 tree[k].value=tree[k<<1].value+tree[k<<1|1].value; 49 } 50 51 void add(ll k,ll l,ll r,ll j) 52 { 53 tree[k].value=(tree[k].value+(r-l+1)*j)%mod; 54 tree[k].add=(tree[k].add+j)%mod; 55 } 56 57 void multiply(ll k,ll l,ll r,ll j) 58 { 59 tree[k].value=tree[k].value*j%mod; 60 tree[k].add=tree[k].add*j%mod; 61 tree[k].mul=tree[k].mul*j%mod; 62 } 63 64 void pushdown(ll k,ll l,ll r) 65 { 66 if(tree[k].mul!=1) 67 { 68 ll mid=l+r>>1; 69 multiply(k<<1,l,mid,tree[k].mul); 70 multiply(k<<1|1,mid+1,r,tree[k].mul); 71 tree[k].mul=1; 72 } 73 if(tree[k].add!=0) 74 { 75 ll mid=l+r>>1; 76 add(k<<1,l,mid,tree[k].add); 77 add(k<<1|1,mid+1,r,tree[k].add); 78 tree[k].add=0; 79 } 80 } 81 82 ll query(ll k,ll l,ll r,ll x,ll y) 83 { 84 ll res=0; 85 if(l>=x&&r<=y) 86 { 87 return tree[k].value; 88 } 89 ll mid=l+r>>1; 90 pushdown(k,l,r); 91 if(x<=mid) res+=query(k<<1,l,mid,x,y)%mod; 92 if(y>=mid+1) res+=query(k<<1|1,mid+1,r,x,y)%mod; 93 return res; 94 } 95 96 void modify(ll k,ll l,ll r,ll x,ll y,ll a) 97 { 98 if(l>=x&&r<=y) 99 { 100 add(k,l,r,a); 101 return; 102 } 103 pushdown(k,l,r); 104 ll mid=l+r>>1; 105 if(x<=mid) modify(k<<1,l,mid,x,y,a); 106 if(y>=mid+1) modify(k<<1|1,mid+1,r,x,y,a); 107 tree[k].value=(tree[k<<1].value+tree[k<<1|1].value)%mod; 108 } 109 110 void cheng(ll k,ll l,ll r,ll x,ll y,ll a) 111 { 112 if(l>=x&&r<=y) 113 { 114 multiply(k,l,r,a); 115 return; 116 } 117 pushdown(k,l,r); 118 ll mid=l+r>>1; 119 if(x<=mid) cheng(k<<1,l,mid,x,y,a); 120 if(y>=mid+1) cheng(k<<1|1,mid+1,r,x,y,a); 121 tree[k].value=(tree[k<<1].value+tree[k<<1|1].value)%mod; 122 } 123 124 int main() 125 { 126 n=get();mod=get(); 127 for(int i=1;i<=n;i++) 128 { 129 a[i]=get(); 130 } 131 m=get(); 132 build(1,1,n); 133 for(int i=1;i<=m;i++) 134 { 135 ll a,b,c,k; 136 a=get(); 137 switch(a) 138 { 139 case 1: 140 { 141 b=get(),c=get(),k=get(); 142 cheng(1,1,n,b,c,k); 143 break; 144 } 145 case 2: 146 { 147 b=get(),c=get(),k=get(); 148 modify(1,1,n,b,c,k); 149 break; 150 } 151 case 3: 152 { 153 b=get(),c=get(); 154 cout<<query(1,1,n,b,c)%mod<<endl; 155 } 156 } 157 } 158 return 0; 159 }
来几道例题练练手吧!
其实本质上就是区间修改和区间查询,记录一个数组kai[i]代表这个区间开灯的个数,对于每一次区间修改操作,就相当于将kai[i]修改成 区间的长度r-l+1(区间里灯的个数) -kai[i],而对于区间中开灯的个数,就等于区间和(因为开灯是1,关灯是0)。当然,要求再加一个数组ct[i]代表整个区间进行的开关灯次数(操作的对象一定是整个区间),对于标记下放操作,判断如果操作次数是偶数次,就直接return就行,因为操作偶数次后没有任何改变。
代码如下:
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 5 #define ll long long 6 7 using namespace std; 8 9 const int N=2e5+10; 10 ll kai[N*4],a[N*4],ct[N*4]; 11 char ha[N*4]; 12 13 void build(int k,int l,int r) 14 { 15 if(l==r) 16 { 17 kai[k]=a[l]; 18 return; 19 } 20 int mid=l+r>>1; 21 build(2*k,l,mid); 22 build(2*k+1,mid+1,r); 23 kai[k]=kai[k*2]+kai[k*2+1]; 24 } 25 26 void Add(int k,int l,int r) 27 { 28 ct[k]++; 29 kai[k]=r-l+1-kai[k]; 30 } 31 32 void pushdown(int k,int l,int r) 33 { 34 if(ct[k]%2==0) return; 35 int mid=l+r>>1; 36 Add(k*2,l,mid); 37 Add(k*2+1,mid+1,r); 38 ct[k]=0; 39 } 40 41 int query(int k,int l,int r,int x,int y) 42 { 43 ll res=0; 44 if(l>=x&&r<=y) return kai[k]; 45 int mid=l+r>>1; 46 pushdown(k,l,r); 47 if(x<=mid) res+=query(k*2,l,mid,x,y); 48 if(y>=mid+1) res+=query(k*2+1,mid+1,r,x,y); 49 return res; 50 } 51 52 void modify(int k,int l,int r,int x,int y) 53 { 54 if(l>=x&&r<=y) return Add(k,l,r); 55 int mid=l+r>>1; 56 pushdown(k,l,r); 57 if(x<=mid) modify(k*2,l,mid,x,y); 58 if(y>=mid+1) modify(k*2+1,mid+1,r,x,y); 59 kai[k]=kai[k*2]+kai[k*2+1]; 60 } 61 62 int main(){ 63 int m,n; 64 cin>>n>>m; 65 for(int i=1;i<=n;i++) 66 { 67 cin>>ha[i]; 68 a[i]=ha[i]-'0'; 69 } 70 build(1,1,n); 71 for(int i=1;i<=m;i++) 72 { 73 int a; 74 ll b,c; 75 cin>>a; 76 if(a==1) 77 { 78 cin>>b>>c; 79 cout<<query(1,1,n,b,c)<<endl; 80 } 81 else 82 { 83 cin>>b>>c; 84 modify(1,1,n,b,c); 85 } 86 } 87 return 0; 88 }
关于线段树3,由于本人能力有限,还无法进行讲解,敬请期待!
再见!