线段树(内含矩形并)

常规线段树(非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

AC记录 https://www.luogu.com.cn/record/31189187

洛谷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;
}
posted @ 2020-02-29 22:36  et3_tsy  阅读(289)  评论(0编辑  收藏  举报