浅析线段树
1.1何为线段树
线段树是一种支持O(nlogn)的时间复杂度进行区间查询、区间修改,O(logn)进行单点修改、单点查询的数据结构,它比朴素算法(O(n2))快很多,能支持n<=1000000范围,比树状数组、ST表代码实现难度大,且常数较大,但能实现的功能多,“树状数组能做的线段树都能做,线段树能做的树状数组不一定能做”
1.2原理概述
既然叫线段树,那肯定是棵树,准确的说,是一棵二叉树,记u为节点编号,l[u]为区间左边界,r[u]为区间右边界,若该店非叶子节点,则记mid=(l[u+r[u])/2,u的左孩子编号为u*2,表示区间为(l[u],mid),u的右孩子编号为u*2+1,表示区间为(mid+1,l[u]);
考虑n=8的情况:
其中,圆圈内的点为节点的编号,而括号内则表示该节点所表示的区间。加上数值后,父亲节点的值为两儿子的和。
2.1代码实现
在前面提到,树状数组能做的线段树一定能做,我们先看一道模板题:【模板】树状数组 1
首先,对于区间有n个点的话,线段树要将数组开到4*n,这个一定要记得。
该题的要求是单点修改和区间求和,首先是建树:
void build(int u,int l1,int r1) { l[u]=l1; r[u]=r1;//区间的左、右端点; if (l1==r1) { z[u]=a[l1];//因为l1==r1==该点的坐标,将该点的值赋给z[u] return; } build(u*2,l1,(l1+r1)/2); build(u*2+1,(l1+r1)/2+1,r1);//若非叶节点,继续建树 z[u]=z[u*2]+z[u*2+1];//记录区间值 }
l[u]、r[u]的含义如上述,z[u]表示该区间的和;
其次,是实现单点修改的程序:
void jia(int u,int x,int k) {
if (l[u]>x||r[u]<x) return;//若x在该区间之外,直接返回 if (l[u]==r[u])//因为该区间包含x,若该区间是个点则这个点就是x { z[u]+=k; return; } jia(u*2,x,k); jia(u*2+1,x,k); z[u]=z[u*2]+z[u*2+1];//更新z[u] }
之后是区间求和:
int qui(int u,int l1,int r1) { if (l[u]>r1||r[u]<l1) return 0;//若该区间不在所求区间内,返回0 else if (l[u]>=l1&&r[u]<=r1) return z[u];//若该区间被所求区间包含,返回该区间的值 else return qui(u*2,l1,r1)+qui(u*2+1,l1,r1);//继续向下找 }
完整代码如下:
#include<bits/stdc++.h> using namespace std; int n,m,i,a[500001],z[2000001],l[2000001],r[2000001],q,x,y; void build(int u,int l1,int r1) { l[u]=l1; r[u]=r1; if (l1==r1) { z[u]=a[l1]; return; } build(u*2,l1,(l1+r1)/2); build(u*2+1,(l1+r1)/2+1,r1); z[u]=z[u*2]+z[u*2+1]; } void jia(int u,int x,int k) { if (l[u]>x||r[u]<x) return; if (l[u]==r[u]) { z[u]+=k; return; } jia(u*2,x,k); jia(u*2+1,x,k); z[u]=z[u*2]+z[u*2+1]; } int qui(int u,int l1,int r1) { if (l[u]>r1||r[u]<l1) return 0; else if (l[u]>=l1&&r[u]<=r1) return z[u]; else return qui(u*2,l1,r1)+qui(u*2+1,l1,r1); } int main() { scanf("%d%d",&n,&m); for (i=1;i<=n;i++) scanf("%d",&a[i]); build(1,1,n); for (i=1;i<=m;i++) { scanf("%d%d%d",&q,&x,&y); if (q==1) jia(1,x,y); else printf("%d\n",qui(1,x,y)); } return 0; }
2.2标记下传
接下来,我们看一道区间修改的题:【模板】树状数组 2
有人觉得区间修改就是对该区间每个点进行单点修改,但仔细算算,它的时间复杂度还不如朴素算法快呢,这就用得到线段树的另一个特点:懒标记。
当我们发现有一区间在所求区间之内时,先不要对该区间下的节点进行修改,而是对其修改后进行标记,若需要求该区间下的值时再下传
实现下传代码如下:
void xiafang(int u)//下放 { z[u*2]+=c[u]*(r[u*2]-l[u*2]+1);//c[u]即为该区间内每点应该加上的值,则区间加上该区间点的个数乘上c[u] c[u*2]+=c[u]; z[u*2+1]+=c[u]*(r[u*2+1]-l[u*2+1]+1); c[u*2+1]+=c[u]; c[u]=0;//记得清零 }
可能有些看不懂吧?那么,全部代码奉上:
#include <bits/stdc++.h> using namespace std; int n,m,i,q,x,y,a,j,l[50000001],r[50000001],z[50000001],w[50000001],c[50000001],k,x1[5000001];//至少4倍 void xiafang(int u) { z[u*2]+=c[u]*(r[u*2]-l[u*2]+1); c[u*2]+=c[u]; z[u*2+1]+=c[u]*(r[u*2+1]-l[u*2+1]+1); c[u*2+1]+=c[u]; c[u]=0; } void build(int u,int l1,int r1) { l[u]=l1; r[u]=r1; if (l1==r1) { z[u]=x1[l1]; return; } build(u*2,l1,(l1+r1)/2); build(u*2+1,(l1+r1)/2+1,r1); z[u]=z[u*2]+z[u*2+1]; } void jia(int u,int l1,int r1,int k)//这是与之前不同的地方 { if ((l[u]>r1)||(r[u]<l1)) return; if ((l[u]>=l1)&&(r[u]<=r1)) { z[u]+=k*(r[u]-l[u]+1);//修改z[u] c[u]+=k;//c[u]懒标记 return; } xiafang(u);//下传标记 jia(u*2,l1,r1,k); jia(u*2+1,l1,r1,k); z[u]=z[u*2]+z[u*2+1]; } int qui(int u,int x) { if ((x>r[u])||(x<l[u])) return 0; else if (l[u]==r[u]) return z[u]; else { xiafang(u);//下传标记 return (qui(u*2,x)+qui(u*2+1,x)); } } int main() { scanf("%d%d",&n,&m); for (i=1; i<=n; i++) scanf("%d",&x1[i]); build(1,1,n); for (i=1; i<=m; i++) { scanf("%d%d",&q,&x); if (q==1) { scanf("%d%d",&y,&k); jia(1,x,y,k); } else printf("%d\n",qui(1,x)); } return 0; }
3.综合运用
希望读者能靠自己将这两道题A掉
这是我的AC代码:
P3372 【模板】线段树 1
#include <bits/stdc++.h> using namespace std; long long n,m,i,q,x,y,a,j,l[5000001],r[5000001],z[5000001],t[500001],w[5000001],c[5000001],k,x1[100001]; void xiafang(long long u) { z[u*2]+=c[u]*(r[u*2]-l[u*2]+1); c[u*2]+=c[u]; z[u*2+1]+=c[u]*(r[u*2+1]-l[u*2+1]+1); c[u*2+1]+=c[u]; c[u]=0; } void build(long long u,long long l1,long long r1) { l[u]=l1; r[u]=r1; if (l1==r1) { z[u]=x1[l1]; return; } build(u*2,l1,(l1+r1)/2); build(u*2+1,(l1+r1)/2+1,r1); z[u]=z[u*2]+z[u*2+1]; } void jia(long long u,long long l1,long long r1,long long k) { if ((l[u]>r1)||(r[u]<l1)) return; if ((l[u]>=l1)&&(r[u]<=r1)) { z[u]+=k*(r[u]-l[u]+1); c[u]+=k; return; } xiafang(u); jia(u*2,l1,r1,k); jia(u*2+1,l1,r1,k); z[u]=z[u*2]+z[u*2+1]; } long long qui(long long u,long long l1,long long r1) { if ((l1>r[u])||(r1<l[u])) return 0; else if ((l1<=l[u])&&(r1>=r[u])) return z[u]; else { if (c[u]>0) xiafang(u); return (qui(u*2,l1,r1)+qui(u*2+1,l1,r1)); } } int main() { scanf("%lld%lld",&n,&m); for (i=1; i<=n; i++) scanf("%lld",&x1[i]); build(1,1,n); for (i=1; i<=m; i++) { scanf("%lld%lld%lld",&q,&x,&y); if (q==1) { scanf("%lld",&k); jia(1,x,y,k); } else printf("%lld\n",qui(1,x,y)); } return 0; }
P3373 【模板】线段树 2
#include <bits/stdc++.h> using namespace std; unsigned long long z[5000001],l[5000001],r[5000001],c[5000001],s[5000001],n,m,p,i,t[500001],x,y,k,q; void xiafang(unsigned long long u) { z[u*2]=(z[u*2]*s[u]+c[u]*(r[u*2]-l[u*2]+1))%p; c[u*2]=(c[u*2]*s[u]+c[u])%p; s[u*2]=s[u*2]*s[u]%p; c[u*2+1]=(c[u*2+1]*s[u]+c[u])%p; s[u*2+1]=s[u*2+1]*s[u]%p; z[u*2+1]=(z[u*2+1]*s[u]+c[u]*(r[u*2+1]-l[u*2+1]+1))%p; c[u]=0; s[u]=1; } void build(unsigned long long u,unsigned long long l1,unsigned long long r1) { l[u]=l1; r[u]=r1; s[u]=1; if (l1==r1) { z[u]=t[l1]%p; return; } build(u*2,l1,(l1+r1)/2); build(u*2+1,(l1+r1)/2+1,r1); z[u]=(z[u*2]+z[u*2+1])%p; } void cheng(unsigned long long u,unsigned long long l1,unsigned long long r1,unsigned long long k) { if ((r[u]<l1)||(l[u]>r1)) return; else if ((r1>=r[u])&&(l[u]>=l1)) { z[u]=z[u]*k%p; c[u]=c[u]*k%p; s[u]=s[u]*k%p; return; } else { xiafang(u); cheng(u*2,l1,r1,k); cheng(u*2+1,l1,r1,k); z[u]=(z[u*2]+z[u*2+1])%p; } } void jia(unsigned long long u,unsigned long long l1,unsigned long long r1,unsigned long long k) { if ((r[u]<l1)||(l[u]>r1)) return; else if ((r1>=r[u])&&(l[u]>=l1)) { z[u]=(z[u]+k*(r[u]-l[u]+1))%p; c[u]=(c[u]+k)%p; return; } else { xiafang(u); jia(u*2,l1,r1,k); jia(u*2+1,l1,r1,k); z[u]=(z[u*2]+z[u*2+1])%p; } } unsigned long long qui(unsigned long long u,unsigned long long l1,unsigned long long r1) { if ((l[u]>r1)||(r[u]<l1)) return 0; else if ((l[u]>=l1)&&(r[u]<=r1)) return z[u]; else { xiafang(u); return (qui(u*2,l1,r1)+qui(u*2+1,l1,r1))%p; } } int main() { scanf("%lld%lld%lld",&n,&m,&p); for (i=1; i<=n; i++) scanf("%lld",&t[i]); build(1,1,n); for (i=1; i<=m; i++) { scanf("%lld",&q); if (q==1) { scanf("%lld%lld%lld",&x,&y,&k); cheng(1,x,y,k); } else if (q==2) { scanf("%lld%lld%lld",&x,&y,&k); jia(1,x,y,k); } else { scanf("%lld%lld",&x,&y); printf("%lld\n",qui(1,x,y)%p); } } return 0; }
如果读者是T掉或有些WA掉应该是没用高效读入、输出和没开long long吧
可以在我的其他博文中看一下可持久化数组(可持久化线段树/平衡树)的解析