0x43 线段树

0x43 线段树

线段树(Segment Tree)是一种基于分治思想的二叉树结构,用于在区间进行信息统计。与按照二进制位(2的次幂)进行区间划分的树状数组相比,线段树是一种更加通用的结构:

1.线段树的每个节点都代表一个区间。

2.线段树具有唯一的根节点,代表的区间是整个统计范围,如 [ 1 , N ] [1,N] [1,N]

3.线段树的每个叶节点都代表一个长度为1的元区间 [ x , x ] [x,x] [x,x]

4.对于每个内部节点 [ l , r ] [l,r] [l,r],它的左子节点是 [ l , m i d ] [l,mid] [l,mid],右子节点是 [ m i d + 1 , r ] [mid+1,r] [mid+1,r],其中 m i d = ( l + r ) / 2 mid=(l+r)/2 mid=(l+r)/2(向下取整)

在这里插入图片描述

上图展示了一棵线段树。可以发现,除去数的最后一层,整棵线段树一定是一棵完全二叉树,树的深度为 O ( l o g N ) O(logN) O(logN)。因此,我们可以按照与二叉堆类似的“父子2倍”节点编号方法:

1.根节点编号为1。

2.编号为 x x x的节点的左子节点编号为 x ∗ 2 x*2 x2,左子节点编号为 x ∗ 2 + 1 x*2+1 x2+1

这样一来,我们就能简单的使用一个 s t r u c t struct struct数组来保存线段树。当然树的最后一层节点在数组中保存的位置是不连续的,直接空出数组中多余的位置即可。在理想情况下, N N N个节点的满二叉树有 N + N / 2 + N / 4 + . . . + 2 + 1 = 2 N − 1 N+N/2+N/4+...+2+1=2N-1 N+N/2+N/4+...+2+1=2N1个节点。因为在上述储存方式下,最后还有一层产生了空余,所以保存线段树的数组长度要不小于 4 N 4N 4N才能保证不会越界

线段树的建树

线段树的基本用途是对序列进行维护,支持查询和修改指令。给定一个长度为 N N N的序列 A A A,我们可以在区间 [ 1 , N ] [1,N] [1,N]上建立一棵线段树,每个叶节点 [ i , i ] [i,i] [i,i]保存 A [ i ] A[i] A[i]的值。线段树的二叉树结构可以很方便地从下往上传递信息。以区间最大值问题为例,记 d a t ( l , r ) dat(l,r) dat(l,r)等于 m a x l ≤ i ≤ r { A [ i ] } max_{l\leq i \leq r} \{ A[i]\} maxlir{A[i]},显然 d a t ( l , r ) = m a x ( d a t ( l , m i d ) , d a t ( m i d + 1 , r ) ) dat(l,r)=max(dat(l,mid),dat(mid+1,r)) dat(l,r)=max(dat(l,mid),dat(mid+1,r))

在这里插入图片描述

下面这段代码建立了一棵线段树并在每个节点上保存了对于区间的最大值。

struct SegmentTree{
    int l,r;
    int dat;
}t[SIZE*4];  //struct数组存储线段树

void build(int p,int l,int r)
{
    t[p].l=l,t[p].r=r; //节点p代表区间[l,r]
    if(l==r) //叶节点
    {
        t[p].dat=a[l];
        return;
	}
    int mid=(l+r)/2;
    build(p*2,l,mid);     //左子节点[l,mid],编号p*2
    build(p*2+1,mid+1,r); //右子节点[mid+1,r],编号p*2+1
    t[p].dat=max(t[p*2].dat,t[p*2+1].dat); //从下往上传递信息
}

线段树的单点修改

单点修改是一条形如“C x v”的指令,表示把 A [ x ] A[x] A[x]的值修改为 v v v

在线段树中根节点(编号为1的节点)是执行各种指令的入口。我们需要从根节点出发,递归找到代表区间 [ x , x ] [x,x] [x,x]的叶节点,然后从下往上更新 [ x , x ] [x,x] [x,x]以及它的所有祖先节点上保存的信息,如下图所示。时间复杂度为 O ( l o g N ) O(logN) O(logN)

void change(int p,int x,int v)
{
    if(t[p].l==t[p].r)
    {
        t[p].dat=v;
        return;
	}
    int mid=(t[p].l+t[p].r)/2;
    if(x<=mid)
        change(p*2,x,v);
   	else
        change(p*2+1,x,v);
   	t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
}

在这里插入图片描述

线段树的区间查询

区间查询是一条形如“Q l r”的指令,例如查询序列 A A A在区间 [ l , r ] [l,r] [l,r]上的最大值,即 m a x l ≤ i ≤ r { A [ i ] } max_{l\leq i \leq r} \{A[i]\} maxlir{A[i]}。我们只需要从根节点开始,递归执行一下过程:

1.若 [ l , r ] [l,r] [l,r]完全覆盖了当前节点代表的区间,则立即回溯,并且该节点的 d a t dat dat值为候选答案。

2.若左子节点与 [ l , r ] [l,r] [l,r]有重叠部分,则递归访问左子节点。

3.若右子节点与 [ l , r ] [l,r] [l,r]有重叠部分,则递归访问右子节点。

int ask(int p,int l,int r)
{
    if(l<=t[p].l&&r>=t[p].r)
        return t[p].dat;  //完全包含
    int mid=(t[p].l+t[p].r)/2;
    int val=-(1<<30); //负无穷大
    if(l<=mid)
        val=max(val,ask(p*2,l,r)); //左子节点有重叠
    if(r>mid)
        val=max(val,ask(p*2+1,l,r)); //右子节点有重叠
    return val;
}
cout<<ask(1,l,r)<<endl;  //调用入口

在这里插入图片描述

该查询过程会把询问区间 [ l , r ] [l,r] [l,r]在线段树上分成 O ( l o g N ) O(logN) O(logN)个节点,取它们的最大值作为答案。为什么是 O ( l o g N ) O(logN) O(logN)个呢?在每个节点 [ p l , p r ] [p_l,p_r] [pl,pr]上,设 m i d = ( p l + p r ) / 2 mid=(p_l+p_r)/2 mid=(pl+pr)/2(向下取整),可能会出现一下几种情况:

1. l ≤ p l ≤ p r ≤ r l\leq p_l \leq p_r \leq r lplprr,即完全覆盖了当前节点,直接返回。

2. p l ≤ l ≤ p r ≤ r p_l\leq l \leq p_r \leq r pllprr,即只有 l l l处于节点之内。

(1) l > m i d l>mid l>mid,只会递归右子树。

(2) l ≤ m i d l\leq mid lmid,虽然递归两棵子树,但是右子节点会在递归后直接返回。

3. l ≤ p l ≤ r ≤ p r l \leq p_l \leq r \leq p_r lplrpr,即只有 r r r位于节点之内,与情况2类似。

4. p l ≤ l ≤ r ≤ p r p_l \leq l \leq r \leq p_r pllrpr,即 l l l r r r都位于节点之内。

(1) l , r l,r l,r都位于 m i d mid mid一侧,只会递归一棵子树。

(2) l , r l,r l,r分别位于 m i d mid mid两侧,递归左右两棵子树。

也就是说,只有情况4(2)会真正产生对左右两棵子树的递归。这种情况至多产生一次,之后在子节点上就会变成情况2或3。因此,上述查询过程的时间复杂度为 O ( 2 l o g N ) = O ( l o g N ) O(2logN)=O(logN) O(2logN)=O(logN)

至此,线段树已经能像0x06节的ST算法一样处理区间最值问题,并且还支持动态修改某个数的值。同时,线段树也已经能支持上一节树状数组的单点增加与查询前缀和指令。

1.延迟标记(懒标记)

在线段树的“区间查询“指令中,每当遇到被询问区间 [ l , r ] [l,r] [l,r]完全覆盖的节点时,可以立即把该节点上储存的信息作为候选答案返回。我们已经证明,被询问区间 [ l , r ] [l,r] [l,r]在线段树上会被分成 O ( l o g N ) O(logN) O(logN)个小区间(节点),从而在 O ( l o g N ) O(logN) O(logN)的时间内求出答案。不过,在”区间修改“指令中,如果每个节点被修改区间 [ l , r ] [l,r] [l,r]完全覆盖,那么以该节点为根的整棵子树的所有节点储存的信息都会发生变化,若逐一更新,将使得一次区间修改指令的时间复杂度增加到 O ( N ) O(N) O(N),这是我们不能接受的。

如果我们在一次修改指令中发现节点 p p p代表的区间 [ p l , p r ] [p_l,p_r] [pl,pr]被修改区间 [ l , r ] [l,r] [l,r]完全覆盖,并逐一更新了子树 p p p的所有节点,但是之后的查询指令中却完全没有用到 [ l , r ] [l,r] [l,r]的子区间作为候选答案,那么更新 p p p的整棵子树就是徒劳的。

换言之,我们在执行修改指令时,同样可以在 l ≤ p l ≤ p r ≤ r l\leq p_l\leq p_r\leq r lplprr的情况下立即返回,只不过在回溯之前箱节点 p p p增加一个标记,标识“该节点曾经被修改,但其子节点尚未被更新”。

如果在后续的指令中,需要从节点 p p p向下递归,我们检查 p p p是否具有标记。若有标记,就根据标记信息更新 p p p的两个子节点,同时为 p p p的两个子节点增加标记,然后清除 p p p的标记

也就是说,除了在修改指令中直接划分成的 O ( l o g N ) O(logN) O(logN)个节点之外,对任意节点的修改都延迟到“在后续操作中递归进入它的父节点时”再执行。这样一来,每条查询或修改指令的时间复杂度都降低到了 O ( l o g N ) O(logN) O(logN)。这些标记被称为“延迟标记”。延迟标记提供了线段树从上往下传递信息的方式。这种“延迟”也是设计算法与解决问题的一个重要思路。

上一节0x42节树状数组中,我们解决了数字序列区间增长和区间查询和的问题,我们也可以利用线段树的延迟标记来解决,这里多了一个 s p r e a d spread spread函数实现了延迟标记的向下传递。

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
int N, Q;
int a[100005];
struct SegmentTree
{
    int l, r;
    ll sum, add;
} t[400005];

void build(int p, int l, int r)
{
    t[p].l = l, t[p].r = r;
    if (l == r)
    {
        t[p].sum = a[l];
        return;
    }
    int mid = (l + r) / 2;
    build(p * 2, l, mid);
    build(p * 2 + 1, mid + 1, r);
    t[p].sum = t[p * 2].sum + t[2 * p + 1].sum;
}

void spread(int p)
{
    if (t[p].add)
    {
        t[p * 2].sum += (ll)(t[p * 2].r - t[p * 2].l + 1) * t[p].add;
        t[p * 2].add += t[p].add;
        t[p * 2 + 1].sum += (ll)(t[p * 2 + 1].r - t[p * 2 + 1].l + 1) * t[p].add;
        t[p * 2 + 1].add += t[p].add;
        t[p].add = 0;
    }
}

void change(int p, int l, int r, int d)
{
    if (t[p].l >= l && t[p].r <= r)
    {
        t[p].sum += (ll)(t[p].r - t[p].l + 1) * d;
        t[p].add += d;
        return;
    }
    spread(p);
    int mid = (t[p].l + t[p].r) / 2;
    if (l <= mid)
        change(p * 2, l, r, d);
    if (r > mid)
        change(p * 2 + 1, l, r, d);
    t[p].sum = t[p * 2].sum + t[p * 2 + 1].sum;
}

ll ask(int p, int l, int r)
{
    if (t[p].l >= l && t[p].r <= r)
        return t[p].sum;
    spread(p);
    int mid = (t[p].l + t[p].r) / 2;
    ll ans = 0;
    if (l <= mid)
        ans += ask(p * 2, l, r);
    if (r > mid)
        ans += ask(p * 2 + 1, l, r);
    return ans;
}

int main()
{
    scanf("%d%d", &N, &Q);
    for (int i = 1; i <= N; ++i)
        scanf("%d", &a[i]);
    build(1, 1, N);
    char op;
    int a, b, c;
    while (Q--)
    {
        cin >> op;
        if (op == 'Q')
        {
            scanf("%d%d", &a, &b);
            printf("%lld\n", ask(1, a, b));
        }
        else
        {
            scanf("%d%d%d", &a, &b, &c);
            change(1, a, b, c);
        }
    }
    return 0;
}

需要指出的是延迟标记的含义是“该节点曾经被修改,但其子节点尚未被更新”,即延迟标记标识的是子节点等待更新的情况。因此,一个子节点被打上“延迟标记”的同时,它保存的信息应该被修改完毕。在编写代码时,一定要注意“更新信息”与“打标记”之间的关系,避免出现错误。

2.扫描线

给定平面直角坐标系中的 N N N个矩形,求它们的面积并,即这些矩形的并集在坐标系中覆盖的总面积,如下图所示。

在这里插入图片描述

如果我们用一根竖直直线从左往右扫过整个坐标系,那么直线上被并集图形覆盖的长度只会在每个矩形的左右边界处发生变化。

换言之,整个并集图形可以被分成 2 ∗ N 2*N 2N段,每一段在直线上覆盖的长度(记为 L L L)是固定的,因此该段的面积就是 L ∗ L* L该段的宽度,各段面积之和即为所求。这条直线就称为扫描线,这种解题思路被称为扫描线法。

具体来说,我们可以取出 N N N个矩形的左右边界。若一个矩形的两个对角顶点坐标为 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) ( x 2 , y 2 ) (x_2,y_2) (x2,y2),不妨设 x 1 < x 2 , y 1 < y 2 x_1<x_2,y_1<y_2 x1<x2,y1<y2,则左边界记为四元组 ( x 1 , y 1 , y 2 , 1 ) (x_1,y_1,y_2,1) (x1,y1,y2,1),右边界记为四元组 ( x 2 , y 1 , y 2 , − 1 ) (x_2,y_1,y_2,-1) (x2,y1,y2,1)。把这 2 N 2N 2N个四元组按照 x x x递增排序,如下图所示。

在这里插入图片描述

注意到本题中的 y y y坐标范围较大且不一定是整数,我们先把输入的数据中出现的所有 y y y坐标放入一个数组,排序、去重,完成离散化。设 v a l ( y ) val(y) val(y)表示 y y y被离散化之后映射到的整数值, r a w ( i ) raw(i) raw(i)表示整数值 i i i对应的原始 y y y坐标值。

在离散化后,若有 M M M个不同的 y y y坐标值,分别对应 r a w ( 1 ) , r a w ( 2 ) , . . . , r a w ( M ) raw(1),raw(2),...,raw(M) raw(1),raw(2),...,raw(M),则扫描线至多被分成 M − 1 M-1 M1段,其中 i i i段为区间 [ r a w ( i ) , r a w ( i + 1 ) ] [raw(i),raw(i+1)] [raw(i),raw(i+1)]。建立数组 c c c,用 c [ i ] c[i] c[i]记录扫描线上第 i i i段被覆盖的次数。起初 c c c数组中的元素全为0。

逐一扫描排序后的 2 N 2N 2N个四元组,设当前四元组为 ( x , y 1 , y 2 , k ) (x,y_1,y_2,k) (x,y1,y2,k)。我们把数组 c c c c [ v a l ( y 1 ) ] , c [ v a l ( y 1 ) + 1 ] , . . . , c [ v a l ( y 2 ) − 1 ] c[val(y_1)],c[val(y_1)+1],...,c[val(y_2)-1] c[val(y1)],c[val(y1)+1],...,c[val(y2)1]把这些值都加上 k k k,相当于覆盖了 [ y 1 , y 2 ] [y_1,y_2] [y1,y2]这个区间。此时,如果下一个四元组的横坐标为 x 2 x_2 x2,则扫描线从 x x x扫到 x 2 x_2 x2的过程中,被覆盖的长度就固定为 ∑ c [ i ] > 0 ( r a w ( i + 1 ) − r a w ( i ) ) \sum_{c[i]>0} (raw(i+1)-raw(i)) c[i]>0(raw(i+1)raw(i)),即数组 c c c中至少被覆盖一次的“段”的总长度。于是,我们就让最终的答案 a n s ans ans累加上 ( x 2 − x ) ∗ ∑ c [ i ] > 0 ( r a w ( i + 1 ) − r a w ( i ) ) (x_2-x)*\sum_{c[i]>0} (raw(i+1)-raw(i)) (x2x)c[i]>0(raw(i+1)raw(i))

对于每个四元组,采用朴素算法在 c c c数组上执行修改与统计,即可在 O ( N 2 ) O(N^2) O(N2)的时间内求出并集图形的面积。

我们可以用线段树维护 c c c数组,把算法优化到 O ( N l o g N ) O(NlogN) O(NlogN)

本题中,我们只关心整个扫描线(线段树根节点)上被矩形覆盖的长度。而且,因为四元组 ( x , y 1 , y 2 , 1 ) (x,y_1,y_2,1) (x,y1,y2,1) ( x , y 1 , y 2 , − 1 ) (x,y_1,y_2,-1) (x,y1,y2,1)成对出现,所以线段树区间修改也是成对出现的。在这种特殊情形下,我们没有必要下传延迟标记,可以采用更为简单的做法。

除左右端点 l , r l,r l,r之外,在线段树的每个节点上维护两个值:该节点代表的区间被矩形覆盖的长度 l e n len len,该节点自身被覆盖的次数 c n t cnt cnt。最初,两者均为0。

对于一个四元组 ( x , y 1 , y 2 , k ) (x,y_1,y_2,k) (x,y1,y2,k),我们在 [ v a l ( y 1 ) , v a l ( y 2 ) − 1 ] [val(y_1),val(y_2)-1] [val(y1),val(y2)1]上执行区间修改。该区间被线段树划分成 O ( l o g N ) O(logN) O(logN)个节点,我们把这些节点的 c n t cnt cnt都加 k k k

对于线段树中任意一个节点 [ l , r ] [l,r] [l,r],若 c n t > 0 cnt>0 cnt>0,则 l e n len len等于 r a w ( r + 1 ) − r a w ( l ) raw(r+1)-raw(l) raw(r+1)raw(l)。否则,该点 l e n len len等于两个子节点的 l e n len len之和。在一个节点的 c n t cnt cnt值被修改,以及线段树从下往上传递信息时,我们都按照该方法更新 l e n len len值。根节点的 l e n len len值就是整个扫描线上被覆盖的长度。

因为在扫入四元组 ( x , y 1 , y 2 , 1 ) (x,y_1,y_2,1) (x,y1,y2,1)后进行区间修改 c h a n g e change change操作,被分成的 l o g N logN logN个区间 c n t cnt cnt值加1,如果没有扫入四元组 ( x , y 1 , y 2 , − 1 ) (x,y_1,y_2,-1) (x,y1,y2,1)则这被分成的 l o g N logN logN个区间 c n t cnt cnt值不可能为0(就算扫入其他出边,之前也扫入了对应的入边,所以这被分成的 l o g N logN logN个区间 c n t cnt cnt值不可能为0),只有当扫入对应的四元组 ( x , y 1 , y 2 , − 1 ) (x,y_1,y_2,-1) (x,y1,y2,1),这被分成的 l o g N logN logN个区间 c n t cnt cnt值才可能为0。所以按照上述方法修改,而不使用延迟修改是行得通的。

#include <iostream>
#include <stdio.h>
#include <algorithm>
using namespace std;

#define l(p) t[p].l
#define r(p) t[p].r
#define ls (p<<1)
#define rs (p<<1|1)
const int SIZE=1e5;
int N,M,now;
double a[2*SIZE],b[2*SIZE];
struct Line{
    double x,y1,y2;
    int k;
    bool operator<(const Line b)const{
        return x<b.x;
    }
}line[2*SIZE];
struct SegmentTree{
    int l,r;
    int cnt;
    double len;
}t[8*SIZE];

void build(int p,int l,int r)
{
    l(p)=l,r(p)=r;
    if(l==r)
    {
        t[p].cnt=0;
        t[p].len=0;
        return;
    }
    int mid=(l+r)/2;
    build(ls,l,mid);
    build(rs,mid+1,r);
    t[p].cnt=0;
    t[p].len=0;
}

void pushup(int p)
{
    if(t[p].cnt>0)
        t[p].len=b[r(p)+1]-b[l(p)];
    else
    {
        if(l(p)!=r(p))
            t[p].len=t[ls].len+t[rs].len;
        else
            t[p].len=0;
    }
}

void change(int p,int l,int r,int k)
{
    if(l(p)>=l&&r(p)<=r)
    {
        t[p].cnt+=k;
        pushup(p);
        return;
    }
    int mid=(l(p)+r(p))/2;
    if(l<=mid)
        change(ls,l,r,k);
    if(r>mid)
        change(rs,l,r,k);
    pushup(p);
}

int main()
{
    while(scanf("%d",&N)&&N)
    {
        now++;
        M=0;
        double x1,y1,x2,y2;
        for(int i=1;i<=N;++i)
        {
            scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);
            line[i*2-1].x=x1,line[i*2-1].y1=y1,line[i*2-1].y2=y2,line[i*2-1].k=1;
            line[i*2].x=x2,line[i*2].y1=y1,line[i*2].y2=y2,line[i*2].k=-1;
            a[i*2-1]=y1;
            a[i*2]=y2;
        }
        N<<=1;
        sort(line+1,line+N+1);
        sort(a+1,a+N+1);
        for(int i=1;i<=N;++i)
        {
            if(i==1||a[i]!=a[i-1])
                b[++M]=a[i];
        }
        build(1,1,M-1);
        double ans=0;
        for(int i=1;i<N;++i)
        {
            int id1=lower_bound(b+1,b+M+1,line[i].y1)-b;
            int id2=lower_bound(b+1,b+M+1,line[i].y2)-b;
            change(1,id1,id2-1,line[i].k);
            ans+=(line[i+1].x-line[i].x)*t[1].len;
        }
        printf("Test case #%d\nTotal explored area: %.2f\n\n",now,ans);
    }

    return 0;
}

注意若数据有 N N N组,那记录扫描线有 2 N 2N 2N组,则线段树大小至少开到 8 N 8N 8N。注意在 p u s h u p pushup pushup操作中,我们要注意叶节点的判断,例如在 [ 1 , 10 ] [1,10] [1,10]建线段树,叶节点区间 [ 7 , 7 ] [7,7] [7,7]编号为25,如果不判断叶节点,那他的 p u s h u p pushup pushup可能会用到50和51号节点,超过了40(超过了 4 ∗ s i z e 4*size 4size),如果不想判断叶节点则线段树开到80的大小,即开到 16 N 16N 16N 8 ∗ s i z e 8*size 8size)。但要注意,如果有多组测试数据,会重复使用覆盖同一个线段树,则想要使用 16 N 16N 16N 8 ∗ s i z e 8*size 8size),需要在使用下一组数据前将整个线段树重置。

3.动态开点和线段树合并

在一些计数问题中,线段树用于维护值域(一段权值范围),这样的线段树也称为权值线段树。为了降低空间复杂度,我们可以不建出整棵线段树的结构,而是在最初只建立一个根节点,代表整个区间,当需要访问线段树的某棵子树(某个区间)时,再建立代表这个子区间的节点。采用这种方法维护的线段树称为动态开点的线段树。动态开点的线段树抛弃了完全二叉树父子节点的2倍编号规则,改为使用变量记录左右子节点的编号(相当于指针)。同时,它也不再保存每个节点代表的区间,而是在每次递归访问的过程中作为参数传递。下面是一个动态开点的线段树的节点结构。

struct SegmentTree{
    int lc,rc; //左右子节点的编号
    int dat; //区间最大值
}tr[SIZE*2];

int build() //新建一个节点
{
    tot++;
    tr[tot].lc=tr[tot].rc=tr[tot].dat=0;
    return tot;
}

//在main函数中
tot=0;
root=build(); //根节点

下面的代码对线段树单点修改的过程稍加变动,实现了在动态开点的线段树中把 v a l val val位置上值加 d e l t a delta delta,同时维护区间最大值的操作。

void insert(int p,int l,int r,int val,int delta)
{
    if(l==r)
    {
        tr[p].dat+=delta;
        return;
	}
    int mid=(l+r)>>1;
    if(val<=mid)
    {
        if(!tr[p].lc)
            tr[p].lc=build(); //动态开点
        insert(tr[p].lc,l,mid,val,delta);
    }
    else
    {
        if(!tr[p].rc)
            tr[p].rc=build(); //动态开点
        insert(tr[p].rc,mid+1,r,val,delta);
    }
    tr[p].dat=max(tr[tr[p].lc].dat,tr[tr[p].rc].dat);
}
insert(root,1,n,val,delta); //调用

常规线段树的其他操作也可以通过类似的变动,在动态开点的线段树上实现,这里就不在赘述。一棵维护值域 [ 1 , n ] [1,n] [1,n]的动态开点的线段树在经历 m m m次单点操作后,节点数量的规模为 O ( m l o g n ) O(mlogn) O(mlogn),最终至多有 2 n − 1 2n-1 2n1个节点。

如果有若干颗线段树,它们都维护相同的值域 [ 1 , n ] [1,n] [1,n],那么它们对各个子区间的规划显然是一致的。假如有 m m m次单点修改操作,每次操作在某一棵线段树上执行。所有操作完成后,我们希望把这些线段树对应位置上的值相加,同时维护区间的最大值。

该问题可以通过线段树合并算法实现。我们依次合并这些线段树。合并两颗线段树时,用两个指针 p , q p,q p,q从两个根节点出发,以递归的方式同步遍历两颗线段树。换句话说, p , q p,q p,q指向的节点总是代表相同的子区间。

1.若 p , q p,q p,q之一为空,则以非空的那个作为合并后的节点。

2.若 p , q p,q p,q均不为空,则递归合并两颗左子树和两颗右子树,然后删除节点 q q q,以 p p p为合并后的节点,自底向上更新最值信息。若已到达叶子节点,则直接把两个最值相加即可。

int merge(int p,int q,int l,int r)
{
    if(!p) return q;
    if(!q) return p;
    if(l==r)
    {
        tr[p].dat+=tr[q].dat;
        return p;
	}
    int mid=(l+r)>>1;
    tr[p].lc=merge(tr[p].lc,tr[q].lc,l,mid);
    tr[p].rc=merge(tr[p].rc,tr[q].rc,mid+1,r);
    tr[p].dat=max(tr[tr[p].lc].dat,tr[tr[p].rc].dat);
    return p; //以p作为合并后的节点,相当于删除q
}

仔细观察可以发现,若线段树合并过程中发生递归,则必定会导致 p , q p,q p,q之一被删除。因此,在完成所有线段树的合并后, m e r g e merge merge函数被执行的次数不会超过所有线段树的节点总数加一。故这个合并过程的时间复杂度为 O ( m l o g n ) O(mlogn) O(mlogn),与完成所有单点修改操作的时间复杂度相等,是一个很高效的过程。

我们会在0x63节结合具体例题,探讨线段树合并的应用并完成代码实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/261088.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

探讨小鹏汽车CAN通讯协议分析破解过程数据研究技术应用

当前新能源电动汽车设计日益复杂&#xff0c;为提高舒适性、功能性、提升性能和确保更高的安全性&#xff0c;很多汽车的设计中融入了更复杂的功能。包括了雷达、激光雷达、自适应巡航、L2以上自动驾驶系统&#xff0c;高级驾驶辅助系统、盲区监测等等。安装在汽车上的传感器和…

在vue中获取文件的Md5值,以上传图片与视频为例

在vue中获取文件的Md5值 1. Md5 是什么&#xff1f;2. 使用插件spark-md5处理3. 获取图片文件的Md5值4. 视频文件的Md5值获取 1. Md5 是什么&#xff1f; MD5信息摘要算法&#xff08;英语&#xff1a;MD5 Message-Digest Algorithm&#xff09;&#xff0c;一种被广泛使用的…

【智慧校园】基于国标GB28181协议EasyCVR视频技术的高校宿舍智能监管方案

现如今&#xff0c;各大学校不乏众多住校生&#xff0c;但由于很多学生年龄较小 &#xff0c;又缺乏独自生活的经历&#xff0c;如何给在校住宿生做到安全与生活双重保障&#xff1f;旭帆科技校园智能视频监控通过人工智能技术对住宿区域进行智能监管&#xff0c;确保学生住宿安…

自定义权限管理系统概述

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 今天我们来聊聊如何自定…

Markdown语法 in Typora

Typora 是个好东西&#xff0c;如果不收费的话就更好了&#xff1b; Typora 破解 其实一直点击15天试用也是可以一直用一直用的&#xff1b; 数学公式 在Markdown扩展语法这里要选一下&#xff0c;才可以使用行内数学公式&#xff1b; 行内公式 $f(x) 2x^25x3$&#xff…

500平左右需要用建筑模板多少张?

为了计算500平方米&#xff08;㎡&#xff09;的建筑面积需要多少张模板&#xff0c;我们首先需要知道每张模板的面积。你提供了两种尺寸的模板&#xff1a;915毫米 x 1830毫米 和 1220毫米 x 2440毫米。我们先将这些尺寸从毫米转换为米&#xff0c;然后计算每张模板的面积&…

springboot整合rabbitmq附源码

前提是对rabbitmq有一定的了解&#xff0c;比如虚拟主机&#xff0c;交换机&#xff0c;队列&#xff0c;信道&#xff0c;绑定&#xff0c;路由键&#xff0c;direct&#xff0c;fanout&#xff0c;topic等 我使用的是docker部署的rabbitmq&#xff0c;看到简书的这个&#x…

如何通过宝塔面板搭建一个MySQL数据库服务并实现无公网ip远程访问?

文章目录 前言1.Mysql服务安装2.创建数据库3.安装cpolar3.2 创建HTTP隧道 4.远程连接5.固定TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址 前言 宝塔面板的简易操作性,使得运维难度降低,简化了Linux命令行进行繁琐的配置,下面简单几步,通过宝塔面板cp…

WEB渗透—PHP反序列化(五)

Web渗透—PHP反序列化 课程学习分享&#xff08;课程非本人制作&#xff0c;仅提供学习分享&#xff09; 靶场下载地址&#xff1a;GitHub - mcc0624/php_ser_Class: php反序列化靶场课程&#xff0c;基于课程制作的靶场 课程地址&#xff1a;PHP反序列化漏洞学习_哔哩…

69 排序高手

数据以字符串形式输入&#xff0c;期间转到数组内 #include <iostream> #include <string> #include <vector>using namespace::std; using std::cout; using std::cin; int pxgs(vector<int>& nums) {int nnums.size();int l0,rn-1;while(l<…

博客3万访问量了……

博客有3万访问量了呢。自从第一次用了赠送的1500的流量券&#xff0c;粉丝了从零突破了&#xff0c;到现在有150个粉丝了。 之前预想的写博客的初衷&#xff0c;也是记录自己的学习过程&#xff0c;毕竟好记忆不如烂笔头&#xff0c;记录下来就是长长久久的&#xff0c;随时可以…

Java-Security-1

JWT详解​​​​​​​ 1、SpringSecurity 1.1 简介 Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架 Shiro &#xff0c;它提供了更丰富的功能&#xff0c;社区资源也比 Shiro 丰富。 一般来说中大型的项目都是使用 SpringSecurity 来做安全框…

金蝶EAS打印凭证,数据量多点的就会出错

金蝶EAS打印凭证&#xff0c;数据量多点的就会出错&#xff0c;约过100页&#xff0c;提示数据源有问题 经咨询工程师需修改java虚拟机内存。 打开eas客户端目录&#xff0c;运行set-url.bat 看到原来java虚拟机只配置了512M内存&#xff0c;把虚拟机内存修改为4096&#xff0…

基于SSM的学生在线考试系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

sql学习笔记(四)

1. 避免字段名与关键字冲突 1️⃣ 2️⃣ as 设置别名 2. 同比与环比 「同比」&#xff1a;与历史「同时期&#xff3d;比较&#xff0c;例如2011年3月份与2010年3月份相比&#xff0c;叫「同比」。 「环比」&#xff1a;与「上一个」统计周期比较&#xff0c;例如2011年4…

Postman测试文件上传接口

文章目录 一、Postman测试文件上传接口1、创建一个请求→选择请求方式→填写请求的URL→填写请求的参数值2、选择 "Body" → "form-data" → "Test" → "File"3、点击 "Select Files"4、选择要上传的文件5、点击 Send 进行测…

10 Vue3中v-html指令的用法

概述 v-html主要是用来渲染富文本内容&#xff0c;比如评论信息&#xff0c;新闻信息&#xff0c;文章信息等。 v-html是一个特别不安全的指令&#xff0c;因为它会将文本以HTML的显示进行渲染&#xff0c;一旦文本里面包含一些恶意的js代码&#xff0c;可能会导致整个网页发…

Unresolved plugin: ‘org.apache.maven.plugins‘解决报错

新建springboot项目报Unresolved plugin: ‘org.apache.maven.plugins:maven-surefire-plugin:3.1.2’ 缺什么插件 引入什么插件的依赖就行 <dependency><groupId>org.apache.maven.plugins</groupId><artifactId>maven-install-plugin</artifact…

设计模式03结构型模式

结构型模式 参考网课:黑马程序员Java设计模式详解 博客笔记 https://zgtsky.top/ 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式&#xff0c;前者采用继承机制来组织接口和类&#xff0c;后者釆用组合或聚合来组合对象。 由于…

【Unity】【WebRTC】如何用Unity而不是浏览器接收远程画面

【背景】 之前几篇我们讨论了如何设置信令服务器&#xff0c;如何发送画面给远端以及如何用浏览器查看同步画面&#xff0c;今天来讨论如何实现Unity内部接收画面。 看本篇之前请先看过之前将web服务器设置和基本远程画面功能的几篇博文。&#xff08;同专栏下查看&#xff09…
最新文章