好几天没写博客了,然后做线段bug树做不动,回来写个博客冷静一下hhhhh——萌新的成长之路
主要说线段树的创建,单点、区间的修改、查询,lazy标记和在修改和查询时候的下传标记
一、线段树是什么?
- 百科说:线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。区间操作的时间复杂度是O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。为什么线段树要开4倍空间
- 功能:单点、区间的修改、查询
- 时间复杂度:更新—O(logn);查询—O(logn);建树—o(nlogn)
- 线段树和其他RMQ算法的区别
常用的解决RMQ问题有ST算法,二者预处理时间都是O(NlogN),而且ST算法的单次查询操作是O(1),看起来比线段树好多了,但二者的区别在于线段树支持在线更新值,而ST算法不支持在线操作。
这里也存在一个误区,刚学线段树的时候就以为线段树和树状数组差不多,用来处理RMQ问题和求和问题,但其实线段树的功能远远不止这些,我们要熟练的理解线段这个概念才能更加深层次的理解线段树。
二、线段树的基本操作
下
面
的
内
容
都
是
以
维
护
区
间
和
为
例
介
绍
它
的
一
些
最
基
本
的
操
作
\color{red}{下面的内容都是以维护区间和为例介绍它的一些最基本的操作}
下面的内容都是以维护区间和为例介绍它的一些最基本的操作
例题:poj 3468: A Simple Problem with Integers
题目大意:给出n个数,和m个操作,每个操作修改一段连续区间[a,b]的值或询问区间[l,r]的和,并输出
先看它是怎么划分区间的:(线段树是建立在线段的基础上,每个结点都代表了一条线段[a,b]。它的两个子结点中:左结点代表的线段为[a,(a + b) / 2],右结点代表的线段为[((a + b) / 2)+1,b])
在维护区间和的问题中,原区间[1,n]的值都是存在叶子节点的
1、线段树的创建
如上图,线段树中的每个结点都表示一段区间的和,先给子节点赋值,在回溯的过程中得到各区间段的和
若一个结点的下标为root, 则他的左儿子下标为 root << 1, 右儿子下标为( rt << 1) | 1.
LL num[manx];//存的是原来区间[1,n]的值,必须从1开始存
struct node
{
LL sum,lazy;
}tree[manx<<2];
void eval(int root)
{
tree[root].sum=tree[root<<1].sum+tree[(root<<1)|1].sum;//可以看图,比如[1,3]的和就是它的左右儿子[1,2]和[3,3]的和
}
void build_tree(int root,int l,int r)
{
tree[root].lazy=0;//初始化,如果是乘法就初始化为1,这个是懒惰标记的操作
if(l==r)//遍历到叶子结点,赋值,然后返回
{
tree[root].sum=num[l];
return;
}
int mid=(l+r)>>1;
build_tree(root<<1,l,mid);
build_tree((root<<1)|1,mid+1,r);
eval(root);//得到左右儿子的值之后,合并得到根节点的值
}
build_tree(1,1,n);
2、单点的修改
看一组数据:
6 3
5 2 8 4 5 6
C 1 3 2
C 1 2 1
Q 1 3
构造的线段树如下:
如果要修改第二个数的值,那么往上看,影响的结点——就是遍历到点[2,2]这条路径上的点,如图:
void change(int root,int l,int r,int pos,LL val)
{
if(l==r)//便利到要改的这个结点[pos,pos]
{
tree[root].sum=val;
return;
}
int mid=(l+r)>>1;
if(pos<=mid)//二分查找
change(root<<1,l,mid,pos,val);
else
change((root<<1)|1,mid+1,r,pos,val);
eval(root);
}
change(1,1,n,pos,val);
单点的查询和区间查询一样不过区间查询多了一个操作
3、区间修改
商业互吹时刻(「・ω・)「嘿:
如果要给一段连续的区间加上某个值
c
c
c(发零花钱哈哈哈),当然可以每次都找到叶子节点然后将它们更新就可以了,但是这样子太费时间了(这样就叫做单点更新,但是我还是喜欢单点更新,因为代码少)。不过有更高级的做法噻:如果某个结点所管辖的区间在你要更新的区间内(也就是说你要更新的区间包含某个结点所管辖的区间)那就直接让这个结点的sum值乘以区间的长度再乘以
c
c
c就行了撒,那就出大问题了啊,它就十分的贪污了啊,那他的孩子结点呢,就不管了吗。为了解决这个问题,我们引入了lazy标记,就是结构体里的lazy。偷了懒之后,就把lazy的值加上它贪污的值(为甚麽要“加”,直接赋值不就好了吗,不不不,因为它有可能多次贪污哈哈)。
在加上lazy之后,为甚麽有个
p
u
s
h
d
o
w
n
(
)
pushdown()
pushdown()函数呢,这个函数的作用就是将爸爸的贪污值传递给它的儿子,让他的儿子的sum加上贪污值,并且给它的儿子也标记上lazy值,然后再把自己的lazy值删去(不仅贪污,还让儿子背锅)。线段树入门(线段懵逼树
回到这组数据:
6 3
5 2 8 4 5 6
C 1 3 2
C 1 2 1
Q 1 3
第一次操作之后:
继续看为什么还要用pushdown()函数将标记下移,不是已经将区间和改变了吗?
1、对查询的影响:如果现在要找[1,2]的区间和,因为前面加了2,所以他们的和应该是11,虽然我们修改了代表[1,3]区间和的结点值,但下面没有改变,所以图中还是7
C 1 2 1
2、对下一次操作的影响:因为在修改之后都要进行回溯操作,下图是修改返回的时候还没有回溯修改上面的值,也没有pushdown()操作
可以看到,如果从这里回溯的话,[1,3]的区间和就变成了17,但是前面经过两次修改,[1,3]的区间和应该是23
因为除叶子结点的区间和都是我们在回溯的过程中,通过两个孩子结点的值和并得来的,所以要知道父亲贪污了多少钱,还是要先让儿子知道之前和这次总共贪污了多少钱,如果没有用pushdown()告诉它的儿子自己之前贪污的钱,那儿子也不知道,下一次贪污的时候父亲也忘了,就少算了
所以正确的操作结果应该是这样子得:(先告诉他的儿子,再进行下一次操作)
void pushdown(int root,LL l,LL r)
{
if(tree[root].lazy==0)
return;
LL mid=(l+r)>>1;
int chl=root<<1;
int chr=(root<<1)+1;
tree[chl].sum+=(mid-l+1)*tree[root].lazy;
tree[chr].sum+=(r-mid)*tree[root].lazy;
tree[chl].lazy+=tree[root].lazy;
tree[chr].lazy+=tree[root].lazy;
tree[root].lazy=0;///记得置0
}
void changelong(int root,LL l,LL r,int ll,int rr,LL val)
{
if(l==ll&&rr==r)//找到区间
{
tree[root].sum+=val*(r-l+1);
tree[root].lazy+=val;
return;
}
pushdown(root,l,r);///(主角)
int mid=(l+r)>>1;
if(mid>=rr)//二分,如果整个区间都在左边的话,就不用遍历右边区间了
changelong(root<<1,l,mid,ll,rr,val);
else if(mid<ll)
changelong((root<<1)|1,mid+1,r,ll,rr,val);
else
{//两边都有的话就分开改,比如说要改[1,5]的
changelong(root<<1,l,mid,ll,mid,val);
changelong((root<<1)|1,mid+1,r,mid+1,rr,val);
}
eval(root);//合并
}
4、询问
直接找就行了,记得加pushdown(),防止询问儿子结点或儿子的儿子的时候,父亲还没有告诉它,就出错了
LL query(int root,int l,int r,int ll,int rr)
{
if(l==ll&&r==rr)
{
return tree[root].sum;
}
pushdown(root,l,r);//单点查询可以不用加pushdown()
int mid=(l+r)>>1;
if(mid>=rr)
return query(root<<1,l,mid,ll,rr);
else if(mid<ll)
return query((root<<1)|1,mid+1,r,ll,rr);
else
return query(root<<1,l,mid,ll,mid)+query((root<<1)|1,mid+1,r,mid+1,rr);
}
整体实现:
#include <iostream>
#include <string.h>
#include<stdio.h>
#include<math.h>
#include<algorithm>
#include<vector>
#include<queue>
typedef long long LL;
using namespace std;
const int manx=1e5+10;
const int INF=0x3f3f3f3f;
LL num[manx];
struct node
{
LL sum,lazy;
}tree[manx<<2];
void eval(int root)
{
tree[root].sum=tree[root<<1].sum+tree[(root<<1)|1].sum;
}
void build_tree(int root,int l,int r)
{
tree[root].lazy=0;
tree[root].sum=0;
if(l==r)
{
tree[root].sum=num[l];
return;
}
int mid=(l+r)>>1;
build_tree(root<<1,l,mid);
build_tree((root<<1)|1,mid+1,r);
eval(root);
}
void pushdown(int root,LL l,LL r)
{
if(tree[root].lazy==0)
return;
LL mid=(l+r)>>1;
int chl=root<<1;
int chr=(root<<1)+1;
tree[chl].sum+=(mid-l+1)*tree[root].lazy;
tree[chr].sum+=(r-mid)*tree[root].lazy;
tree[chl].lazy+=tree[root].lazy;
tree[chr].lazy+=tree[root].lazy;
tree[root].lazy=0;
}
void changelong(int root,LL l,LL r,int ll,int rr,LL val)
{
if(l==ll&&rr==r)
{
tree[root].sum+=val*(r-l+1);
tree[root].lazy+=val;
return;
}
pushdown(root,l,r);
int mid=(l+r)>>1;
if(mid>=rr)
changelong(root<<1,l,mid,ll,rr,val);
else if(mid<ll)
changelong((root<<1)|1,mid+1,r,ll,rr,val);
else
{
changelong(root<<1,l,mid,ll,mid,val);
changelong((root<<1)|1,mid+1,r,mid+1,rr,val);
}
eval(root);
}
LL query(int root,int l,int r,int ll,int rr)
{
if(l==ll&&r==rr)
{
return tree[root].sum;
}
pushdown(root,l,r);
int mid=(l+r)>>1;
if(mid>=rr)
return query(root<<1,l,mid,ll,rr);
else if(mid<ll)
return query((root<<1)|1,mid+1,r,ll,rr);
else
return query(root<<1,l,mid,ll,mid)+query((root<<1)|1,mid+1,r,mid+1,rr);
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
scanf("%lld",&num[i]);
build_tree(1,1,n);
char opert[30];
while(m--)
{
scanf("%s",opert);
if(opert[0]=='C')
{
int l,r;
LL val;
scanf("%d%d%lld",&l,&r,&val);
changelong(1,1,n,l,r,val);
}
else
{
int l,r;
scanf("%d%d",&l,&r);
printf("%lld\n",query(1,1,n,l,r));
}
}
}
例题:
维护区间最大值:http://acm.hdu.edu.cn/showproblem.php?pid=1754
维护区间和(加+乘):https://www.luogu.org/problem/P2023
后面可以不用看了
。
。
最开始一直在想既然lazy记录了所有修改过的情况,那就可以在询问的时候下传个标记就行了啊,为啥还要在修改里面加一个,然后,然后我就把主角删了,,T_T,开始修改里面也没有写合并,一直错,还不知道为啥T_T,,,,,
但是改过来之后还是把主角去了,又写了一个,不过合并哪里改了一下
#include <iostream>
#include <string.h>
#include<stdio.h>
#include<math.h>
#include<algorithm>
#include<vector>
#include<queue>
typedef long long LL;
using namespace std;
const int manx=1e5+10;
const int INF=0x3f3f3f3f;
LL num[manx];
struct node
{
LL sum,lazy;
}tree[manx<<3];
void eval(int root,int l,int r)
{//既然没告诉儿子,就自己加上去hhh,还是要自己记
tree[root].sum=tree[root<<1].sum+tree[(root<<1)|1].sum+tree[root].lazy*(r-l+1);
}
void build_tree(int root,int l,int r)
{
tree[root].lazy=0;
tree[root].sum=0;
if(l==r)
{
tree[root].sum=num[l];
return;
}
int mid=(l+r)>>1;
build_tree(root<<1,l,mid);
build_tree((root<<1)|1,mid+1,r);
tree[root].sum=tree[root<<1].sum+tree[(root<<1)|1].sum;
}
void pushdown(int root,int l,int r)
{
if(tree[root].lazy==0)
return;
int mid=(l+r)>>1;
int chl=root<<1;
int chr=(root<<1)+1;
tree[chl].sum+=(mid-l+1)*tree[root].lazy;
tree[chr].sum+=(r-mid)*tree[root].lazy;
tree[chl].lazy+=tree[root].lazy;
tree[chr].lazy+=tree[root].lazy;
tree[root].lazy=0;
}
void changelong(int root,int l,int r,int ll,int rr,LL val)
{
if(l==ll&&rr==r)
{
tree[root].sum+=val*(r-l+1);
tree[root].lazy+=val;
return;
}
//pushdown(root,l,r);
int mid=(l+r)>>1;
if(mid>=rr)
changelong(root<<1,l,mid,ll,rr,val);
else if(mid<ll)
changelong((root<<1)|1,mid+1,r,ll,rr,val);
else
{
changelong(root<<1,l,mid,ll,mid,val);
changelong((root<<1)|1,mid+1,r,mid+1,rr,val);
}
eval(root,l,r);
}
LL query(int root,int l,int r,int ll,int rr)
{
if(l==ll&&r==rr)
{
return tree[root].sum;
}
pushdown(root,l,r);
int mid=(l+r)>>1;
if(mid>=rr)
return query(root<<1,l,mid,ll,rr);
else if(mid<ll)
return query((root<<1)|1,mid+1,r,ll,rr);
else
return query(root<<1,l,mid,ll,mid)+query((root<<1)|1,mid+1,r,mid+1,rr);
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
scanf("%lld",&num[i]);
build_tree(1,1,n);
char opert[30];
while(m--)
{
scanf("%s",opert);
if(opert[0]=='C')
{
int l,r;
LL val;
scanf("%d%d%lld",&l,&r,&val);
changelong(1,1,n,l,r,val);
}
else
{
int l,r;
scanf("%d%d",&l,&r);
printf("%lld\n",query(1,1,n,l,r));
}
}
}