zoukankan      html  css  js  c++  java
  • 线段树(内含矩形并)

    常规线段树(非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<<'
    ';
    }
        return 0;
    }
  • 相关阅读:
    django中函数之间的关系
    文件文本的操作
    边框宽度 边框圆角 边框颜色
    nstimer实现倒计时
    用nstimer实现倒计时
    IOS常用宏定义
    计算文字的Size
    判断UITableView滚动是否到底
    设置Label行间距
    UIAlertView 提示弹窗
  • 原文地址:https://www.cnblogs.com/et3-tsy/p/12386701.html
Copyright © 2011-2022 走看看