线段树入门(线段懵逼树)
线段树上懵逼果,线段树下我和我,线段树上找bug,掉发多又多-----------题记
辣鸡张当时学习的博客(如果学到了新的东西或者说有新的理解后期再更新)
xy 写得这篇也挺好的,商业
互吹链接???
线段树常用来处理区间和、区间最大值、区间最小值问题,但是不仅局限于处理区间问题。
什摸士线段树:线段树是一棵二叉搜索树。每个结点存储的是一段区间的最大值、最小值或者区间内元素的和。每个结点最多有两个孩子(可以有一个或者两个甚至是个没有孩子的单身大汉)。如果父亲结点的标号是
,辣么它的左孩子的标号解就是
,右孩子的标号就是
。以下面的线段懵逼树为例:1号结点就存储的是【1,5】区间的和。
问题引入:点此打开自闭页给你一个
序列
再来
次骚操作。
每次操作有两种:“C a b c” means adding c to each of
“Q a b” means querying the sum of
So, you need to answer each ‘Q’.
Sample Input
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
Sample Output
4
55
9
15
懵逼少年找了许久bug,终于解决了这个题
模板代码:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<queue>
#include<vector>
#include<iostream>
#include<queue>
#include<string>
using namespace std;
typedef long long LL;
const int inf=0x3f3f3f3f;
const int maxn = 1e5 + 10;
LL num[maxn];
LL n,q;
struct node{LL sum,lazy;}tree[maxn*6];
void update(LL root){
tree[root].sum=tree[root * 2].sum+tree[root * 2 + 1].sum;
return ;
}
void build(LL root,LL l,LL r){///建树
tree[root].lazy = 0;///初始化
tree[root].sum = 0;///初始化
if(l == r){///如果左端点=又端点就说明走到了叶子节点,此时的sum肯定是叶子节点的值
tree[root].sum = num[l];
return ;
}
LL m = (l + r) / 2;
build(root*2,l,m);
build(root*2+1,m+1,r);
update(root);
}
void pushdown(LL root,LL l,LL r){
if(tree[root].lazy == 0)return ;
LL m= (l + r) / 2;
LL left = root * 2;
LL right = root * 2 + 1;
tree[left].lazy += tree[root].lazy;
tree[right].lazy += tree[root].lazy;
tree[left].sum += tree[root].lazy * (m-l+1);
tree[right].sum += tree[root].lazy * (r-m);
tree[root].lazy = 0;
}
void change(LL root,LL l,LL r,LL ql,LL qr,LL val){
if(l >= ql && r <= qr){
tree[root].lazy += val;
tree[root].sum += (r-l+1) * val;
return ;
}
pushdown(root,l,r);
LL m = (l + r) / 2;
if(ql <= m)change(root*2,l,m,ql,qr,val);
if(qr > m)change(root*2+1,m+1,r,ql,qr,val);
update(root);
}
LL query(LL root,LL l,LL r,LL ql,LL qr){
if(l >= ql && r <= qr){
return tree[root].sum;
}
pushdown(root,l,r);
LL m = (l + r) / 2;
LL sum = 0;
if(ql<=m)sum += query(root*2,l,m,ql,qr);
if(qr>m)sum += query(root*2+1,m+1,r,ql,qr);
return sum;
}
void input(){
for(int i = 1;i <= n;i++)
scanf("%lld",&num[i]);
}
int main()
{
while(scanf("%lld%lld",&n,&q)!=EOF){
input();
build(1,1,n);
while(q--){
char str[10]={'\0'};
scanf("%s",str);
if(strcmp(str,"C") == 0){
LL a,b,c;
scanf("%lld%lld%lld",&a,&b,&c);
change(1,1,n,a,b,c);
}
if(strcmp(str,"Q") == 0){
LL a,b;
scanf("%lld%lld",&a,&b);
LL ans = query(1,1,n,a,b);
printf("%lld\n",ans);
}
}
}
return 0;
}
解释一下 、 函数:
void update(LL root){
tree[root].sum=tree[root * 2].sum+tree[root * 2 + 1].sum;
return ;
}
void build(LL root,LL l,LL r){///建树
tree[root].lazy = 0;///初始化
tree[root].sum = 0;///初始化
if(l == r){///如果左端点=又端点就说明走到了叶子节点,此时的sum肯定是叶子节点的值
tree[root].sum = num[l];
return ;
}
LL m = (l + r) / 2;
build(root*2,l,m);
build(root*2+1,m+1,r);
update(root);
}
这是一个递归建树的过程,下面就来模拟一下回溯的过程,现在就到了递归的出口,就是左端点=右端点,此时就走到了叶子节点,此时的sum肯定是叶子节点的值,黄色代表已经更新过值的结点,蓝色代表更新完叶子节点后回溯到上一层的结点:
然后就要通过
函数更新他们的爸爸4号节点:
更新完爸爸结点后又要回溯:
此时又要去更新2号节点的右儿子5号结点:
5号结点更新完后又回溯到了2号结点
然后就要通过
函数更新2号结点:
之后的过程就是这样,直至整棵树构造好。
解释一下 和 函数:
void pushdown(LL root,LL l,LL r){
if(tree[root].lazy == 0)return ;
LL m= (l + r) / 2;
LL left = root * 2;
LL right = root * 2 + 1;
tree[left].lazy += tree[root].lazy;
tree[right].lazy += tree[root].lazy;
tree[left].sum += tree[root].lazy * (m-l+1);
tree[right].sum += tree[root].lazy * (r-m);
tree[root].lazy = 0;
}
void change(LL root,LL l,LL r,LL ql,LL qr,LL val){
if(l >= ql && r <= qr){
tree[root].lazy += val;
tree[root].sum += (r-l+1) * val;
return ;
}
pushdown(root,l,r);
LL m = (l + r) / 2;
if(ql <= m)change(root*2,l,m,ql,qr,val);
if(qr > m)change(root*2+1,m+1,r,ql,qr,val);
update(root);
}
如果要给一段连续的区间加上某个值 (发零花钱哈哈哈),当然可以每次都找到叶子节点然后将它们更新就可以了,但是这样子太费时间了(这样就叫做单点更新,但是我还是喜欢单点更新,因为代码少)。不过有更高级的做法噻:如果某个结点所管辖的区间在你要更新的区间内(也就是说你要更新的区间包含某个结点所管辖的区间)那就直接让这个结点的sum值乘以区间的长度再乘以 就行了撒,那就出大问题了啊,它就十分的贪污了啊,那他的孩子结点呢,就不管了吗。为了解决这个问题,我们引入了lazy标记,就是结构体里的lazy。偷了懒之后,就把lazy的值加上它贪污的值(为甚麽要“加”,直接赋值不就好了吗,不不不,因为它有可能多次贪污哈哈)。
在加上lazy之后,为甚麽有个 函数呢,这个函数的作用就是将爸爸的贪污值传递给它的儿子,让他的儿子的sum加上贪污值,并且给它的儿子也标记上lazy值,然后再把自己的lazy值删去(不仅贪污,还让儿子背锅)。你可能会问这不就多此一举吗,不不不。如果题目有多次给一段区间加上某个值,假如有一次给2号结点标记了lazy值,然后下一次又要给4号结点标记lazy值,如果没有 函数,是不是2号结点的孩子4号就少了钱,那他的爸爸就真的贪污了。如果有 函数,并且 函数还是加在递归走左右两个孩子的语句之前,那么在给4号结点标记lazy值之前就把它的爸爸2号的lazy值传递到4号了,这样4号的钱就不会少了。这个 函数就是预防贪污。
注意递归走完左右两个孩子之后还要 一下。
解释一下 函数:
LL query(LL root,LL l,LL r,LL ql,LL qr){
if(l >= ql && r <= qr){
return tree[root].sum;
}
pushdown(root,l,r);
LL m = (l + r) / 2;
LL sum = 0;
if(ql<=m)sum += query(root*2,l,m,ql,qr);
if(qr>m)sum += query(root*2+1,m+1,r,ql,qr);
return sum;
}
函数函数里的 函数跟上一个是一样的功能,就不多说了。递归出口就是如果某个结点所管辖的区间在你要更新的区间内(也就是说你要更新的区间包含某个结点所管辖的区间),就直接返回sum值就行了。
再贴一个求区间最大值的代码:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<queue>
#include<vector>
#include<iostream>
#include<queue>
#include<string>
using namespace std;
typedef long long LL;
const int inf=0x3f3f3f3f;
const int maxn = 2e5 + 10;
LL num[maxn];
LL n,q;
struct node{LL val;}tree[maxn*4];
void update(LL root){
tree[root].val=max(tree[root*2].val,tree[root*2+1].val);
return ;
}
void build(LL root,LL l,LL r){
tree[root].val=-inf;
if(l==r){
tree[root].val=num[l];
return ;
}
LL m = ( l + r ) / 2;
build(root*2,l,m);
build(root*2+1,m+1,r);
update(root);
}
void change(LL root,LL l,LL r,LL pos,LL val){
if(l == r){
tree[root].val = val;
return ;
}
int m = ( l + r ) / 2;
if(pos <= m)change(root*2,l,m,pos,val);
if(pos > m)change(root*2+1,m+1,r,pos,val);
update(root);
}
int query(LL root,LL l,LL r,LL ql,LL qr){
if(l>=ql&&r<=qr){
return tree[root].val;
}
int m = (l + r) / 2;
int ans = -inf;
if(ql <= m)ans = max(ans,query(root*2,l,m,ql,qr));
if(qr > m)ans = max(ans,query(root*2+1,m+1,r,ql,qr));
return ans;
}
void input(){
for(int i=1;i<=n;i++){
scanf("%lld",&num[i]);
}
}
int main()
{
while(scanf("%lld%lld",&n,&q)!=EOF){
input();
build(1,1,n);
while(q--){
char str[10] = {'\0'};
scanf("%s",str);
if(strcmp(str,"Q") == 0){
LL a,b;
scanf("%lld%lld",&a,&b);
LL ans=query(1,1,n,a,b);
printf("%lld\n",ans);
}
if(strcmp(str,"U") == 0){
LL a,b;
scanf("%lld%lld",&a,&b);
change(1,1,n,a,b);
}
}
}
return 0;
}