通过做题目思考:为什么我们要学线段树呢?

这个博客是用结构体写的线段树:
下面的抛出两个问题一起思考一下:

问题1

给1000个正整数,编号1到1000,用a[1],a[2],…,a[1000]表示。
统计:1.编号从L到R的所有数之和为多少?
其中1<= L <= R <= 1000.

你很快就可以思考出解决方法:

  1. O(n)枚举搜索
  2. 前缀和O(1)。

问题2:

给出n个数,n<=1000000,和m个操作,每个操作可能有两种:

  1. 在某个位置加上一个数;
  2. 询问区间[l,r]的和,并输出。

O(n)枚举。(超时呀!!!)那怎么办呢?
这个时候也思考一下之前认知里有的解决问题的方式----前缀和
你也会发现动态修改是不能用静态的前缀和实现的。

那我们通过思考,可以知道我们所学习的知识对有些问题是无法解决的时候。(学过线段树或者数组数组的除外)于是,我们就需要学习新的算法了。一种强大的数据结构:线段树(树状数组也是可以做这几个问题的)

好,接下来,我们开始一步一步走向线段树:

一.详细讲解线段树。

线段树的基础操作主要有5个:

建树、单点查询、单点修改、区间查询、区间修改。

线段树,是一种二叉搜索树。它将一段区间划分为若干单位区间,每一个节点都储存着一个区间。虽然说是一个区间,但实际它储存的是一个区间的信息(数值)。

线段树基础思想:分块?二分?各有看法,但实际用起来就是二分。

  1. 每个节点的左孩子区间范围为[l,mid],右孩子为[mid+1,r]

  2. 对于结点k,左孩子结点为2k,右孩子为2k+1,这是完全二叉树性质。

如下图:

1.建树

建树的实现,思想就是分治,根本上也可以说是二分。

每个节点以结构体的方式存储(当然以后用数组比较好,节省代码量减少写代码的时间),结构体包含以下几个信息:

  1. 区间左端点、右端点;
  2. 这个区间要维护的信息(视实际情况而定,数目不等)

实现思路:

利用二分,从二叉树的根开始。

  1. 二分到每一个节点,给它左右端点确定范围。

  2. 如果是叶子节点,存储要维护的信息。(输入的初始数据。)

  3. 状态合并。将某个结点w的左右两个孩子值之和合并到w上。

可以看代码:

struct node//结构体
{
   
    int l,r,w;//l,r分别表示区间左右端点,w表示区间和
}tree[15000];//数组开4倍
void build(int l,int r,int k)//k从1 开始
{
   
    tree[k].l=l,tree[k].r=r;
    if(l==r)//叶子节点
    {
   
        scanf("%d",&tree[k].w);
        return;
    }
    int mid=(l+r)/2;
    build(l,mid,2*k);//左孩子
    build(mid+1,r,2*k+1);//右孩子
    tree[k].w=tree[2*k].w+tree[2*k+1].w;//状态合并,此结点的w=两个孩子的w之和 
}

最后要注意:

  1. 结构体要开4倍空间,看上面画的一个[1,10]的线段树就懂了。

  2. 自己写的时候不要漏了return语句,因为到了叶子节点不需要再继续递归了。

2.单点查询

单点,就是一个点进行查询。因为线段树的特殊性,这里的点,就是左右端点相等的时候的 “区间”,也可以说是叶子节点。

方式:二分查询:

  1. 如果当前枚举的点左右端点相等,即叶子节点,就是目标节点。

  2. 如果不是,利用二分思想。对查询位置为x,和当前结点区间范围为 [l,r],中点为mid。如果x<=mid,则递归它的左孩子,否则递归它的右孩子,那么最后一定可以得出答案。

我们仍然需要证明一下其正确性:
假如不是目标位置,由if—else语句对目标位置定位,逐步缩小目标范围,最后一定能只到达目标叶子节点。

再详细一点:
我们二分的情况有三种:

void ask(int k)
{
   
    if(tree[k].l==tree[k].r) //当前结点的左右端点相等,是叶子节点,这就是答案
    {
   
        ans=tree[k].w;
        return ;
    }
    int mid=(tree[k].l+tree[k].r)/2;
    if(x<=mid) ask(k*2);//目标位置比中点靠左,递归左孩子 
    else ask(k*2+1);//反之,递归右孩子 
}

3.单点修改

结合单点查询的原理,找到x的位置;根据建树状态合并的原理,修改每个结点的状态。

int ask(int k)
{
   
    if(tree[k].l==tree[k].r)//二分到左右端点相等,也就是叶子节点。
    {
   
        tree[k].w+=y;
    }
    int mid=(tree[k].l+tree[k].r)/2;
    if(x<=mid)
        ask(2*k);
    else
        ask(2*k+1);
    tree[k].w=tree[2*k].w+tree[2*k+1].w;
}

4.区间查询

mid=(l+r)/2;

y<=mid ,即 查询区间全在,当前区间的左子区间,往左孩子走

x>mid 即 查询区间全在,当前区间的右子区间,往右孩子走

否则,两个子区间都走

正确性分析

情况1,3不用说,对于情况2,最差情况是搜到叶子节点,此时一定满足情况1

void sum( int k)
{
   
    if(tree[k].l<=x&&tree[k].r>=y)//找区间左右端点
    {
   
        ans+=tree[k].w;//找到目标
        return ;
    }
    int mid=(tree[k].l+tree[k].r)/2;//二分
    if(y<=mid)//查询区间全在当前区间的左子区间,往左孩子走.
        sum(2*k);
    if(x>mid)//查询区间全在当前区间的右子区间,往右孩子走.
        sum(2*k+1);
}

5.区间修改

修改的时候只修改对查询有用的点。
对,这就是区间修改的关键思路。
为了实现这个,我们引入一个新的状态——懒标记。
Ⅱ 懒标记
(懒标记比较难理解,我尽力讲明白。。。。。。)
1、直观理解:“懒”标记,懒嘛!用到它才动,不用它就睡觉。
2、作用:存储到这个节点的修改信息,暂时不把修改信息传到子节点。就像家长扣零花钱,你用的时候才给你,不用不给你。
3. 实现思路(重点):
a.原结构体中增加新的变量,存储这个懒标记。
b.递归到这个节点时,只更新这个节点的状态,并把当前的更改值累积到标记中。注意是累积,可以这样理解:过年,很多个亲戚都给你压岁钱,但你暂时不用,所以都被你父母扣下了。
c.什么时候才用到这个懒标记?当需要递归这个节点的子节点时,标记下传给子节点。这里不必管用哪个子节点,两个都传下去。就像你如果还有妹妹,父母给你们零花钱时总不能偏心吧
d.下传操作:
3部分:
①当前节点的懒标记累积到子节点的懒标记中。
②修改子节点状态。在引例中,就是原状态+子节点区间点的个数*父节点传下来的懒标记。

这就有疑问了,既然父节点都把标记传下来了,为什么还要乘父节点的懒标记,乘自己的不行吗?

因为自己的标记可能是父节点多次传下来的累积,每次都乘自己的懒标记造成重复累积

③父节点懒标记清0。这个懒标记已经传下去了,不清0后面再用这个懒标记时会重复下传。就像你父母给了你5元钱,你不能说因为前几次给了你10元钱, 所以这次给了你15元,那你不就亏大了。

懒标记下穿代码:f为懒标记,其余变量与前面含义一致。

void down(int k)
{
   
    tree[k*2].f+=tree[k].f;
    tree[k*2+1].f+=tree[k].f;
    tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
    tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
    tree[k].f=0;
}

完整的区间修改代码:

void add(int k)
{
   
    if(tree[k].l>=a&&tree[k].r<=b)//当前区间全部对要修改的区间有用 
    {
   
        tree[k].w+=(tree[k].r-tree[k].l+1)*x;//(r-l)+1区间点的总数
        tree[k].f+=x;
        return;
    }
    if(tree[k].f) down(k);//懒标记下传。只有不满足上面的if条件才执行,所以一定会用到当前节点的子节点 
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) add(k*2);
    if(b>m) add(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;//更改区间状态 
}

因为引入了懒标记,很多用不着的更改状态存了起来,这就会对区间查询、单点查询造成一定的影响。
所以在使用了懒标记的程序中,单点查询、区间查询也要像区间修改那样,对用得到的懒标记下传。其实就是加上一句if(tree[k].f) down(k),其余不变。

单点修改也需要下传懒标记

void ask(int k)//单点查询
{
   
    if(tree[k].l==tree[k].r)
    {
   
        ans=tree[k].w;
        return ;
    }
    if(tree[k].f) down(k);//懒标记下传,唯一需要更改的地方
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) ask(k*2);
    else ask(k*2+1);
}
void sum(int k)//区间查询
{
   
    if(tree[k].l>=x&&tree[k].r<=y) 
    {
   
        ans+=tree[k].w;
        return;
    }
    if(tree[k].f)  down(k)//懒标记下传,唯一需要更改的地方
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) sum(k*2);
    if(y>m) sum(k*2+1);
}
#include<cstdio>
using namespace std;
int n,p,a,b,m,x,y,ans;
struct node
{
   
    int l,r,w,f;
}tree[400001];
inline void build(int k,int ll,int rr)//建树 
{
   
    tree[k].l=ll,tree[k].r=rr;
    if(tree[k].l==tree[k].r)
    {
   
        scanf("%d",&tree[k].w);
        return;
    }
    int m=(ll+rr)/2;
    build(k*2,ll,m);
    build(k*2+1,m+1,rr);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
inline void down(int k)//标记下传 
{
   
    tree[k*2].f+=tree[k].f;
    tree[k*2+1].f+=tree[k].f;
    tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
    tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
    tree[k].f=0;
}
inline void ask_point(int k)//单点查询
{
   
    if(tree[k].l==tree[k].r)
    {
   
        ans=tree[k].w;
        return ;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) ask_point(k*2);
    else ask_point(k*2+1);
}
inline void change_point(int k)//单点修改 
{
   
    if(tree[k].l==tree[k].r)
    {
   
        tree[k].w+=y;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) change_point(k*2);
    else change_point(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w; 
}
inline void ask_interval(int k)//区间查询 
{
   
    if(tree[k].l>=a&&tree[k].r<=b) 
    {
   
        ans+=tree[k].w;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) ask_interval(k*2);
    if(b>m) ask_interval(k*2+1);
}
inline void change_interval(int k)//区间修改 
{
   
    if(tree[k].l>=a&&tree[k].r<=b)
    {
   
        tree[k].w+=(tree[k].r-tree[k].l+1)*y;
        tree[k].f+=y;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) change_interval(k*2);
    if(b>m) change_interval(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
int main()
{
   
    scanf("%d",&n);//n个节点 
    build(1,1,n);//建树 
    scanf("%d",&m);//m种操作 
    for(int i=1;i<=m;i++)
    {
   
        scanf("%d",&p);
        ans=0;
        if(p==1)
        {
   
            scanf("%d",&x);
            ask_point(1);//单点查询,输出第x个数 
            printf("%d",ans);
        } 
        else if(p==2)
        {
   
            scanf("%d%d",&x,&y);
            change_point(1);//单点修改 
        }
        else if(p==3)
        {
   
            scanf("%d%d",&a,&b);//区间查询 
            ask_interval(1);
            printf("%d\n",ans);
        }
        else
        {
   
             scanf("%d%d%d",&a,&b,&y);//区间修改 
             change_interval(1);
        }
    }
}

这个博客是用结构体写的线段树:
想看用数组写的可以去看这篇: