线段树相关(研究总结,线段树)

线段树相关(研究总结,线段树)

线段树是信息学竞赛中的一种常用数据结构,能够很方便的进行区间查找和修改操作。

引入

假设我们现在有一列数,我们需要支持一下操作:

1.修改某个数的值
2.询问一段区间的和

我们很容易想到朴素的做法,用一个数组存下所有的值,如果是修改操作就直接修改,如果是询问就循环统计一遍。但这样效率不高。

或许有人可以想出另外一个算法,就是用前缀和来优化,Sum[i]表示1到i的和,这样方便了询问操作,但对于修改操作,就要修改很多Sum的值,效率同样不高。

怎么办呢?我们可以用线段树解决

线段树

对于下面这个区间
此处输入图片的描述
我们可以把其分成两个
此处输入图片的描述
再对分出的两个区间进行分离操作
像这样重复操作,我们就可以得到一棵线段树
此处输入图片的描述

一些小性质与本文习惯约定

通过观察我们可以发现,如果按从上到下,从左到右的顺序,把每一个区间看作一个点并给其编号,我们可以得出如下规律:

对于每一个节点i,我们假设其代表的区间是[x,y],定义mid=(x+y)/2,那么,它的左区间[x,mid]的编号就是i*2,而它的右区间[mid+1,y]编号就是i*2+1

为了方便叙述,下面我们称i*2为i的左子节点,i*2+1为i的右子节点
另外,本文中约定线段树的节点编号从1开始

线段树的简单操作

现在有了线段树,但它有什么用呢?
先简单的说一下,假设我们现在要查找[x,y]的和,并且我们现在正好在线段树的这个[x,y]区间,那么我们就可以直接返回这个点上的Sum域(当然要提前确定好,这是可以在建树的时候顺带完成的)
比如说,对于下面这个数组:
此处输入图片的描述
我们想建立一棵线段树来处理其任意两点之间的和,那么我们可以这样:
对于每一个线段树的节点,我们维护一个Sum表示当前线段树的节点中覆盖的区间的和,表示出来就是这样:
此处输入图片的描述
而由于我们知道线段树的性质是左儿子是i*2而右儿子是i*2+1,所以我们甚至不需要存下对应的左儿子右儿子编号。
首先我们来讲一下查询操作
在本例中,查询是查询一个区间内的值之和,假设当前我们要查询的区间是[l0,r0],当前我们所在的线段树的节点是now(开始时就是1啦),当前我们所在的线段树节点的左右区间是[l,r],再定义mid=(l+r)/2,也就是中间结点。
我们定义查询的函数是Query(l0,r0,l,r,now)我们会碰到如下的三种情况:
1.查询区间完全在左区间内(我们用透明的框表示当前所在的线段树区间,用蓝色的框表示我们的查询区间,红色的框代表mid所在)
此处输入图片的描述
对于这种情况,我们直接递归地调用查找左子树区间就可以了,即Query(l0,r0,l,mid,now*2)
2.类似的,查询区间完全在右区间内时
此处输入图片的描述
递归调用Query(l0,r0,mid+1,now*2+1)
3.稍微复杂一点的就是这第三种情况,查询区间横跨左右
此处输入图片的描述
这个时候我们要分别对左区间和右区间进行递归查询并累加Query(l0,mid,l,mid,now*2)+Query(mid+1,r0,mid+1,r,now*+1)
其实还有第四种情况,就是l0l,r0r,此时直接返回该区间的Sum值就可以了
总结一下,代码如下:

int Query(int l0,int r0,int l,int r,int now)
{
    if ((l0==l)&&(r0==r))
        return T[now].data;//T就是线段树,data就是值域
    int mid=(l+r)/2;
    if (l0>=mid+1)
    {
        return Query(l0,r0,mid+1,r,now*2+1);
    }
    else
    if (r0<=mid)
    {
        return Query(l0,r0,l,mid,now*2);
    }
    else
    {
        return Query(l0,mid,l,mid,now*2)+Query(mid+1,r0,mid+1,r,now*2+1);
    }
}

然后我们来讲一下修改操作
相对于查询操作,修改操作相对好理解。
同样也是进行递归的操作,若修改的数在左区间,则递归到左子树,否则递归到右子树。
但要注意的是,修改的时候要记得一路修改所有经过的线段树节点的值。

void Updata(int num,int data,int l,int r,int now)//num是我们要修改的数的编号,data是我们要把num修改成什么,l和r分别是当前所在线段树的now节点的左右区间端点
{
    if (l==r)
    {
        T[now].data=data;
        return;
    }
    int mid=(l+r)/2;
    if (num<=mid)
        Updata(num,data,l,mid,now*2);//分别进入左右子树
    else
        Updata(num,data,mid+1,r,now*2+1);
    T[now].data+=data;//注意一路修改
}

在有些题目中,递归调用可能会爆栈,而又因为修改操作的特殊性(它不会涉及到同时操作两个区间),我们可以把递归的方式改成不递归的,,其原理与递归方式一样

void Updata(int num,int data)
{
    int now=1;
    int l=1,r=n;
    do
    {
        int mid=(l+r)/2;
        T[now].data+=data;
        if (l==r)
            break;
        if (num<=mid)
        {
            r=mid;
            now=now*2;
        }
        else
        {
            l=mid+1;
            now=now*2+1;
        }
    }
    while (1);
    return;
}

线段树的其他操作

在了解了线段树的基本操作后,相信读者已经对线段树有了基本的了解。
线段树其实还有很多操作,它们都是建立在对线段树的理解上面的,笔者这里仅列出常用的一种,其它的读者可以在遇到相关题目时自行推导。
在上文中,我们讲到了线段树的修改操作,但准确地说,这是线段树的单源修改操作,如果我们要对数列的一段区间的数进行修改呢?(比如说,我们要使得[x,y]中的每一个数都加上一个输入的数)
一种解决方法就是调用(y-x+1)次单源修改操作,但这样时间复杂度太高。
我们回想一下线段树的原理:它是区间的操作。那我们能否把区间修改与区间操作相结合起来呢?
当然可以。我们在每一个线段树的值域中再引入一个Lazy,或者叫做迟缓标记,在我们上面的例子中(即使得[x,y]中的每一个数都加上一个数),它表示的就是在当前线段树节点所覆盖的每一个点上都加上Lazy。
举个例子,现在我们要在一个[1,10]的数列中,给l0=1,r0=5内的所有数都加上1,那么我们在递归修改的时候,首先进入的是l=1,r=10的区间,然后进入l=1,r=5的区间,这是我们发现l0l且r0r,所以我们直接给该节点上的Laze+=1。
那么对应的,因为我们加入了迟缓标记,我们就要修改一下查询和修改操作。在每一次进入下一层时,首先要下放当前点中的Lazy标记,因为加入迟缓标记后,该层下面的点都是没有修改的,此时我们要向下查询的话就要临时把Lazy标记中的内容向下传递,这就相当于迟缓了修改操作(现在知道为什么叫迟缓标记了吧)
查询操作:

int Query(int l0,int r0,int l,int r,int now);
{
    if ((l0==l)&&(r0==r))//如果符合就直接返回
    {
        return T[now].data+T[now].Lazy*(l-r+1);
    }
    int Lazy=T[now].lazy;//下放Lazy标记
    T[now].data+=Lazy*(l-r+1);
    T[now*2].lazy+=Lazy;
    T[now*2+1].lazy+=Lazy;
    T[now].lazy=0;
    int mid=(l+r)/2;
    if (r0<=mid)//向下递归
        return Query(l0,r0,l,mid,now*2);
    else
    if (l0>=mid+1)
        return Query(l0,r0,mid+1,r,now*2+1);
    else
        return Query(l0,mid,l,mid,now*2)+Query(mid+1,r0,mid+1,r,now*2+1);
}

修改操作:

void Updata(int l0,int r0,int l,int r,int now,int data)
{
    if ((l0==l)&&(r0==r))
    {
        T[now].lazy=data;
        return;
    }
    int Lazy=T[now].lazy;//向下传递Lazy
    T[now].data+=Lazy*(l-r+1);
    T[now*2].lazy+=Lazy;
    T[now*2+1].lazy+=Lazy;
    T[now].lazy=0;
    int mid=(l+r)/2;
    if (r0<=mid)
        Updata(l0,r0,l,mid,now*2,data);
    else
    if (l0>=mid+1)
        Updata(l0,r0,mid+1,r,now*2+1,data);
    else
    {
        Updata(l0,mid,l,mid,now*2,data);
        Updata(mid+1,r0,mid+1,r,now*2+1,data);
    }
    return;
}

好题推荐

请到我的博客右侧线段树分类中查看
欢迎大佬查错,谢谢

posted @ 2017-07-21 14:42  SYCstudio  阅读(573)  评论(0编辑  收藏  举报