数据结构线段树 (定义 & 点修改/区间查询)

Posted stelayuri

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构线段树 (定义 & 点修改/区间查询)相关的知识,希望对你有一定的参考价值。

【本文描述高级数据结构线段树的定义】

【并解决 点修改/区间查询 的问题】




结构与定义


  • 线段树的基本结构

    ? 技术图片

    由图可知,线段树的每一个节点都代表着一段区间

    且同一层的节点(深度相同的节点)所表示的区间互不重叠

所有叶子节点代表的区间左边界与右边界相同(叶子节点代表单个元素)

  • 普遍规定

    如果某个非叶子节点代表的区间包含元素个数为奇数

    则它的左儿子包含的元素个数比右儿子大 1

    在代码部分,非叶子节点表示区间为 [l,r]

    则左儿子为 [ l , (l+r)/2 ] ,右儿子为 [ (l+r)/2+1 , r ]

    除法计算向下取整

    如果需要用线段树对一段包含N个数的区间操作,则树一般需要开4N的空间以避免越界

  • 存储方式

    线段树以数组存储节点的值

    以层序遍历的方式自上而下,自左而右依次进行编号

    一般从0开始或者从1开始

    如果从0开始,则左儿子位置为2k+1,右儿子为2k+2

    如果从1开始,则左儿子位置为2k,右儿子为2k+1

    本文代码编号从1开始




线段树实现方法演示

? 该部分图片截自av6835937

? 首先,对于一个区间大小为10的线段树,它的基本构造如下图所示

? 技术图片

? 如果区间大小为5,代入一个数组[1,5,4,1,6],那么线段树各个节点表示的元素为

? 技术图片

? 按照上述图示即可先进行建树

? 然后先考虑点修改的操作

? 如果要将a[2]的值修改为3,则从根节点到叶子节点的访问顺序为

? 技术图片

? 访问到叶子节点后,修改叶子节点的值,然后自下而上回溯每一个访问时经过的节点并依次进行更新,直到回溯到根节点

? 因为只要叶子节点更新后,它的父节点就可以通过左儿子+右儿子来重新更新sum值,最后完成更新

? 技术图片

? 然后考虑区间查询操作

? 传统方法中,查询区间元素之和的做法就是从[ l , r ]一个个加起来输出,这种O(n)的做法在遇到大数量的查询时运行速度完全无法满足要求

? 因此,根据线段树的特点,我们可以通过预处理线段的和当作多个元素之和

? 在上面建树时已经把线段和处理完了,所以接下来就需要考虑把符合的线段按最小个数取出来求和即可

? 下图展示了求区间[3,5]的和的过程

? 技术图片

? 可以看到,在元素总数为5的线段树里,4 5这两个元素可以用 [4,5] 这个线段来表示

? 所以3 4 5这三个元素和的值只需要访问[3,3] [4,5]这两个区间就能得到

? 在查找时,需要以左边界3与右边界5作为媒介

? 如果访问到的一个节点全部包含于[3,5],那么整个节点就可以直接拿过来作为答案,不需要再访问子树。图中访问到的节点[4,5]就满足这个情况。

? 如果访问到的一个节点部分包含于[3,5],此时不能拿整个节点的值作为答案,需要分别访问其左子树和右子树。图中的根节点[1,5]与节点[1,3]就满足这个情况。

? 如果访问到的一个节点不包含于[3,5],则无需继续考虑。图中访问到的节点[1,2]就满足这个情况。(在代码实现部分,这个节点需要访问到,但是因为对答案无贡献所以直接return)




代码实现 - 以点修改/区间求和为例


  • 节点类型的构造 struct node

typedef long long ll;
const int MAXN=1e5+50;

struct node
{
    int l,r;
    ll sum;
}tree[MAXN*4];



?

  • 建树 buildTree

int ar[MAXN];
scanf("%d",&n);
for(i=1;i<=n;i++)
    scanf("%d",&ar[i]);
buildTree(1,1,n);
void buildTree(int id,int l,int r)
{
    tree[id].l=l;
    tree[id].r=r;
    tree[id].sum=0;
    if(l==r)
        tree[id].sum=ar[l];
    else
    {
        int mid=(l+r)>>1;
        buildTree(id<<1,l,mid);
        buildTree(id<<1|1,mid+1,r);
        push_up(id);
    }
}

? 按照递归的方法进行建树,main函数读入原数组ar,然后树从编号1开始创建

? 建树函数传入共三个变量:当前要进行构造的节点id,这个节点代表的区间 l 与 r

? 因为调用buildTree函数也就相当于tree结构体数组的初始化,所以要记得加tree[id].sum=0这一句

? 如果当前节点就是叶子节点(即表示的区间的 l 与 r 相同),则直接把sum赋值成对应的ar数组内的值即可

? 如果是非叶子节点,先获得当前节点表示的区间中间值mid=(l+r)>>1

? 然后递归左儿子与右儿子建树

? 这里写法运用了位运算(细微省时)

? id<<1 **就是把id的二进制全部左移一位,相当于id*2**

? id<<1|1 **就是先把id的二进制全部左移一位,再对1做取或运算,相当于id*2+1**

? 当左儿子与有儿子建树完成时,再更新当前节点的sum值,即push_up函数



?

  • 更新节点值(向上传递) push_up

void push_up(int id)
{
    tree[id].sum=tree[id<<1].sum+tree[id<<1|1].sum;
}

? 更新节点编号id的sum值为两子节点sum之和

? 如果子节点发生了改变,则必须更新一遍父节点

? 所以push_up函数会在建树buildTree和修改节点值update时调用



?

  • 修改节点值 update

update(1,3,5);//把位置3的值修改为5
void update(int id,int pos,ll val)
{
    int L=tree[id].l,R=tree[id].r;
    if(L==R&&L==pos){
        tree[id].sum=val;
        return;
    }
    if(L<=pos&&pos<=R)
    {
        int mid=(L+R)>>1;
        update(id<<1,pos,val);
        update(id<<1|1,pos,val);
        push_up(id);
    }
}

? 同样的,update函数需要的三个参数中有一个是迭代参数id,指现在处理的节点id

? 后两个参数固定不变,故也能修改为全局变量

? 如果pos正好位于此时节点id表示的线段中,但是id节点不是叶子节点

? 那么就处理节点id的左右儿子

? 如果一个节点有被访问到,并且左右儿子都已经访问过了

? 说明满足条件的那个子节点的值已经成为更新之后的值

? 此时再更新自己,即push_up(id),然后退出函数,让栈中上一个函数继续执行

? 这里就是查找路径的回溯,先更新叶子节点再一层层回溯父节点并进行更新

? 如果满足条件L==R&&L==pos(或等价条件)时,说明此时id指向的是一个叶子节点,并且就是要修改的节点

? 所以修改后直接return即可,就可以让路径开始回溯,更新父节点

? 回溯到根节点时,说明这一次update操作就完成了



?

  • 查询区间和 query

query(1,3,5);//查询区间[3,5]的和
ll query(int id,int l,int r)
{
    int L=tree[id].l,R=tree[id].r;
    if(l<=L&&R<=r)
        return tree[id].sum;
    int mid=(L+R)>>1;
    ll res=0;
    if(mid>=l)
        res+=query(id<<1,l,r);
    if(mid<r)
        res+=query(id<<1|1,l,r);
    return res;
}

? 与update函数类似的迭代

? 如果满足l<=L&&R<=r,说明当前访问的节点id代表的区间完全包含于查询的区间内

? 此时直接return tree[id].sum返回id节点的sum值即可

? 如果节点id代表的区间只是部分包含于查询的区间内

? 则需要迭代访问两个子节点

? 如果满足mid>=l,说明节点id的左儿子会部分或全部包含于查询的区间[l,r],则可以用迭代左节点得到的值加入答案

? 如果满足mid<r,说明节点id的右儿子会部分或全部包含于查询的区间[l,r],则可以用迭代右节点得到的值加入答案

? 注意这里不能写成mid<=r,因为实际上判断的是mid+1<=r,等价于mid<r

? 最后返回和加入答案即可



?

  • 代码调用部分

    这一部分采用了例题 HDU1166敌兵布阵 的输入要求:

    ? Add a b 使编号为a的元素值加上b

    ? Sub a b 使编号为a的元素值减去b

    ? Query a b 询问区间[a,b]之和

    ? End 结束程序

char opr[10];
while(1)
{
    scanf("%s",opr);
    if(opr[0]=='E')
        break;
    scanf("%d%d",&a,&b);
    if(opr[0]=='Q')
        printf("%d
",query(1,a,b));
    else if(opr[0]=='A')
        update(1,a,b);
    else if(opr[0]=='S')
        update(1,a,-b);
}

?



?

完整程序

#include<iostream>
using namespace std;
typedef long long ll;
const int MAXN=50050;

struct node
{
    int l,r;
    ll sum;
}tree[MAXN*4];

int ar[MAXN];

void push_up(int id)
{
    tree[id].sum=tree[id<<1].sum+tree[id<<1|1].sum;
}//向上更新节点值 - 更新两子节点之和

void buildTree(int id,int l,int r)
{
    tree[id].l=l;
    tree[id].r=r;
    tree[id].sum=0;
    if(l==r)
        tree[id].sum=ar[l];
    else
    {
        int mid=(l+r)>>1;
        buildTree(id<<1,l,mid);
        buildTree(id<<1|1,mid+1,r);
        push_up(id);
    }
}

void update(int id,int pos,ll val)
{
    int L=tree[id].l,R=tree[id].r;
    if(L==R&&L==pos){
        tree[id].sum+=val;
        return;
    }
    if(L<=pos&&pos<=R)
    {
        int mid=(L+R)>>1;
        update(id<<1,pos,val);
        update(id<<1|1,pos,val);
        push_up(id);
    }
}

ll query(int id,int l,int r)
{
    int L=tree[id].l,R=tree[id].r;
    if(l<=L&&R<=r)
        return tree[id].sum;
    int mid=(L+R)>>1;
    ll res=0;
    if(mid>=l)
        res+=query(id<<1,l,r);
    if(mid<r)
        res+=query(id<<1|1,l,r);
    return res;
}

int main()
{
    int T;
    scanf("%d",&T);
    while(T--){
        int i,n;
        scanf("%d",&n);
        for(i=1;i<=n;i++)
            scanf("%d",&ar[i]);
        buildTree(1,1,n);
        char opr[10];
        while(1)
        {
            scanf("%s",opr);
            if(opr[0]=='E')
                break;
            scanf("%d%d",&a,&b);
            if(opr[0]=='Q')
                printf("%d
",query(1,a,b));
            else if(opr[0]=='A')
                update(1,a,b);
            else if(opr[0]=='S')
                update(1,a,-b);
        }
    }
    
    return 0;
}

还有区间修改、求区间最大值,明后天补掉

以上是关于数据结构线段树 (定义 & 点修改/区间查询)的主要内容,如果未能解决你的问题,请参考以下文章

数据结构线段树 (区间修改/区间求和)

区间查改(线段树)

线段树(单点修改+区间查询)&(区间修改+区间查询)

CCF(除法):线段树区间修改(50分)+线段树点修改(100分)+线段树(100分)

P1712 [NOI2016] 区间(线段树&尺取)

bzoj4094 && luogu3097 最优挤奶