线段树 学习笔记

P3372 【模板】线段树 1

这是很久以前打过一遍的求区间和的模板,但也就是照着题解敲一遍,半懂不懂想着之后再说,最近愈发感到树状数据结构的重要性,线段树得好好学了,所以拿粗来好好看,好好学。
在看代码之前先上两篇dalao的线段树博客
线段树从零开始
线段树详解
两篇讲的都很详细也很易懂这线段树到底是个啥玩意,其实链接一放好像就没我笔记的事了。
不过想了想还是得自己来谈谈,这个懒标记,我想这会是线段树唯一难理解的一项,当然也是线段树的精髓所在。

我一开始想,每次更新的时候顺便把所有的子节点全部更新不就OK了吗?但是后来再想想,这样做是很浪费的。
来,我们设一串长为6的序列,当我们想要知道sum[1…6]的时候,您只需要询问1号节点即可,无论是修改还是询问,其实很多子节点如果不需要,您是没有必要去修改它的,您可以用数组(tag数组,即延迟标记)把它们暂时存储起来,当您想要用到的时候,再一起更新子节点。
甚至对于多次修改时候更加巧妙,若您想两次修改区间同一个区间[1…2]时,即打了两次延迟标记,两次标记叠加和,最后如果有需要再一起向下pushdown,即更加节省时间,当前节点的子树只需要更新一次,如果每一次有更新就更新全部子树,那么肯定就丢失了线段树的时间优化。

#include<bits/stdc++.h>
#define maxn 1000005
#define FOR(a, b, c) for(int a=b; a<=c; a++)
#define hrdg 1000000007
#define inf 0x7fffffff
#define ll long long
#define pi acos(-1.0)
#define ls p<<1						//left son 左儿子(即ls=p*2)
#define rs p<<1|1					//right son 右儿子(即rs=p*2+1)
using namespace std;

ll n, m, a[maxn], tr[maxn<<2], tag[maxn<<2];				//树上数组开4倍
ll type, l, r, d;

//快读
inline ll read()
{
    char c=getchar();long long x=0,f=1;
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    return x*f;
}

void pushup(ll p)       //这个点的值是左儿子+右儿子的和
{
    tr[p] = tr[ls] + tr[rs];
}

void build(ll p, ll l, ll r)
{
    tag[p] = 0;
    if(l==r)        //左右相同,说明这是一个点(叶子节点)
    {
        tr[p] = a[l];      //给叶子节点赋值键入的a[l]
        return;
    }
    ll mid = (l+r)>>1;
    build(ls, l, mid);
    build(rs, mid+1, r);
    pushup(p);              //这是在所有叶子节点赋值后才能进行的向上溯根并更新节点值的过程
}

void load(ll p, ll l, ll r, ll k)			//装载lazytag
{
    tag[p] += k;
    tr[p] += k*(r-l+1);				//并且把当前节点算出来
}

void pushdown(ll p, ll l, ll r)
{
    ll mid = (l+r)>>1;
    load(ls, l, mid, tag[p]);
    load(rs, mid+1, r, tag[p]);
    tag[p] = 0;
}

//要求的左边界右边界,递归目前的左边界右边界,当前节点,更新的值
void update(ll nl, ll nr, ll l, ll r, ll p, ll k)
{
    if(nl <= l && nr >= r)          //覆盖了递归当前节点全区的情况
    {
        tr[p] += k*(r-l+1);			//那我就不往下push了,要用到再说,更新这个p点的值(p点覆盖了l到r)
        tag[p] += k;				//先给p点存一个tag,如果到时候需要调用到l到r间的部分区间,再将tag向下push加入tr[]
        //其实上面两行可以用load(p, l, r, k);来代替,反正就是储存懒标记
        return;
    }
    pushdown(p, l, r);
    ll mid = (l+r)>>1;
    if(nl <= mid)            			//把k值,从p节点向下递归找底层
        update(nl, nr, l, mid, ls, k);
    if(nr >= mid+1)
        update(nl, nr, mid+1, r, rs, k);
    pushup(p);
}

ll query(ll nl, ll nr, ll l, ll r, ll p)			//相比之下查询好理解很多
{
    ll ret = 0;
    if(nl <= l && nr>= r) return tr[p]; 		//如果所需的区间包涵了当前递归的区间,直接返回区间代表点位p的值
    ll mid = (l+r)>>1;
    pushdown(p, l, r);			//查询时往下传递懒标记,如果有储存,子树会更新,没有则会更新0
    if(nl <= mid)
        ret += query(nl, nr, l, mid, ls);
    if(nr >= mid+1)
        ret += query(nl, nr, mid+1, r, rs);
    return ret;
}

int main()
{
    n = read();
    m = read();
    FOR(i, 1, n)
        a[i] = read();
    build(1, 1, n);         //从1开始建树
    FOR(i, 1, m)
    {
        type = read();
        if(type == 1)
        {
            l=read(); r=read(); d=read();
            update(l, r, 1, n, 1, d);          //l与r间更新d
            continue;
        }
        if(type == 2)
        {
            l=read(); r=read();
            printf("%lld\n", query(l, r, 1, n, 1));
        }
    }
    return 0;
}

/*
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
*/

不过说实话,我第二遍搞完这道题我还是迷迷糊糊的,对于lazytag还是理解不能,之所以博客里能得出那样的结论,还是多亏搞了点其他的题不断的练习,尤其是这第二道模板。

P3373 【模板】线段树 2

这道题加入了乘法更新,其实也就是变相的加法运算。做法是设置两个懒标记,一个代表后续的加法,一个代表后续的乘法,并且定义好懒标记叠加的规则(先乘后加),这也是本题的难点,攻克它!你就能宣布你已经弄懂懒标记了。
不管怎么说,上题,练!


这里回忆一下线段树会用到些什么吧

#define ls p<<1						
#define rs p<<1|1						//定义左右儿子
ll tr[maxn<<2], tag[maxn<<2];			//节点赋值和懒标记数组

void load(ll p, ll l, ll r, ll k)		//在p节点装载懒标记
void pushup(ll p) 						//向上回溯更新区间值
void pushdown(ll p, ll l, ll r)			//向下传递懒标记
void build(ll p, ll l, ll r) 			//基础建树
void update(ll nl, ll nr, ll l, ll r, ll p, ll k)		//更新区间[l,r]
ll query(ll nl, ll nr, ll l, ll r, ll p)				//查询

就这么几个基础操作,解题还是得找思路


然后记一下这几道练习吧:

P3372 【模板】线段树 1
P3373 【模板】线段树 2
两道模板题,没什么好说的,基础。

P1198 [JSOI2008]最大数
求区间最大值,也是线段树一种用法,然后本题操作也是很普通的单点修改,如果理解了线段树立刻变水题。(当然也可以用ST表做)

P2184 贪婪大陆
思路是建两棵树,分别统计左端点和右端点,然后每次要判断有多少点不在区间内,听起来写两棵树很可怕,不过你会学到用结构体套函数(因为这个我还被逼哥喷去好好复习C语言课本呜呜呜,我真是第一次知道)
相比起上题来说可以称为更灵活,但是思路想出来就满好写。


之后会陆续更新的,等多做点线段树相关题吧,突然发现线段树对于解很多题目都好有用

猜你喜欢

转载自blog.csdn.net/qq_43455647/article/details/89230405