线段树

一、基本概念

1、线段树是一棵二叉搜索树,它储存的是一个区间的信息。

2、每个节点以结构体的方式存储,结构体包含以下几个信息:

     区间左端点、右端点;(这两者必有)

     这个区间要维护的信息(事实际情况而定,数目不等)。

3、线段树的基本思想:二分

4、线段树一般结构如图所示:

5、特殊性质:

由上图可得,

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

2、对于结点k,左孩子结点为2*k,右孩子为2*k+1,这符合完全二叉树的性质

二、线段树的基础操作

注:以下基础操作均以引例中的求和为例,结构体以此为例:

struct node
{
    int left,right;//左右
    int weight;//权值
    int flag;//延迟标记
} tree[400001];

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

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

1、建树,即建立一棵线段树

   ① 主体思路:

      a、对于二分到的每一个结点,给它的左右端点确定范围。

                     b、如果是叶子节点,存储要维护的信息。

                     c、状态合并。

   ②代码 

void build(int k,int ll,int rr)//建树,k是父亲节点,ll、rr左右孩子
{
    tree[k].left=ll,tree[k].right=rr;
    if(tree[k].left==tree[k].right)//如果是叶子结点,直接赋值
    {
        scanf("%d",&tree[k].weight);
        return;
    }
    int m=(ll+rr)/2;
    build(k*2,ll,m);
    build(k*2+1,m+1,rr);
    tree[k].weight=tree[k*2].weight+tree[k*2+1].weight;//求区间和,所以父亲结点是左右孩子的和
}

    ③注意

     a.结构体要开4倍空间,因为叶子节点的存储并不是按顺序存储的。

     b.千万不要漏了return语句,因为到了叶子节点不需要再继续递归了。 

2、单点查询,即查询一个点的状态,设待查询点为x

   ①主体思路:与二分查询法基本一致,如果当前枚举的点左右端点相等,即叶子节点,就是目标节点。如果不是,因为这是二分法,所以设查询位置为x,当前结点区间范围为了l,r,中点为mid,则如果x<=mid,则递归它的左孩子,否则递归它的右孩子

     ②代码

void ask_point(int k)//单点查询
{
    if(tree[k].left==tree[k].right)
    {
        ans=tree[k].weight;
        return ;
    }
    if(tree[k].flag) down(k);
    int m=(tree[k].left+tree[k].right)/2;
    if(x<=m) ask_point(k*2);
    else ask_point(k*2+1);
}

  3、单点修改,即更改某一个点的状态。用引例中的例子,对第x个数加上y

     ①主体思路

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

      ②代码

void change_point(int k)//单点修改,第x个数加上y
{
    if(tree[k].left==tree[k].right)
    {
        tree[k].weight+=y;
        return;
    }
    if(tree[k].flag) down(k);
    int m=(tree[k].left+tree[k].right)/2;
    if(x<=m) change_point(k*2);
    else change_point(k*2+1);
    tree[k].weight=tree[k*2].weight+tree[k*2+1].weight;
}

4、区间查询,即查询一段区间的状态,在引例中为查询区间[x,y]的和

   ①主体思路

     

    ②代码

void ask_interval(int k)//区间查询,查询区间和
{
    if(tree[k].left>=a&&tree[k].right<=b)
    {
        ans+=tree[k].weight;
        return;
    }
    if(tree[k].flag) down(k);
    int m=(tree[k].left+tree[k].right)/2;
    if(a<=m) ask_interval(k*2);
    if(b>m) ask_interval(k*2+1);
}

5、区间修改,即修改一段连续区间的值,我们已给区间[a,b]的每个数都加x为例讲解

      Ⅰ.引子

 

       有人可能就想到了:

       修改的时候只修改对查询有用的点。

       对,这就是区间修改的关键思路。

      为了实现这个,我们引入一个新的状态——延迟标记

     Ⅱ 延迟标记

       1、直观理解:“延迟”标记,用到它才动,不用它就不动。

       2、作用:存储到这个节点的修改信息,暂时不把修改信息传到子节点。

       3、实现思路(重点):

           a.原结构体中增加新的变量,存储这个延迟标记。

           b.递归到这个节点时,只更新这个节点的状态,并把当前的更改值累积到标记中。

           c.当需要递归这个节点的子节点时,标记下传给子节点。这里不必管用哪个子节点,两个都传下去。

           d.下传操作:

               3部分:①当前节点的延迟标记累积到子节点的延迟标记中。

                            ②修改子节点状态。在引例中,就是原状态+子节点区间点的个数*父节点传下来的延迟标记。 

                            ③父节点延迟标记清0。

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

延迟标记代码:

void down(int k)//延迟标记下传
{
    tree[k*2].flag+=tree[k].flag;
    tree[k*2+1].flag+=tree[k].flag;
    tree[k*2].weight+=tree[k].flag*(tree[k*2].right-tree[k*2].left+1);
    tree[k*2+1].weight+=tree[k].flag*(tree[k*2+1].right-tree[k*2+1].left+1);
    tree[k].flag=0;
}

  区间修改代码

void change_interval(int k)//区间修改,把a-b之间数改成k
{
    if(tree[k].left>=a&&tree[k].right<=b)
    {
        tree[k].weight+=(tree[k].right-tree[k].left+1)*y;
        tree[k].flag+=y;
        return;
    }
    if(tree[k].flag) down(k);
    int m=(tree[k].left+tree[k].right)/2;
    if(a<=m) change_interval(k*2);
    if(b>m) change_interval(k*2+1);
    tree[k].weight=tree[k*2].weight+tree[k*2+1].weight;
}

 

三、线段树五种操作模板

//#include <bits/stdc++.h>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <iostream>
#include <algorithm>
#include <string>
#include <queue>
#include <stack>
#include <map>
#include <set>

#define IO ios::sync_with_stdio(false);\
    cin.tie(0);\
    cout.tie(0);

typedef long long LL;
const long long INF = 0x3f3f3f3f;
const long long mod = 1e9+7;
const double PI = acos(-1.0);
const int maxn = 100000;
const char week[7][10]= {"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"};
const char month[12][10]= {"Janurary","February","March","April","May","June","July",
                           "August","September","October","November","December"
                          };
const int daym[2][13] = {{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
    {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
const int dir4[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
const int dir8[8][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}};

using namespace std;
LL n,m;//n个结点,m种操作,操作为p
LL ans;//存储答案
struct node
{
    LL left,right;//左右
    LL weight;//权值
    LL flag;//延迟标记
} tree[410000];
void build(LL k,LL ll,LL rr)//建树,k是父亲节点,ll、rr左右孩子
{
    tree[k].left=ll,tree[k].right=rr;
    if(tree[k].left==tree[k].right)//如果是叶子结点,直接赋值
    {
        scanf("%lld",&tree[k].weight);
        return;
    }
    LL m=(ll+rr)/2;
    build(k*2,ll,m);
    build(k*2+1,m+1,rr);
    tree[k].weight=tree[k*2].weight+tree[k*2+1].weight;//求区间和,所以父亲结点是左右孩子的和
}
void down(LL k)//延迟标记下传
{
    tree[k*2].flag+=tree[k].flag;
    tree[k*2+1].flag+=tree[k].flag;
    tree[k*2].weight+=tree[k].flag*(tree[k*2].right-tree[k*2].left+1);
    tree[k*2+1].weight+=tree[k].flag*(tree[k*2+1].right-tree[k*2+1].left+1);
    tree[k].flag=0;
}
void ask_point(LL k,LL x)//单点查询
{
    if(tree[k].left==tree[k].right)
    {
        ans=tree[k].weight;
        return ;
    }
    if(tree[k].flag) down(k);
    LL m=(tree[k].left+tree[k].right)/2;
    if(x<=m) ask_point(k*2,x);
    else ask_point(k*2+1,x);
}
void change_point(LL k,LL x,LL y)//单点修改,第x个数加上y
{
    if(tree[k].left==tree[k].right)
    {
        tree[k].weight+=y;
        return;
    }
    if(tree[k].flag) down(k);
    LL m=(tree[k].left+tree[k].right)/2;
    if(x<=m) change_point(k*2,x,y);
    else change_point(k*2+1,x,y);
    tree[k].weight=tree[k*2].weight+tree[k*2+1].weight;
}
void ask_interval(LL k,LL a,LL b)//区间查询,查询区间和
{
    if(tree[k].left>=a&&tree[k].right<=b)
    {
        ans+=tree[k].weight;
        return;
    }
    if(tree[k].flag) down(k);
    LL m=(tree[k].left+tree[k].right)/2;
    if(a<=m) ask_interval(k*2,a,b);
    if(b>m) ask_interval(k*2+1,a,b);
}
void change_interval(LL k,LL a,LL b,LL y)//区间修改,把a-b之间数改成k
{
    if(tree[k].left>=a&&tree[k].right<=b)
    {
        tree[k].weight+=(tree[k].right-tree[k].left+1)*y;
        tree[k].flag+=y;
        return;
    }
    if(tree[k].flag) down(k);
    LL m=(tree[k].left+tree[k].right)/2;
    if(a<=m) change_interval(k*2,a,b,y);
    if(b>m) change_interval(k*2+1,a,b,y);
    tree[k].weight=tree[k*2].weight+tree[k*2+1].weight;
}
int main()
{
    scanf("%d",&n);//n个节点
    scanf("%d",&m);//m种操作
    build(1,1,n);//建树,传值根节点1,左孩子1,有孩子n
    for(int i=1; i<=m; i++)
    {
        int p;
        getchar();
        scanf("%d",&p);
        ans=0;
        /*以下操作传值过去的为根节点1*/
        if(p==1)
        {
            LL x;
            scanf("%lld",&x);
            ask_point(1,x);//单点查询,输出第x个数
            printf("%lld",ans);
        }
        else if(p==2)
        {
            LL x,y;
            scanf("%lld%lld",&x,&y);
            change_point(1,x,y);//单点修改
        }
        else if(p==3)
        {
            LL a,b;
            scanf("%lld%lld",&a,&b);//区间查询
            ask_interval(1,a,b);
            printf("%lld\n",ans);
        }
        else if(p==4)//每段都加上一个数
        {
            LL a,b,y;
            scanf("%lld%lld%lld",&a,&b,&y);//区间修改
            change_interval(1,a,b,y);
        }
    }
}
posted @ 2017-08-10 10:37  爱国呐  阅读(310)  评论(0编辑  收藏  举报