->知识点整理-线段树<-
知识点讲解:
首先对于线段树,其实与其他各种树都是一样的,都有着树形的结构。接下来让我们考虑一些问题:
~~给出一个数组,要求满足下面的操作:
~~①给定三个值x,y,z,要求吧【x,y】区间的每个数都加上z。
~~②给定两个值x,y,要求输出【x,y】区间的最大值(or最小值or和)。
如果我们用暴力做这些问题,那么对于操作①和②,每次的复杂度为区间的长度。这种方法在数据比较小的时候可以直接用,在数据比较大的情况下就很难满足时间限制了。这个时候我们就需要用到线段树。
线段树,最主要的点就在于它可以满足对于一个区间的询问以及操作,达到O(log)的时间复杂度,能够更好的满足数据实时更新的题目要求。
知识点实现:
接下来我们就来构建一棵线段树。首先我们需要考虑线段树的每个定点上面保存的值是什么。由于我们查询的时候是直接查询线段树定点的值,所以我们线段树的定点所保存的值就是我们题目中所要求的值(区间最大值or区间最小值or区间和)。然后每次查询只要从根节点开始查找,如果当前查找到的区间在所询问的区间之内,那么就返回当前区间的值,否则就继续向下查找。
接下来我们分别来介绍线段树的每个操作(蒟蒻不怎么会用指针~~见谅):
假定我们有一个7个数的数组:1 5 6 9 2 3 4
那么下面这幅图就是该数组的线段树(区间最小值)表示:
~~①建树(最基本的~)
首先,由于一个节点记录的是一个区间的最小值,而每个节点的值又会受到下面节点的影响,所以我们可以尝试着用递归的方式来写。蒟蒻用的是数组记录(不怎么会指针),以零为起点,所以两个孩子节点就是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||nowr<l)
{
return 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||nowr<l)
{
return 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||nowr<l)return;//当前区间与所查询区间没有交集,返回
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||nowr<l)
{
return 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||nowr<l)return;
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||nowr<l)
{
return 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||nowr<l)return;
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这个操作再进行一波解释。实际上,线段树在执行了区间改变数值之后,仅仅只有执行区间所对应节点的值发生了改变。举个例子,就用我们上面所用的数组,如果计算的是区间的和的话,那么应该是下面这棵线段树:
如果我们进行区间改变值得操作,把1~4的每个点加上4。那么实际上在change操作完成之后,只有区间1~4的这个节点的值变为了47(21+4*4),而这个节点以下的节点的值都没有进行改变。直到我们进行查询操作涉及到了1~4这个区间的时候,我们才会将区间1~4的这个节点的addmark向下进行传递。所以本蒟蒻认为线段树最大的优化便是在这个点上,减少了很多不会对查询有影响的操作,使得算法更加优秀。
不过在比赛的时候其实不怎么推荐用线段树去解题,真的是能不用就不。第一, 线段树的代码量已经有点偏大了,比赛的时候时间本身就比较少。第二,线段树打完之后仍有许多细节需要调节,也会花费大量的时间。第三,线段树的数组要开到原来的2~4倍,所需的空间会比较大。
以上就是本蒟蒻对线段树的初步了解,如果有大佬发现了这篇文章的错误,欢迎在下面指出~~