@gzr666
2019-07-24T12:03:01.000000Z
字数 6596
阅读 826
线段树是一种高级数据结构,必须先学会树([戳这里了解][1])和分治([戳这里了解][2])。
想必分治学的还不错的话就一定会知道二分查找这种东西。
那么二分是什么样的?在一个数列中不断的折半查找,显然,这是一维的。
如果改成了二维,又会是怎样的?这便是线段树。
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
其实线段树就是把二分查找的过程记录下来的树。
每一个节点存储的都是一段区间,和二叉搜索树一样,都是用来查找的,只不过能维护的东西更多,如图所示,线段树就是这样的:
- 单点加
- 区间加
- 区间乘
- 区间修改
- 区间询问
- ……
不论实现什么操作,我们肯定需要先建起来这棵树。
但是指针、邻接表看起来有点令人头疼,那么我们就采用数组模拟树。
还记得吗?这种树看起来与众不同,首先它是一棵二叉树,其次,它又是一棵完全二叉树。
那么完全二叉树有什么性质?假设当前节点编号为i,那么左孩子编号一定为2*i,右孩子编号一定为2*i+1。
那么我们就递归建树就好了。
代码如下:
void build(int o,int l,int r)
{
if(l==r) {sumv[o]=a[l];return;}
int mid=(l+r)>>1;
build(o<<1,l,mid);
build(o<<1|1,mid+1,r);
pushup(o);
}
那么pushup操作是什么呢?就是合并下面的两个区间,记录这两个区间的和。
pushup
的代码很简单:void pushup(int o) {sumv[o]=sumv[o<<1]+sumv[o<<1|1];}
如果只是用于询问区间和的话,那么使用ST表足矣。不同的是,线段树支持修改数字。
那么我们如何修改一个数字的值呢?当然是先找到这个数字,加上相应的值,上面父节点就只要沿路pushup
就可以修改了。
代码如下:
void change(int o,int l,int r,int k,int v)
{
if(l==r) {sumv[o]+=v;return;}
int mid=(l+r)>>1;
if(k<=mid) change(o<<1,l,mid,k,v);
else change(o<<1|1,mid+1,r,k,v);
pushup(o);
}
有时题目会相当变态,让你实现区间加,那么我们该怎样解决?难道是暴力单点加?
显然不行,我们应该先找到这个区间(也可能是多个区间组成),然后修改了区间,但是它的子区间还没有修改啊,所以我们就需要下放,同时打好标记,这样就会准确的知道哪些子区间需要修改。
那么下放我们又定义了一个新的函数pushdown
,代码是这样的:
void pushdown(int o,int l,int r)
{
if(add[o]==0) return;
add[o<<1]+=add[o];
add[o<<1|1]+=add[o];
int mid=(l+r)>>1;
sumv[o<<1]+=add[o]*(mid-l+1);
sumv[o<<1|1]=add[o]*(r-mid);
add[o]=0;
}
其中add是标记,sumv是树存的和,注意:一定要下方后清除该标记。
还有一个做标记的函数puttag
:void puttag(int o,int l,int r,int v) {add[o]+=v;sumv[o]+=(r-l+1)*v;}
整个操作的代码是这样的:
void optadd(int o,int l,int r,int ql,int qr,int v)
{
if(ql<=l&&r<=qr) {puttag(o,l,r,v);return;}
int mid=(l+r)>>1;
pushdown(o,l,r);
if(ql<=mid) optadd(o<<1,l,mid,ql,qr,v);
if(qr>mid) optadd(o<<1|1,mid+1,r,ql,qr,v);
pushup(o);
}
询问呢区间就一定要先找到区间,当然询问区间也有可能是多个树的区间构成的,所以首先来思考询问区间和当前区间可能是什么样的。
显然,要么是整个当前区间在询问区间中,要么只有一部分是。图解一下:
当整个区间都被包含时,我们直接返回和就可以了。
只有一部分时就要继续分类讨论:当ql<=mid
时,一部分肯定在左区间;当qr>mid
时,那么右区间肯定有一部分。
所以整个代码就很清晰了:
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
build(1,1,n);
for(int i=1;i<=m;i++)
{
int pos,x,y,z;
cin>>pos>>x>>y;
if(pos==1) change(1,1,n,x,y);
else if(pos==2)
{
cin>>z;
optadd(1,1,n,x,y,z);
}
else cout<<query(1,1,n,x,y);
}
return 0;
}
亲测能用:
代码如下:
#include<iostream>
using namespace std;
int sumv[10000],add[10000],n,m,a[10000];
void pushup(int o) {sumv[o]=sumv[o<<1]+sumv[o<<1|1];}
void puttag(int o,int l,int r,int v) {add[o]+=v;sumv[o]+=(r-l+1)*v;}
void pushdown(int o,int l,int r)
{
if(add[o]==0) return;
add[o<<1]+=add[o];
add[o<<1|1]+=add[o];
int mid=(l+r)>>1;
sumv[o<<1]+=add[o]*(mid-l+1);
sumv[o<<1|1]=add[o]*(r-mid);
add[o]=0;
}
void build(int o,int l,int r)
{
if(l==r) {sumv[o]=a[l];return;}
int mid=(l+r)>>1;
build(o<<1,l,mid);
build(o<<1|1,mid+1,r);
pushup(o);
}
void change(int o,int l,int r,int k,int v)
{
if(l==r) {sumv[o]+=v;return;}
int mid=(l+r)>>1;
if(k<=mid) change(o<<1,l,mid,k,v);
else change(o<<1|1,mid+1,r,k,v);
pushup(o);
}
void optadd(int o,int l,int r,int ql,int qr,int v)
{
if(ql<=l&&r<=qr) {puttag(o,l,r,v);return;}
int mid=(l+r)>>1;
pushdown(o,l,r);
if(ql<=mid) optadd(o<<1,l,mid,ql,qr,v);
if(qr>mid) optadd(o<<1|1,mid+1,r,ql,qr,v);
pushup(o);
}
int query(int o,int l,int r,int ql,int qr)
{
if(ql<=l&&r<=qr) return sumv[o];
int ans=0,mid=(l+r)>>1;
pushdown(o,l,r);
if(ql<=mid) ans+=query(o<<1,l,mid,ql,qr);
if(qr>mid) ans+=query(o<<1|1,mid+1,r,ql,qr);
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
build(1,1,n);
for(int i=1;i<=m;i++)
{
int pos,x,y,z;
cin>>pos>>x>>y;
if(pos==1) change(1,1,n,x,y);
else if(pos==2)
{
cin>>z;
optadd(1,1,n,x,y,z);
}
else cout<<query(1,1,n,x,y);
}
return 0;
}
这里面用到了很多位运算,如果你是小白的话一定要尽快学位运算(快的可不止一星半点),那么发一个不带位运算的代码:
#include<iostream>
using namespace std;
int sumv[10000],add[10000],n,m,a[10000];
void pushup(int o) {sumv[o]=sumv[o*2]+sumv[o*2+1];}
void puttag(int o,int l,int r,int v) {add[o]+=v;sumv[o]+=(r-l+1)*v;}
void pushdown(int o,int l,int r)
{
if(add[o]==0) return;
add[o*2]+=add[o];
add[o*2+1]+=add[o];
int mid=(l+r)>>1;
sumv[o*1]+=add[o]*(mid-l+1);
sumv[o*1+1]=add[o]*(r-mid);
add[o]=0;
}
void build(int o,int l,int r)
{
if(l==r) {sumv[o]=a[l];return;}
int mid=(l+r)/2;
build(o*2,l,mid);
build(o*2+1,mid+1,r);
pushup(o);
}
void change(int o,int l,int r,int k,int v)
{
if(l==r) {sumv[o]+=v;return;}
int mid=(l+r)/2;
if(k<=mid) change(o*2,l,mid,k,v);
else change(o*2+1,mid+1,r,k,v);
pushup(o);
}
void optadd(int o,int l,int r,int ql,int qr,int v)
{
if(ql<=l&&r<=qr) {puttag(o,l,r,v);return;}
int mid=(l+r)/2;
pushdown(o,l,r);
if(ql<=mid) optadd(o*2,l,mid,ql,qr,v);
if(qr>mid) optadd(o*2+1,mid+1,r,ql,qr,v);
pushup(o);
}
int query(int o,int l,int r,int ql,int qr)
{
if(ql<=l&&r<=qr) return sumv[o];
int ans=0,mid=(l+r)/2;
pushdown(o,l,r);
if(ql<=mid) ans+=query(o*2,l,mid,ql,qr);
if(qr>mid) ans+=query(o*2+1,mid+1,r,ql,qr);
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
build(1,1,n);
for(int i=1;i<=m;i++)
{
int pos,x,y,z;
cin>>pos>>x>>y;
if(pos==1) change(1,1,n,x,y);
else if(pos==2)
{
cin>>z;
optadd(1,1,n,x,y,z);
}
else cout<<query(1,1,n,x,y);
}
return 0;
}
废话不多说,直接上题:
某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米。我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置;数轴上的每个整数点,即0,1,2,…,L,都种有一棵树。
由于马路上有一些区域要用来建地铁。这些区域用它们在数轴上的起始点和终止点表示。已知任一区域的起始点和终止点的坐标都是整数,区域之间可能有重合的部分。现在要把这些区域中的树(包括区域端点处的两棵树)移走。你的任务是计算将这些树都移走后,马路上还有多少棵树。
第一行有2个整数L(1≤L≤10000)和 M(1≤M≤100),L代表马路的长度,M代表区域的数目,L和M之间用一个空格隔开。
接下来的M行每行包含2个不同的整数,用一个空格隔开,表示一个区域的起始点和终止点的坐标。
1个整数,表示马路上剩余的树的数目。
输入样例#1:
500 3
150 300
100 200
470 471
输出样例#1:
298
NOIP2005普及组第二题
对于的数据,区域之间没有重合的部分;
对于其它的数据,区域之间有重合的情况。
这道题简直就是模板题,只要把区间加换成区间修改就可以了。
代码如下:
#include<iostream>
using namespace std;
int sumv[100000],add[100000],l,m;
void pushup(int o) {sumv[o]=sumv[o<<1]+sumv[o<<1|1];}
void puttag(int o,int l,int r) {add[o]=1;sumv[o]=0;}
void pushdown(int o,int l,int r)
{
if(add[o]==0) return;
add[o<<1]=1;
add[o<<1|1]=1;
sumv[o<<1]=0;
sumv[o<<1|1]=0;
add[o]=0;
}
void build(int o,int l,int r)
{
add[o]=0;
if(l==r) {sumv[o]=1;return;}
int mid=(l+r)>>1;
build(o<<1,l,mid);
build(o<<1|1,mid+1,r);
pushup(o);
}
void optadd(int o,int l,int r,int ql,int qr)
{
if(ql<=l&&r<=qr) {puttag(o,l,r);return;}
int mid=(l+r)>>1;
pushdown(o,l,r);
if(ql<=mid) optadd(o<<1,l,mid,ql,qr);
if(qr>mid) optadd(o<<1|1,mid+1,r,ql,qr);
pushup(o);
}
//int query(int o,int l,int r,int ql,int qr)
//{
// if(ql<=l&&r<=qr) return sumv[o];
// int mid=(l+r)>>1;
// pushdown(o,l,r);
// if(ql<=mid) int t1=optadd(sumv[o<<1],l,mid,ql,qr);
// if(r>qr) int t2=optadd(sumv[o<<1|1],mid+1,r,ql,qr);
// return t1+t2;
//}
int main()
{
cin>>l>>m;
build(1,1,l+1);
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
optadd(1,1,l+1,x+1,y+1);
}
cout<<sumv[1];
return 0;
}
偷偷的告诉你,这道题可以暴力做的:
#include<iostream>
using namespace std;
int main()
{
int l,m,vis[100000],ans=0;
cin>>l>>m;
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
for(int i=x;i<=y;i++)
vis[i]=1;
}
for(int i=0;i<=l;i++)
if(vis[i]==0) ans++;
cout<<ans;
return 0;
}