czl的知识点整理4——线段树
来源:互联网 发布:厦门天心软件 编辑:程序博客网 时间:2024/06/06 21:40
- -知识点整理-线段树-
- 知识点讲解
- 知识点实现
->知识点整理-线段树<-
知识点讲解:
首先对于线段树,其实与其他各种树都是一样的,都有着树形的结构。接下来让我们考虑一些问题:
~~给出一个数组,要求满足下面的操作:
~~①给定三个值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倍,所需的空间会比较大。
以上就是本蒟蒻对线段树的初步了解,如果有大佬发现了这篇文章的错误,欢迎在下面指出~~
- czl的知识点整理4——线段树
- czl的知识点整理1——堆
- czl的知识点整理2——高斯消元
- czl的知识点整理3——LCA
- czl的知识点整理5——单调队列
- czl蒟蒻的模板库5——线段树
- czl蒟蒻的模板库4——Tarjan
- czl蒟蒻的模板库1——Dijkstra
- czl蒟蒻的模板库2——FASTIO
- czl蒟蒻的模板库3——KMP
- 树的知识点整理
- Javase知识点的整理(—)
- czl蒟蒻的模板库6——倍增LCA
- czl蒟蒻的模板库7——最长公共子序列
- czl蒟蒻的模板库8——单调队列
- czl蒻蒟的OI之路4
- 【知识点】 ---线段树的常用操作
- 线段树知识点
- C#EXCEL 操作类
- git命令回顾
- Springboot 添加JdbcTemplates 访问mysql 数据库
- 本地存取bitmap
- UE4 第三人称完全流程
- czl的知识点整理4——线段树
- Angularjs 利用 $modal 实现 confirm 弹窗
- xcode 打印内存中的值
- 零基础无理论实战Mnist手写字库模型训练并输出结果
- 20171106
- 创建类Person和对象
- 组建一台计算机5_硬件5 多位存储器&累加器&初始汇编(1)
- angular中mvvm模式
- HDU3594:Cactus(Tarjan)