【数据结构】线段树

我与影子孤独终老i 提交于 2019-11-28 21:46:58

【数据结构】线段树

这两天被线段树折磨得痛不欲生,大把大把的时间被花在改错上,遂决定将我在这一知识点的学习笔记记录下来,供以后复习使用。
线段树是一种维护区间信息的数据结构,可以在O(logn)的时间复杂度内实现单点修改/查询以及区间修改/查询。应用范围包括区间求和,区间求最值等。
这篇笔记以维护区间和的线段树为例。

初始化

线段树的存储

我之前使用的是结构体来存储线段树,如下:

struct SegTree
{
    int l,r;    //存储每个节点表示的区间
    int sum;    //存储区间和
    int lazy;   //存储延迟标记(下文提到,也是个令人抓狂的玩意儿)
}segtree[400005];

但当我写到区间查询时,我把用于存储节点区间的l,r和询问的l,r弄混了(!!!)。
这也是为什么我查错查了一个下午没查出来QAQ
其实,我们可以直接使用数组存储线段树的数据以及懒惰标记,每个节点表示的区间现场算,这样也更节省空间。

int tree[400005];   //这里存储的是区间和
int lazy[400005];

当然如果你的线段树要维护多个东西,还是用结构体方便些,也更加直观。
其实嘛,如果你足够细心,就不会弄出我这种沙雕操作。存下l和r也挺好,可以减小代码常数,节省时间。

建树

众所周知,线段树是一棵完全二叉树,当父节点的编号为x时,它的两个子节点可表示为x<<1和x<<1|1(同x2和x2+1)
_注:位运算'<<1'相当于'*2',同时运算效率更高,同时在二进制表示下,左移一位后末位为零,此时'|1'(即把最后一位置为1)相当于'+1'_

void build(int l,int r,int root) //建树 
{
    lazy[root]=0;   //初始化延迟标记为0
    if(l==r)        //已经初始化到了叶子节点——递归边界
    {
        tree[root]=source[l];   //这里的source数组存的是初始数据
        return;
    }
    int mid=(l+r)>>1;   //将区间分开
    build(l,mid,root<<1);   //递归初始化左边的子节点
    build(mid+1,r,root<<1|1);   //递归初始化右边的子节点
    pushup(root);   //这个函数用于回溯时从下往上更新节点数据(之前是零),后面讲解
}

数据查询

树建成了,就可以应对一波又一波的询问了。

单点查询

在线段树中找单点类似于二分查找,比较简单,直接上代码:

int query1(int root,int l,int r,int pos)
{
    if(l==r)    //已经搜到了叶子节点——答案
        return tree[root];
    int mid=(l+r)>>1;   //把区间分开
    if(pos<=mid)    //如果要找的点在左边的区间
        return query(root<<1,l,mid,pos);    //向左儿子递归查找
    else if(pos>mid)    //同理,如果在右边的区间
        return query(root<<1|1,mid+1,r,pos);    //向右儿子递归
}

区间查询

区间查询较单点查询麻烦,但也还好,加上延迟标记才算真麻烦呢。在线段树中,当我们查询一个区间时,我们不必一个一个点地去查询统计,直接取用预处理好的区间的答案即可。
那如果我们要查的区间与预处理的区间不一样,怎么办?(比如说建树时建的是1~5,6~10,要查的是1~6)简单,把区间照着割开查完,再合起来****呀!3~7就分割成3~3,4~5和6~7,找到它们的和再加起来。

//为了避免重蹈覆辙,询问的区间用ql和qr表示。它们在递归中是始终不变的常量,这一点要记住
int query2(int root,int l,int r,int ql,int qr)
{
    if(ql<=l&&qr>=r)    //如果我们查到的区间已经包含在了询问的区间中
        return tree[root];  //不要犹豫,直接返回答案,后面再加起来
    int mid=(l+r)>>1;   //同样的,按照之前的划分把区间分开
    int ans=0;
    if(ql<=mid) //如果在左儿子中有我们要查询区间的一部分
        ans+=query2(root<<1,l,mid,ql,qr);   //那我们就要递归去找到那一部分失落的宝藏,加上来
    if(qr>mid)  //右儿子同理
        ans+=query2(root<<1|1,mid+1,r,ql,qr);
    return ans;
}

数据修改

pushup()

当我们建树初始化或修改数据时,都是递归找到叶子节点,然后修改叶子节点的数据。而这时它的父节点还没有被更新,因此我们需要构建一个pushup函数,回溯时在每一层都用自己的两个子节点的数据更新自己的数据

void pushup(int root) 
{
    tree[root]=(tree[root<<1]+tree[root<<1|1]);
}

这样可以保证每次修改之后,每一层的数据都是最新的。

单点修改

单点修改的原理和单点查询是一样的,都是递归查找到某一点。只不过单点修改把返回值变为了修改值,并且回溯时更新了父节点

void update1(int l,int r,int root,int pos,int value)    //多了一个传入参数——修改的值
{
    if(l==r)
    {
        tree[root]+=value;
        return;
    }
    int mid=(l+r)>>1;
    if(pos<=mid)
        update1(l,mid,root<<1,pos,value);
    else
        update1(mid+1,r,root<<1|1,pos,value);
    pushup(root);   //递归完毕,向上更新
}

区间修改

与标准的线段树相比,这还是一个半成品(without延迟标记),因此它跑起来很慢,但放在这里有助于我们的理解。
分区间过程类似区间查询,而修改过程类似单点修改

//仔细查看这里与区间查询的不同点,有处地方产生了大量不必要运算,大大增加了时间复杂度
//延迟标记为了解决它应运而生
void update2(int l,int r,int root,int ul,int ur,int value)
{
    if(l==r)
    {
        tree[root]=tree[root]+value;
        return;
    }
    int mid=(l+r)>>1;
    if(ul<=mid) //如果左边部分包含了我们要修改的区间
        update2(l,mid,root<<1,ul,ur,value); //递归下去,找到它,改掉它!
    if(ur>mid)
        update2(mid+1,r,root<<1|1,ul,ur,value);
    pushup(root);   //别忘了更新
}

数据更新进阶版——延迟标记的使用

线段树之精髓所在

延迟标记(又名懒惰标记)

懒一点反而更省时间

之前提到,每次区间修改时都要递归到叶子节点,修改完毕再回溯,简直就像一个一个单点修改。这时就出现了问题——举个例子:假设我们拥有一个1~10的线段树,此时我们需要改1~5的值(刚好是根节点的左儿子),而这时再递归到叶子节点就显得过于麻烦了,要从1~1,2~2......到1~2,1~3,4~5最后到1~5,这些全部都要更新一遍,如果数据量再大一点,估计就T掉了。况且我们花这么大力气修改,如果后面没有查询到,岂不是很亏(╯﹏╰)。
因此,我们干脆每次修改时,只更新到对应父节点而不向下继续(比如在上文例子中只更新到1~5)。当我们需要查询子节点时,再继续向下更新,查到哪里更哪里。而这时,我们需要一个标记来表示我们曾更新到这里但没有向下继续,以便下次查询时继续更新,这就是“延迟标记”。

延迟标记的记录

因此,我们在区间修改时就可以像区间查询那样,把整个区间分割成几段已有的区间进行修改,再给它们加上延迟标记。
注意:此时我们修改的是区间,也就是说区间内的每一个元素都要加上相应数值,因此在修改区间时,要乘上区间内元素的数量。

if(ul<=l&&ur>=r)
    {
        tree[root]=tree[root]+k*(r-l+1);
        lazy[root]=lazy[root]+k;
        return;
    }

这样就省事多了~

标记下传 pushdown()

可这时候我们又碰到了一个难题:我们之前记下了延时标记,那当我们要用标记以下的数据的时候,怎么继续向下更新呢?
向下更新主要分两个部分:延迟标记下传,把延迟标记转化为区间和加上去。第一步还好说,直接把延迟标记的值赋给子节点就好了,第二步就有点麻烦了:我们之前存储的是区间内每个点需要更改的数值,而我们要加上的是区间内总共的变化。因此,我们还是需要乘上区间内元素的个数。
为了达成这个目的,我们在此构建一个pushdown()函数。

void pushdown(int root,int ln,int rn)   //需要这一操作的节点,左区间元素个数,右区间元素个数
{
    if(lazy[root]!=0)   //总不能传个寂寞吧
    {
        lazy[root<<1]+=lazy[root];  //左区间延迟标记赋值
        lazy[root<<1|1]+=lazy[root];    //右区间延迟标记赋值
        tree[root<<1]+=lazy[root]*ln;   //左区间数据更新
        tree[root<<1|1]+=lazy[root]*rn; //右区间数据更新
        lazy[root]=0;   //别忘了把原来的标记清除,不然下次就会重复下传了
    }
}

其实到了叶子节点,你就可以跳过pushdown这一操作了(叶子节点下啥也没有),这也是对代码的一个优化。
因此我们隆重推出各种查询修改的2.0版本(单点修改不需要传回最新数据,也不像区间修改)

单点查询2.0版本

直接上代码,只是多了个pushdown

    int query1(int root,int l,int r,int pos)
    {
        if(l==r)
            return tree[root];
        int mid=(l+r)>>1;
        pushdown(root,mid-l+1,r-mid);   //看这里!
        if(pos<=mid)
            return query(root<<1,l,mid,pos);
        else if(pos>mid)
            return query(root<<1|1,mid+1,r,pos);
    }

区间查询2.0版本

也是多了一行一样的代码:pushdown(root,mid-l+1,r-mid)
……emmm我不想全部再贴一遍了你们自己脑补添加吧

区间修改2.0版本

void update2(int l,int r,int root,int ul,int ur,int value)
{
    if(ul<=l&&ur>=r)
        {
            tree[root]=tree[root]+value*(r-l+1);
            lazy[root]=lazy[root]+value;
            return;
        }
    int mid=(l+r)>>1;
    pushdown(root,mid-l+1,r-mid);
    if(ul<=mid)
        update2(l,mid,root<<1,ul,ur,value);
    if(ur>mid)
        update2(mid+1,r,root<<1|1,ul,ur,value);
    pushup(root);
}

总结

先贴一下完整版的代码吧~
P3372 【模板】线段树 1的题解
没有单点修改和查询
不要看我只是把之前的代码复制粘贴合起来凑字数,这工程量也不小的qwq

#include<cstdio>
#define int long long
using namespace std;
int n,m;
int source[100005];
int tree[400005];
int lazy[400005];
void pushdown(int root,int ln,int rn)
{
    if(lazy[root]!=0)
    {
        lazy[root<<1]+=lazy[root];
        lazy[root<<1|1]+=lazy[root];
        tree[root<<1]+=lazy[root]*ln;
        tree[root<<1|1]+=lazy[root]*rn;
        lazy[rt]=0;
    }
}
void pushup(int root) 
{
    tree[root]=(tree[root<<1]+tree[root<<1|1]);
}
void build(int l,int r,int root) //建树 
{
    lazy[root]=0;
    if(l==r)
    {
        tree[root]=source[l];
        return;
    }
    int mid=(l+r)>>1;
    build(l,mid,root<<1);
    build(mid+1,r,root<<1|1);
    pushup(root);
    }
int query(int root,int l,int r,int ql,int qr)
{
    if(ql<=l&&qr>=r)
        return tree[root];
    int mid=(l+r)>>1;
    int ans=0;
    pushdown(root,mid-l+1,r-mid);
    if(ql<=mid)
            ans+=query(root<<1,l,mid,ql,qr);
    if(qr>mid)
        ans+=query(root<<1|1,mid+1,r,ql,qr);
    return ans;
}
void update(int l,int r,int root,int ul,int ur,int value)
{
    if(ul<=l&&ur>=r)
        {
            tree[root]=tree[root]+value*(r-l+1);
            lazy[root]=lazy[root]+value;
            return;
        }
    int mid=(l+r)>>1;
    pushdown(root,mid-l+1,r-mid);
    if(ul<=mid)
        update(l,mid,root<<1,ul,ur,value);
    if(ur>mid)
        update(mid+1,r,root<<1|1,ul,ur,value);
    pushup(root);
}
signed main()
{
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%lld",&source[i]);
    build(1,n,1);
    for(int i=1;i<=m;i++)
    {
        //不要吐槽我的千奇百怪的变量名了qwq
        //我们机房还有用全拼命名变量的旷世奇才呢
        int ctrl,x,y,k;
        scanf("%lld%lld%lld",&ctrl,&x,&y);
        if(ctrl==1)
        {
            scanf("%lld",&k);
            update(1,n,1,x,y,k);
        }
        if(ctrl==2)
            printf("%lld\n",query(1,1,n,x,y));
    }
    return 0;
}

最后,这只是一篇来自一个小蒟蒻的笔记,请谨慎食用。
如果有错误,请一定一定一定要向我提出,也避免大家学到个错误答案是吧~
我的洛谷账号在右边的菜单里,欢迎打扰qwq

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!