线段树 (入门篇)

在信息学竞赛中,经常遇到这样一类问题:这类问题通常可以建模成数轴上的问题或是数列的问题,具体的操作一般是每次对数轴上的一个区间或是数列中的连续若干个数进行一种相同的处理。常规的做法一般依托于线性表这种数据结构,导致了处理只能针对各个元素逐个进行,因此算法的效率较低。
线段树是一种能够有效处理区间操作的高级数据结构,利用这种数据结构,我们能够设计出针对上述问题更加高效的算法。

一、什么是线段树

首先,线段树是一棵“树”,而且是一棵完全二叉树。同时,“线段”两字反映出线段树的另一个特点:每个节点表示的是一个“线段”,或者说是一个区间。事实上,一棵线段树的根节点表示的是“整体”的区间,而它的左右子树也是一棵线段树,分别表示的是这个区间的左半边和右半边。
线段树就是这样一种数据结构。它能够将我们需要处理的区间不相交的分成若干个小区间,每次维护都可以在这样一些分解后的区间上进行,并且查询的时候,我们也能够根据这些被分解了的区间上的信息合并出整个询问区间上的查询结构。
下面是个线段树的图示:

可以发现,如果一棵线段树的根结点表示的区间为(0,n],那么这棵线段树将会有n个叶子结点,又因为每个结点要么有两个子结点,要么没有子结点,所以线段树总结点数为2*n-1,因此线段树的空间复杂度为O(n)
。然后可以发现一棵线段树除了最后一层外,前面每一层的结点都是满的,因此线段树的深度 。

二、线段树的结构

根据上图,我们理解一下线段树的定义:

定义1、长度为1的线段称为元线段。
定义2、一棵树被称为线段树,当且仅当这棵树满足如下条件:
(1)该树是一棵二叉树;
(2)树中的每一个结点都对应一条线段[a,b];
(3)树中的结点是叶子结点,当且仅当它所代表的线段是元线段;
(4)树中非叶子结点都有左右两棵子树,左子树树根对应线段[a ,(a+b)/2],右子树树根对应线段[(a+b)/2+1,b]。
通俗地说,线段树是一棵二叉树,树中的每一个结点表示了一个区间[a,b]。每一个叶子节点上a=b,这表示了一个初等区间。对于每一个内部结点b-a>=1,设根为[a,b]的线段树为T(a,b),则进一步将此线段树分为左子树T(a,(a+b)/2),以及右子树T((a+b)/2+1,b),直到分裂为一个初等区间为止。
这个结构我们是将区间分离成单个点的线段树,这是最常用的一种线段树形式。还有一种线段树记录的是区间的状态,点是忽略的。图例如下:

我们在拿到题目的时候,要分清楚我们在乎的是点还是在乎的长度。
从上的的定义可以看出,线段树的结构是递归定义的。 这时候再看上一页的线段树,深刻理解这个结构。

三、线段树的性质

性质1、长度范围为[1,L]的一棵线段树的深度不超过log2(L-1)+1;
性质2、线段树上的结点个数不超过2L个;
性质3、线段树把区间上的任意一条长度为L的线段都分成不超过2log2L条不相交的线段。
性质4、任两个结点要么是包含关系要么没有公共部分, 不可能部分重叠
性质5、 给定一个叶子p, 从根到p路径上所有结点(即p的所有直系祖先)代表的区间都包含点p, 且其他结点代表的区间都不包含点p

根据这些性质:我们知道,在线段树中,对于1..N包含的所有区间,在线段树中都存在,所以,在线段树中,我们可以对区间进行整体操作。并且根据二叉树的层数,我们知道,查找一个区间的时间辅助度是log2(n)级别的。
比如:线段[1, 9]的线段树和[2, 8]的分解

根据性质3,我们知道,线段树的对线段的操作是2* log2(n)级别的。

四、线段树的存储方式

根据性质,我们知道,线段树类似于完全二叉树,所以,我们用完全二叉树的存储形式——一维数组来存储线段树,这是比较常用的存储方式。(动态写法我们直接忽略掉)
我们可以将线段树根节点存储在1号单元,i号单元的左孩子存储在i*2号单元,右孩子存储在i*2+1号单元。这样一个一维数组就可以存储下整个线段树。思考一个问题,包含n个点的线段树,一维数组开多少够用? 不会超过4*n。仔细思考为什么?
对于线段树中的每个结点,其表示一个区间,我们可以记录和这个区间相关的一些信息(如最大值、最小值、和等),但要满足可二分性,即能直接由其子结点的相关信息得到。

简单来说,如果要修改一个区间的值,如果这个区间刚好被完全包含了就直接返回,打上标记(要改成多少),那么下次要用子节点的时候将标记下传即可,显然,对于本题要打两个tag,那么sum[o] = sum[o] * mul[o] + add[o],sum为和,mul为乘的数,add为加的数,那么如果我们乘一个数c,sum[o] = sum[o] * mul[o] * c + add[o] * c,将mul[o] * c和add[o] * c看作两个整体,那么可以发现如果乘一个数c的话,add数组要*c,mul数组也要*c,同理,如果加一个数c,那么只需要add数组+c即可,因为在下传标记的时候既有加,又有减(可能为0),所以add和mul数组的计算一定要将这两种情况都计算到.

   如果在线段树上几个区间操作的性质相同的,先把要求的数用操作需要的量给表示出来,然后对于每一种操作看看需要维护的标记的变化,最后合并一下即可.
   以下是我经过看过许多板子综合的较好的板子:
#include<bits/stdc++.h>
#define le l,mid,o*2
#define re mid+1,r,o*2+1
using namespace std;
const long long maxn=1000010;
int op,t,g,c;
long long n,p,a[maxn],add[maxn],mul[maxn],sum[maxn],m;
long long read()
{
    long long sum=0;
    char c;
    c=getchar();
    for(;c<'0'||c>'9';c=getchar());
    for(;c>='0'&&c<='9';c=getchar())sum=sum*10+c-'0';
    return sum;
}
void build(int l,int r,int o)
{
    mul[o]=1;add[o]=0;
    if(l==r)
    {
        sum[o]=a[l];
        return;
    }
    int mid=(l+r)>>1;
    build(le);
    build(re);
    sum[o]=(sum[o*2]+sum[o*2+1])%p;
}

void pushdown(int o,int k)
{
    sum[o*2]=(sum[o*2]*mul[o]+add[o]*(k-(k>>1)))%p;
    sum[o*2+1]=(sum[o*2+1]*mul[o]+add[o]*(k>>1))%p;
    mul[o*2]=mul[o*2]*mul[o]%p;
    mul[o*2+1]=mul[o*2+1]*mul[o]%p;
    add[o*2]=(add[o*2]*mul[o]+add[o])%p;
    add[o*2+1]=(add[o*2+1]*mul[o]+add[o])%p;
    mul[o]=1;
    add[o]=0;
}
void jia(int l,int r,int o)
{
    if(t<=l&&r<=g)
    {
        add[o]=(add[o]+c)%p;
        sum[o]=(sum[o]+c*(r-l+1))%p;
        return;
    }
    pushdown(o,r-l+1);
    int mid=(l+r)>>1;
    if(t<=mid) jia(le);
    if(g>mid) jia(re);
    sum[o]=(sum[o*2]+sum[o*2+1])%p;
}
void cheng(int l,int r,int o)
{
    if(t<=l&&r<=g)
    {
        add[o]=(add[o]*c)%p;
        mul[o]=(mul[o]*c)%p;
        sum[o]=(sum[o]*c)%p;
        return;
    }
    pushdown(o,r-l+1);
    int mid=(l+r)>>1;
    if(t<=mid) cheng(le);
    if(g>mid) cheng(re);
    sum[o]=(sum[o*2]+sum[o*2+1])%p;
}
long long query(int l,int r,int o)
{
    if(t<=l&&r<=g) return sum[o]%p;
    int mid=(l+r)>>1;
    pushdown(o,r-l+1);
    long long temp=0;
    if(t<=mid) temp=(temp+query(le))%p;
    if(g>mid) temp=(temp+query(re))%p;
    sum[o]=(sum[o*2]+sum[o*2+1])%p;
    return temp%p;
}
int main()
{
    n=read();m=read();p=read();
    for(int i=1;i<=n;i++) a[i]=read();
    build(1,n,1);
    for(int i=1;i<=m;i++)
    {

        op=read();t=read();g=read();
        if (op==1)
        {
            c=read();
            cheng(1,n,1);
        }
        if (op==2)
        {
            c=read();
            jia(1,n,1);
        }
        if(op==3)
        {
            printf("%lld\n",query(1,n,1)%p);
        }
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值