czl的知识点整理4——线段树

  • -知识点整理-线段树-
    • 知识点讲解
    • 知识点实现

->知识点整理-线段树<-

知识点讲解:

首先对于线段树,其实与其他各种树都是一样的,都有着树形的结构。接下来让我们考虑一些问题:
~~给出一个数组,要求满足下面的操作:
~~①给定三个值x,y,z,要求吧【x,y】区间的每个数都加上z。
~~②给定两个值x,y,要求输出【x,y】区间的最大值(or最小值or和)。

如果我们用暴力做这些问题,那么对于操作①和②,每次的复杂度为区间的长度。这种方法在数据比较小的时候可以直接用,在数据比较大的情况下就很难满足时间限制了。这个时候我们就需要用到线段树。

线段树,最主要的点就在于它可以满足对于一个区间的询问以及操作,达到O(log)的时间复杂度,能够更好的满足数据实时更新的题目要求。

知识点实现:

接下来我们就来构建一棵线段树。首先我们需要考虑线段树的每个定点上面保存的值是什么。由于我们查询的时候是直接查询线段树定点的值,所以我们线段树的定点所保存的值就是我们题目中所要求的值(区间最大值or区间最小值or区间和)。然后每次查询只要从根节点开始查找,如果当前查找到的区间在所询问的区间之内,那么就返回当前区间的值,否则就继续向下查找。

接下来我们分别来介绍线段树的每个操作(蒟蒻不怎么会用指针~~见谅):

假定我们有一个7个数的数组:1 5 6 9 2 3 4

那么下面这幅图就是该数组的线段树(区间最小值)表示:


czl的知识点整理4——线段树_第1张图片

~~①建树(最基本的~)

首先,由于一个节点记录的是一个区间的最小值,而每个节点的值又会受到下面节点的影响,所以我们可以尝试着用递归的方式来写。蒟蒻用的是数组记录(不怎么会指针),以零为起点,所以两个孩子节点就是pos*2+1和pos*2+2。我们定义如下的结构体:

struct node
{
    int val;
}no[maxn*4];//这里是很多新手会犯的错误之一,因为线段树是二叉树,但在最后一层,有很多的子节点是递归所要用到的,但是其本身却没有存在的意义,所以线段树的数组大小一般要开大到原来的2~4倍左右

当递归到区间的长度只有1的时候,那么这个节点的值就是数组中对应的值,然后回溯回去,比较得出每个非叶子节点的值。

void build(int pos,int l,int r)
{
    no[pos].addmark=0;
    if(l==r)    //当前区间的长度位1,对当前节点赋值
    {
        no[pos].val=a[l];
        return ;    //进行回溯
    }
    else 
    {
        int mid=(l+r)>>1;
        build(pos*2+1,l,mid);
        build(pos*2+2,mid+1,r);    //递归进行建树
        no[pos].val=min(no[pos*2+1].val,no[pos*2+2].val);    //回溯之后,当前节点的值等于其两个子节点的值的最小值。
    }
}

~~②查询某个区间的给定值(最大值or最小值or和)

在上面我们已经提到过查询区间值的方法了,从根节点开始搜索:
~(1)如果当前搜索到的节点所表示的区间不在所询问的区间中,那么就返回,返回的值根据查询的值来定,如果查询最大值,那就返回-INF,如果查询最小值,那就返回INF,如果查询和,那就返回0。
~(2)如果当前搜索到的节点所表示的区间包含在所询问的区间中,那么就直接返回该区间的值。
~(3)如果当前搜索到的节点所表示的区间与所询问的区间有交集,那么就继续向其子节点搜索.

int find(int pos,int nowl,int nowr,int l,int r)
{
    if(nowl>r||nowrreturn 0x3f3f3f3f;
    }
    if(nowl>=l&&nowr<=r)
    {
        return no[pos].val;
    }
    int mid=(nowl+nowr)>>1;
    return min(find(pos*2+1,nowl,mid,l,r),find(pos*2+2,mid+1,nowr,l,r));
}

到现在来看,如果线段树只有这些,那么还算是比较简单的了,但是线段树核心的优化就在于其修改的操作,大大降低了线段树的总复杂度。

~~③对某一区间的值进行修改

到这一步,我们就要引进一个新的参数:addmark。把这个参数作为延时标记,具体的作用在下面进行重点讲解。

~(1)定义的结构体进行改变:

struct node
{
    int val;
    int addmark;    //延时标记(手动高亮)
}no[maxn];

~(2)建树的时候顺便进行初始化,其余没变化:

void build(int pos,int l,int r)
{
    no[pos].addmark=0;//初始化(手动高亮)
    if(l==r)
    {
        no[pos].val=a[l];
        return ;
    }
    else 
    {
        int mid=(l+r)/2;
        build(pos*2+1,l,mid);
        build(pos*2+2,mid+1,r);
        no[pos].val=min(no[pos*2+1].val,no[pos*2+2].val);
    }
}

~(3)新加入一个操作:push_down,目的在于把延时标记addmark向节点下方传递。

void push_down(int now)
{
    if(no[now].addmark!=0)
    {
        no[now*2+1].addmark+=no[now].addmark;
        no[now*2+2].addmark+=no[now].addmark;//向节点下方传递(手动高亮)
        no[now*2+1].val+=no[now].addmark;
        no[now*2+2].val+=no[now].addmark;//节点下方的两个子节点的值进行更改(手动高亮)
        no[now].addmark=0;//当前节点的延时标记addmark清零(手动高亮)
    }
}

~(4)查询的同时进行push_down操作,实时更新数组数据。(本蒟蒻认为线段树最为优秀也是最核心的一个点,最后会进行专门的讲解)

int find(int pos,int nowl,int nowr,int l,int r)
{
    if(nowl>r||nowrreturn 0x3f3f3f3f;
    }
    if(nowl>=l&&nowr<=r)
    {
        return no[pos].val;
    }
    push_down(pos);//查询时向下传递addmark(手动高亮)
    int mid=(nowl+nowr)/2;
    return min(find(pos*2+1,nowl,mid,l,r),find(pos*2+2,mid+1,nowr,l,r));
}

~(5)改变区间的值,同时也向下传递markdown(运用递归)

void change(int pos,int nowl,int nowr,int l,int r,int addval)
{
    if(nowl>r||nowrreturn;//当前区间与所查询区间没有交集,返回
    if (nowl>=l&&nowr<=r)//包含在查询区间中,把当前节点的值加上addmark,并且把当前节点的addmark加上所需要加的值
    {
        no[pos].addmark+=addval;
        no[pos].val+=addval;
        return ;
    }
    push_down(pos);//向下传递addmark
    int mid=(nowl+nowr)/2;
    change(pos*2+1,nowl,mid,l,r,addval);
    change(pos*2+2,mid+1,nowr,l,r,addval);//递归进行区间改变值
    no[pos].val=min(no[pos*2+1].val,no[pos*2+2].val);//回溯后改变当前节点的值
}

放出完整的代码:

区间最小值:

struct node
{
    int val;
    int addmark;
}no[maxn];

int a[maxn];

void build(int pos,int l,int r)
{
    no[pos].addmark=0;
    if(l==r)
    {
        no[pos].val=a[l];
        return ;
    }
    else 
    {
        int mid=(l+r)/2;
        build(pos*2+1,l,mid);
        build(pos*2+2,mid+1,r);
        no[pos].val=min(no[pos*2+1].val,no[pos*2+2].val);
    }
}

void push_down(int now)
{
    if(no[now].addmark!=0)
    {
        no[now*2+1].addmark+=no[now].addmark;
        no[now*2+2].addmark+=no[now].addmark;
        no[now*2+1].val+=no[now].addmark;
        no[now*2+2].val+=no[now].addmark;
        no[now].addmark=0;
    }
}

int find(int pos,int nowl,int nowr,int l,int r)
{
    if(nowl>r||nowrreturn 0x3f3f3f3f;
    }
    if(nowl>=l&&nowr<=r)
    {
        return no[pos].val;
    }
    push_down(pos);
    int mid=(nowl+nowr)/2;
    return min(find(pos*2+1,nowl,mid,l,r),find(pos*2+2,mid+1,nowr,l,r));
}

void change(int pos,int nowl,int nowr,int l,int r,int addval)
{
    if(nowl>r||nowrreturn;
    if (nowl>=l&&nowr<=r)
    {
        no[pos].addmark+=addval;
        no[pos].val+=addval;
        return ;
    }
    push_down(pos);
    int mid=(nowl+nowr)/2;
    change(pos*2+1,nowl,mid,l,r,addval);
    change(pos*2+2,mid+1,nowr,l,r,addval);
    no[pos].val=min(no[pos*2+1].val,no[pos*2+2].val);
}

区间之和:

typedef long long ll;
struct node
{
    ll val;
    ll addmark;
}no[maxn*4];

ll a[maxn];

void build(int pos,int l,int r)
{
    no[pos].addmark=0;
    if(l==r)
    {
        no[pos].val=a[l];
        return ;
    }
    else 
    {
        int mid=(l+r)/2;
        build(pos*2+1,l,mid);
        build(pos*2+2,mid+1,r);
        no[pos].val=no[pos*2+1].val+no[pos*2+2].val;
    }
}

void push_down(int now,int l,int r)
{
    if(no[now].addmark!=0)
    {
        // printf("left:%d right:%d\n",now*2+1,now*2+2);
        int mid=(l+r)>>1;
        no[now*2+1].addmark+=no[now].addmark;
        no[now*2+2].addmark+=no[now].addmark;
        no[now*2+1].val+=no[now].addmark*(mid-l+1);
        no[now*2+2].val+=no[now].addmark*(r-mid);
        no[now].addmark=0;
    }
}

ll find(int pos,int nowl,int nowr,int l,int r)
{
    if(nowl>r||nowrreturn 0;
    }
    if(nowl>=l&&nowr<=r)
    {
        return no[pos].val;
    }
    push_down(pos,nowl,nowr);
    int mid=(nowl+nowr)/2;
    return find(pos*2+1,nowl,mid,l,r)+find(pos*2+2,mid+1,nowr,l,r);
}

void change(int pos,int nowl,int nowr,int l,int r,ll addval)
{
    // printf("%d %d\n", nowl,nowr);
    if(nowl>r||nowrreturn;
    if (nowl>=l&&nowr<=r)
    {
        no[pos].addmark+=addval;
        no[pos].val+=addval*(nowr-nowl+1);
        return ;
    }
    push_down(pos,nowl,nowr);
    int mid=(nowl+nowr)/2;
    change(pos*2+1,nowl,mid,l,r,addval);
    change(pos*2+2,mid+1,nowr,l,r,addval);
    no[pos].val=no[pos*2+1].val+no[pos*2+2].val;
}

最后对push_down这个操作再进行一波解释。实际上,线段树在执行了区间改变数值之后,仅仅只有执行区间所对应节点的值发生了改变。举个例子,就用我们上面所用的数组,如果计算的是区间的和的话,那么应该是下面这棵线段树:


czl的知识点整理4——线段树_第2张图片

如果我们进行区间改变值得操作,把1~4的每个点加上4。那么实际上在change操作完成之后,只有区间1~4的这个节点的值变为了47(21+4*4),而这个节点以下的节点的值都没有进行改变。直到我们进行查询操作涉及到了1~4这个区间的时候,我们才会将区间1~4的这个节点的addmark向下进行传递。所以本蒟蒻认为线段树最大的优化便是在这个点上,减少了很多不会对查询有影响的操作,使得算法更加优秀。

不过在比赛的时候其实不怎么推荐用线段树去解题,真的是能不用就不。第一, 线段树的代码量已经有点偏大了,比赛的时候时间本身就比较少。第二,线段树打完之后仍有许多细节需要调节,也会花费大量的时间。第三,线段树的数组要开到原来的2~4倍,所需的空间会比较大。

以上就是本蒟蒻对线段树的初步了解,如果有大佬发现了这篇文章的错误,欢迎在下面指出~~

你可能感兴趣的:(知识点整理)