线段树(内含矩形并)
常规线段树(非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
定义矩形的两条垂直于x轴的边中x坐标较小的为入边,x坐标较大的为出边,入边权值为+1,出边权值为-1,并将所有的线段按照x坐标递增排序,为了等等遍历使用。
存边内容:x,y上端,y下端,权值
STEP3
将所有矩形端点的y坐标进行离散化处理(有些坐标可能很大而且不一定是整数),将原坐标映射成小范围的整数可以作为数组下标更方便计算。如图所示,蓝色数字表示的是离散后的坐标,即1、2、3、4分别对应原先的5、10、23、25。假设离散后的y方向的坐标个数为m,则y方向被分割成m-1个独立单元,下文称这些独立单元为“单位线段”,分别记为<1-2>、<2-3>、❤️-4>。这些线段区间正是我们需要用线段树来维护的,我们需要维护两个值(在后面详细步骤会说)
注意:y的值->端点值,而线段树维护的是面向这些端点之间的区间有效值(下文还会详细谈)
STEP4
线段树中每个节点是面向区间的,并且要开设一个cover一一用于记录当前区段被完全覆盖的次数。
对应每条被扫入的边。
做这一步的目的是为了后面从左往右进行枚举扫描的时候,判断当前单位线段k[i]的矩形面积是否存在,如果说此时k[i]为非假,就说明至少有一条矩形的左边,也就说明它还没有碰到右边,那么显然这块面积是有效的。
STEP5
接下来就是从左到右的扫描了。长的计算就是通过线段树线段有效值的data,宽就是枚举时前后两条线段对应的x之差,有效值乘差,累加即可。
有效性如图:红色、黄色、蓝色三个矩形分别是3对相邻线段间的矩形面积和,其中红色部分的y方向由<1-2>、<2-3>两个“单位线段”组成,黄色部分的y方向由<1-2>、<2-3>、❤️-4>三个“单位线段”组成,蓝色部分的y方向由<2-3>、❤️-4>两个“单位线段”组成。特殊的,在计算蓝色部分的时候,<1-2>部分的权值由于第3条线段的插入(第3条线段权值为-1)而变为零,所以此长度无效。
** 那么我们怎么利用线段树来维护有效值嘞**
我们先看一下下面这幅图
y方向上的有效长度不一定是连续的!
这也就提醒了我们为什么每个线段树的节点要引入这个cover域:
下面谈谈一些具体操作:
1.存储:
int l;//左端点(注意是点!!)
int r;//右端点
double data;//用于记录当前覆盖区间段的有效长度
int cover;//用于记录当前区段被完全覆盖的次数
注意,这次的线段树和之前的线段树稍微有点区别,就是叶子结点的区间端点不再相等,而是相差1,即l+1 == r。因为一个点对于计算有效区间长度来说是没有意义的。
2.沿x轴正方向扫描
我们会有y上下端点对应的位置,利用之前离散化处理过的数组height,使用stl函数lower_bound查询原端点在离散化之后对应的左右区间端编号<a,b>,那么我们要插入这条边,丢到线段树中维护一下,才能知道它右边面积有效的情况。
3.cover,data的更新
这就是类似于查询的过程
修改完后顺带回溯加值
1)if(cover>0)直接计算出有效长度
2)否则左子的data+右子的data
4.遍历&查询
采用后序dfs,先更新子点,后父节点。
板子看下面洛谷P5490
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
洛谷P5490##
#include<iostream>
#include<cstring>
#include<iomanip>
#include<algorithm>
using namespace std;
#define INF 1e10+5
#define MAXN 1000000 + 10
#define MINN -105
typedef long long int LL;
LL ans;
LL n;
LL height[MAXN<<1];
struct Edge
{
LL x;//x轴上位置
LL down;//y下端
LL up;//y上端
int inout;//记录出边入边,入边权值+1,出边权值-1
void format(LL a,LL b,LL c,int d){x=a,up=c,down=b,inout=d;}
bool operator<(const Edge& a)const
{
return x<a.x;
}
};
Edge edge[MAXN<<1];
struct treenode
{
int l,r;;
LL data;//用于记录当前覆盖区间段的有效长度
int cover;//用于记录当前区段被完全覆盖的次数(也就是两个子节点cover的min)
};
treenode segnode[MAXN<<1];
void built(int a,int b,int pos)
{
segnode[pos].l=a,segnode[pos].r=b,segnode[pos].data=0,segnode[pos].cover=0;
if(a==b-1)return;
built(a,(a+b)/2,pos<<1);
built((a+b)/2,b,(pos<<1)|1);
}
void pushup(int x)
{
int l=segnode[x].l,r=segnode[x].r;
if(segnode[x].cover)segnode[x].data=height[r]-height[l];
else segnode[x].data=segnode[x<<1].data+segnode[(x<<1)|1].data;
}
void updata(int a,int b,int renew,int pos)
{
if(a>segnode[pos].r||b<segnode[pos].l)return;
if(a<=segnode[pos].l&&b>=segnode[pos].r)
{
segnode[pos].cover+=renew;
pushup(pos);
return;
}
updata(a,b,renew,pos<<1);
updata(a,b,renew,(pos<<1)|1);
pushup(pos);
}
int main()
{
cin>>n;
ans=0;
for(int i=1;i<=n;i++)
{
LL x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
if(x1>x2)swap(x1,x2);
if(y1>y2)swap(y1,y2);
height[(i<<1)-1]=y1,height[i<<1]=y2;//记录出现过的高,便于等等的离散化查找
edge[(i<<1)-1].format(x1,y1,y2,1);
edge[i<<1].format(x2,y1,y2,-1);//存边
}
sort(edge,edge+2*n+1);//边排序
sort(height+1,height+(n<<1)+1);
int index=unique(height+1,height+(n<<1)+1)-height;//对边进行离散化处理(排序去重)
built(1,index,1);
for(int i=1;i<=(n<<1)-1;i++)//自左往右扫描
{
//查询原来端点在离散化之后的左右区间端
int l=lower_bound(height+1,height+index,edge[i].down)-height;
int r=lower_bound(height+1,height+index,edge[i].up)-height;
//核心:插入新边
updata(l,r,edge[i].inout,1);
ans+=segnode[1].data*(edge[i+1].x-edge[i].x);
}
cout<<ans<<endl;
return 0;
}
HUD1542##
这题让我不得不吐槽一句HDU OJ,一样的代码GCC->ac,C++
->wa,很迷= =
这题就是矩形并的板子啦~
#include<iostream>
#include<cstring>
#include<iomanip>
#include<algorithm>
using namespace std;
#define INF 1e10+5
#define MAXN 25005
#define MINN -105
typedef long long int LL;
double ans;
int n;
double height[MAXN];
struct Edge
{
double x;//x轴上位置
double down;//y下端
double up;//y上端
int inout;//记录出边入边,入边权值+1,出边权值-1
void format(double a,double b,double c,int d){x=a,up=c,down=b,inout=d;}
};
Edge edge[MAXN];
//记录各个边,并且自定义cmp,方便sort,然后沿x递增方向扫描
bool cmp(Edge a,Edge b)
{
if(a.x!=b.x)return a.x<=b.x;
}
struct segnode
{
int l;//较下,这题没用上
int r;//较上,这题没用上
double data;//用于记录当前覆盖区间段的有效长度
int cover;//用于记录当前区段被完全覆盖的次数(也就是两个子节点cover的min)
};
segnode treenode[MAXN];
/*核心代码:用线段树维护某个区间内有效长度,
注意:这里要采用后序遍历!
因为父节点的有效长度data是取决于子节点的data
*/
void updata(int a,int b,int renew,int pos,int l,int r)
{
if(l>=r||a>r||b<l)return;
if(r-l==1)
{
if(a<=l&&b>=r)
{
treenode[pos].cover+=renew;
if(treenode[pos].cover)treenode[pos].data=height[r]-height[l];
else treenode[pos].data=0;
}
return;
}
//这里是后序遍历。因为面向的是区间,而l,r是端点,所以(l+r)>>1的处理与常规线段树不同
updata(a,b,renew,(pos<<1),l,(l+r)/2);
updata(a,b,renew,(pos<<1)|1,(l+r)/2,r);
treenode[pos].data=treenode[pos<<1].data+treenode[(pos<<1)|1].data;
return;
}
int main()
{
int cace=0;
while(scanf("%d", &n) && n)
{
cace++;
ans=0;
memset(height,0,sizeof(height));
for(int i=1;i<MAXN;i++)
treenode[i].cover=0,treenode[i].data=0;
//初始化线段树
for(int i=1;i<=n;i++)
{
double x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
/*
这里默认x1<x2,y1<y2
有些题目可能还要写:
if(x1>x2)swap(x1,x2);
if(y1>y2)swap(y1,y2);
*/
height[(i<<1)-1]=y1,height[i<<1]=y2;//记录出现过的高,便于等等的离散化查找
edge[(i<<1)-1].format(x1,y1,y2,1);
edge[i<<1].format(x2,y1,y2,-1);//存边
}
sort(edge,edge+2*n+1,cmp);//边排序
sort(height+1,height+(n<<1)+1);
unique(height+1,height+(n<<1)+1);//对边进行离散化处理(排序去重)
for(int i=1;i<=(n<<1)-1;i++)//自左往右扫描
{
//查询原来端点在离散化之后的左右区间端
int l=lower_bound(height+1,height+(n<<1)+1,edge[i].down)-height;
int r=lower_bound(height+1,height+(n<<1)+1,edge[i].up)-height;
//核心:插入新边
updata(l,r,edge[i].inout,1,1,(n<<1)+1);
ans+=treenode[1].data*(edge[i+1].x-edge[i].x);
}
cout<<"Test case #"<<cace<<endl;
cout<<"Total explored area: "<<fixed<<setprecision(2)<<ans<<endl;
cout<<'\n';
}
return 0;
}