常规线段树(非zkw)
部分内容来自:https://blog.csdn.net/WhereIsHeroFrom/article/details/78969718
含义
线段树,是一颗以区段划分为节点的二叉搜索树,查询效率logn,他优于ST表的地方在于,他可以解决动态RMQ问题
换一句话说,他可适用于当子结构的最优性可能发生改变的一类问题中
【例】给定一个n(n <= 100000)个元素的数组A,有m(m <= 100000)个操作,共两种操作:
1、Q a b 询问:表示询问区间[a, b]的元素和;
2、A a b c 更新:表示将区间[a, b]的每个元素加上一个值c;
表示方式
一.指针表示
每个结点可以看成是一个结构体指针,由数据域和指针域组成,其中指针域有两个,分别为左儿子指针和右儿子指针,分别指向左右子树;数据域存储对应数据(区间和,最大值)
二.数组表示
基于数组的静态表示法,需要一个全局的数组,每个结点对应数组中的一个元素,利用下标索引。
基本操作
1.构造
整体思路是二分递归,从区间[1, n]开始拆分,左半区间分配给左子树,右半区间分配给右子树,继续递归构造左右子树。注意回溯的时候传递左右子树的值,更新父节点的数据域。
2.查询
线段树的查询是指查询数组在[x, y]区间的值,同样也是自上而下地递归查询,不过一定要记得传参是五个值树节点位置,查询的左右端,现在的左右端(否则你再搞条件判断麻烦死了)
1.无交集 返回不传值
2.查询的左右段完全包含现在的左右端 返回并传值
3.不符合1,2,那么二分继续向下查找
3.更新
基本与查询无异
但是,为了提高更新的效率,所以每次更新只更新到更新区间完全覆盖线段树结点区间为止,这样就会使得被更新结点的子孙结点的区间得不到需要更新的信息,在下次查询的时候可能会因此忽略一些值。
所以我们有lazy-tag的标记优化(也叫延迟标记)
每一个结点都有一个lazy-tag标记,用来记录部分内容是没有对子节点往下处理过的。
而你一旦经过(查询或更新都算经过)这个含有lazy-tag的节点,就要做pushdown的操作,即释放标签值,左右子节点加上标签值(标签值下移)。
如果你现在是在更新,在像查询那样寻找更新区段完全包含的节点左右区段时,注意是更新完成了当前节点的data域,才给他贴上标签。
那么有些题目需要多个lazytag,那么我们就需要解决这样的标签冲突问题:
1.加法标签和减法标签冲突:可直接混合运算
2.加法标签和覆盖标签冲突:遵循原则先覆盖后加
分析:如果产生标签冲突,那么在产生这种冲突情况的上一步状态必定是
(1)子节点具有加法标记,父节点具有覆盖标记,覆盖标记的下移**
(2)子节点具有覆盖标记,父节点具有加法标记,加法标记的下移**
如果遵循先加后覆盖,那么这个加法tag毫无意义,那么如果遵循先覆盖后加,那么(2)就可以成立,那么(1)为了满足条件,那么我们就需要一个设定,即覆盖标记下移时,要清空子节点的加法tag标记,这样(1)也成立了。
3.加法标签和乘法标签冲突:遵循原则先乘后加(分析详见下面对洛谷P3373的分析)
但反正核心思路就是
看看产生冲突困境的前一步情况是哪几种,模拟一下有什么处理方式以及应用什么样的优先原则,能够使得这种优先级体现在tag值本身上。
经典考察方式
1.区间求和
2.区间求最大值
3.区间查询特征对象个数
4.区间染色
【例】给定一个长度为n(n <= 100000)的木板,支持两种操作:
1、P a b c 将[a, b]区间段染色成c;
2、Q a b 询问[a, b]区间内有多少种颜色;
保证染色的颜色数少于30种。
其实这与状压dp思维并无二异,都是将某个状态的有无用01表示,在更新父节点无非就是时候用“|”来更新
5.区间k大数
【例】给定n(n <= 100000)个数的数组,然后m(m <= 100000)条询问,询问格式如下:
1、l r k 询问[l, r]的第K大的数的值
线段树的每个结点存的不只是区间端点,而是这个区间内所有的数,并且是按照递增顺序有序排列的,建树过程是一个归并排序的过程,从叶子结点自底向上进行归并。最推荐使用的是泛型容器set,multiset以及algorithm自带的归并算法set_union,不过要记得 set _ union归并的两个集合一定要升序排列,并且你要拿个有分配过内存的常规数组来存储合并结果,再把数组中的数倒回去
6.矩形面积并
【例】给定n(n <= 100000)个平行于XY轴的矩形,求它们的面积并。如图四-4-1所示。
这类二维的问题是用线段树来求解,核心思想是降维,将某一维套用线段树,另外一维则用来枚举。具体过程如下:
STEP1
拆:将所有矩形拆成两条垂直于x轴的线段,平行x轴的边可以舍去(也可以是y),如下图所示。
STEP2
将所有矩形端点的y坐标进行离散化处理(有些坐标可能很大而且不一定是整数),将原坐标映射成小范围的整数可以作为数组下标更方便计算。如图所示,蓝色数字表示的是离散后的坐标,即1、2、3、4分别对应原先的5、10、23、25。假设离散后的y方向的坐标个数为m,则y方向被分割成m-1个独立单元,下文称这些独立单元为“单位线段”,分别记为<1-2>、<2-3>、<3-4>。这些线段正是我们需要用线段树来维护的。
STEP3
定义矩形的两条垂直于x轴的边中x坐标较小的为入边,x坐标较大的为出边,入边权值为+1,出边权值为-1,并将所有的线段按照x坐标递增排序,第i条线段的x坐标记为x[i],这个数组是为了下面从左往右扫时更新各个单位线段对应的k[i]的
STEP4
开一个数组k[m-1],一一对应每一条单位线段,第i条单位线段的记为k[i]如图。
做这一步的目的是为了后面从左往右进行枚举扫描的时候,判断当前单位线段k[i]的矩形面积是否存在,如果说此时k[i]为非假,就说明至少有一条矩形的左边,也就说明它还没有碰到右边,那么显然这块面积是有效的。
那么更新它也就靠的是上文的x[i]
STEP5
接下来就是从左到右的扫描了。长的计算无非就是依靠线段树,宽已定,判断面积是否有效,累加即可。
有效性如图:红色、黄色、蓝色三个矩形分别是3对相邻线段间的矩形面积和,其中红色部分的y方向由<1-2>、<2-3>两个“单位线段”组成,黄色部分的y方向由<1-2>、<2-3>、<3-4>三个“单位线段”组成,蓝色部分的y方向由<2-3>、<3-4>两个“单位线段”组成。特殊的,在计算蓝色部分的时候,<1-2>部分的权值由于第3条线段的插入(第3条线段权值为-1)而变为零,所以此长度无效。
以上所有相邻线段之间的面积和就是最后要求的矩形面积并。
有一个点是很容易被忽视的,原因是如下图所示:
y方向上的矩形不一定是连续的!
下面谈谈一些细节:
1)存边的时候要附带的存+-1,这是为了当你把边从左往右排序完后初始化x[i],并且记录点每个点之间对于的位置dis[i],还有一个值得注意的点,就是可能存在某个矩形的右边和某个矩形的左边刚好重合了,那么你需要x[i],x[i+1]来分别处理他们(因为宽长是0,所以这样处理既可以更新到k[],又可以不计入面积),且一定是-1的那个在前。
2)线段树是以节点下标建立的,那么l==r无实际意义。我们在STEP2那里就就可以在O(n)的时间内递归构造初始的线段树。
3)从左往右进行扫描的时候,为了防止出现上面的绿图的那个情况,那么就要自下而上的去扫一下,如果if(k[up_bound])那么up_bound++,否则用线段树查询下(down_bound,up_bound),
乘上区间宽,累加,然后更新down_bound继续扫...
(板子有空写)
7.矩形周长并
(有空再填坑)
例题
1.洛谷板子P3372
(指针版)
#include<iostream>
#include<cstring>
#include<cstdlib>
using namespace std;
#define INF 1e10+5
#define MAXN 100005
#define MINN -105
typedef long long int LL;
int n,m;
LL sta[MAXN];
LL ans;
struct node
{
node* leftc;//左孩子
node* rightc;//右孩子
node* father;//父节点(常规题可以不用)
LL data;//数据域
int l;//左端
int r;//右端
int lazy;//lazy-tag
//这个构造写的很迷= =
node(int a,LL ll,int rr,int b,node*c=NULL,node*d=NULL,node*e=NULL):
data(a),l(ll),r(rr),lazy(b),father(c),leftc(d),rightc(e){}
};
//树根
node* head=new node(0,0,0,0);
//建树
void built(int l,int r,node* pos,node* fa)
{
pos->father=fa;
pos->l=l;
pos->r=r;
pos->lazy=0;
if(l==r){pos->data=sta[l];return;}
pos->leftc=new node(0,0,0,0);
built(l,(l+r)/2,pos->leftc,pos);
pos->rightc=new node(0,0,0,0);
built((l+r)/2+1,r,pos->rightc,pos);
pos->data=pos->leftc->data+pos->rightc->data;//回溯更新父节点
}
//向下pushdown,释放tag值
void pushdown(node* pos)
{
if(pos->leftc!=NULL)pos->leftc->data+=(pos->leftc->r-pos->leftc->l+1)*pos->lazy,
pos->leftc->lazy+=pos->lazy;
if(pos->rightc!=NULL)pos->rightc->data+=(pos->rightc->r-pos->rightc->l+1)*pos->lazy,
pos->rightc->lazy+=pos->lazy;
pos->lazy=0;
}
//更新
void renew(int l,int r,int tag,node* pos)
{
pushdown(pos);
if(pos->l>r||pos->r<l)return;
if(pos->l>=l&&pos->r<=r)
{
pos->data+=(pos->r-pos->l+1)*tag;
pos->lazy=tag;
return;
}
renew(l,r,tag,pos->leftc);
renew(l,r,tag,pos->rightc);
pos->data=pos->leftc->data+pos->rightc->data;
}
//查询
void check(int l,int r,node* pos)
{
pushdown(pos);
if(pos->l>r||pos->r<l)return;
if(pos->l>=l&&pos->r<=r){ans+=pos->data;return;}
check(l,r,pos->leftc);
check(l,r,pos->rightc);
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>sta[i];
built(1,n,head,NULL);
int p,x,y,k;
for(int i=0;i<m;i++)
{
cin>>p;
if(p==1)
{
cin>>x>>y>>k;
renew(x,y,k,head);
}
else
{
cin>>x>>y;
ans=0;
check(x,y,head);
cout<<ans<<endl;
}
}
return 0;
}
(简化版)
HDU1754
水题,区间求最大值,连续查询,小心毒瘤题干(多组数据...)
洛谷P3373
最为坑爹的地方就是在于它出现了两个更新操作,而且这两个更新操作都是必须使用懒惰标记的,否则会超时,那么我们就要正确处理这两个更新优先级的关系,也就是说到底是先乘后加,还是先加后乘?
那我们不妨先想一想,如果两个标记在同一个节点的时候,那么在产生这种冲突情况的上一步状态必定是
1)子节点具有加法标记,父节点具有乘法标记,乘法标记的下移
2)子节点具有乘法标记,父节点具有加法标记,加法标记的下移
那么对于这两种情况,而我们只能有一种处理方式,能使得从数值本身上能够体现出这种先后。
这个时候我们想起来乘法分配律
( a + b ) * c == a * c + b * c
对于a的值,本来是与b先进行运算的,但是在进行乘法分配了之后,a与c运算优先级更高了,正基于此,我们只要b在向下存储的时候做了*c的处理,每步运算先乘后加即可。
对于数据域的更新也是如此,即先乘后加。
所有点的初始化加法tag为0,乘法tag为1