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*2build(p*2+1,mid+1,r); //右子节点[mid+1,r],编号p*2+1t[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);elsechange(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;elset[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.mzph.cn/news/238157.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Hadoop 集群环境搭建

目录 第一部分&#xff1a;系统安装... 3 1&#xff1a;图形化安装... 3 2&#xff1a;选择中文... 3 3&#xff1a;安装选项... 3 4&#xff1a;软件选项... 4 5&#xff1a;安装位置... 4 6&#xff1a;网络配置... 6 7&#xff1a;开始安装... 7 8&#xff1a;创建用户... 7…

TypeScript实战——ChatGPT前端自适应手机端,PC端

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 可以在线体验哦&#xff1a;体验地址 文章目录 前言引言先看效果PC端手机端 实现原理解释 包的架构目录 引言 ChatGPT是由OpenAI开发的一种基于语言模型的对话系统。它是GPT&#xff08;…

esp32-s3训练自己的数据进行目标检测、图像分类

esp32-s3训练自己的数据进行目标检测、图像分类 一、下载项目二、环境三、训练和导出模型四、部署模型五、存在的问题 esp-idf的安装参考我前面的文章&#xff1a; esp32cam和esp32-s3烧录human_face_detect实现人脸识别 一、下载项目 训练、转换模型&#xff1a;ModelAssist…

外汇天眼:我碰到外汇投资骗局了吗?学会这5招,轻松识别外汇诈骗黑平台!

近年来外汇市场因为交易量大、流动性大、不容易被控盘、品种简单、风险相对低等特色&#xff0c;因此吸引不少投资人青睐&#xff0c;成为全球金融市场的热门选择。 然而&#xff0c;市面上充斥许多诈骗集团设立的黑平台&#xff0c;也打着投资外汇的名义行骗&#xff0c;不免会…

25年老品牌 美好蕴育润康守护孕期妈妈的健康之路

孕期是每一位女性人生中最为特殊的阶段。每一位妈妈都期待着健康、快乐的度过每一天。然而&#xff0c;随着孕周的增加&#xff0c;孕期的不适和困扰也随之而来。孕吐、腰酸背痛、便秘等孕期症状让许多妈妈倍感疲惫和焦虑。在这个关键时刻&#xff0c;美好蕴育润康作为25年的老…

Docker 文件和卷 权限拒绝

一 创作背景 再复制Docker影像文件或访问Docker容器内已安装卷上的文件时我们常常会遇到&#xff1a;“权限被拒绝”的错误&#xff0c;在此&#xff0c;您将了解到为什么会出现“权限被拒绝”的错误以及如何解决这个问题。 二 目的 在深入探讨 Docker 容器中的 Permission De…

Python实验报告十一、自定义类模拟三维向量及其运算

一、实验目的&#xff1a; 1、了解如何定义一个类。 2、了解如何定义类的私有数据成员和成员方法。 3、了解如何使用自定义类实例化对象。 二、实验内容&#xff1a; 定义一个三维向量类&#xff0c;并定义相应的特殊方法实现两个该类对象之间的加、减运算&#xff08;要…

YOLOv5涨点技巧:一种新颖的多尺度特征融合方法iAFF

💡💡💡本文全网独家改进:1)引入了一种新颖的多尺度特征融合iAFF;2)为了轻量级部署,和GhostConv有效结合在边缘端具有竞争力的准确性 💡💡💡在YOLOv5中如何使用 1)iAFF加入Neck替代Concat; 💡💡💡Yolov5/Yolov7魔术师,独家首发创新(原创),适用于Y…

回顾丨2023 SpeechHome 第三届语音技术研讨会

下面是整体会议的内容回顾&#xff1a; 18日线上直播回顾 18日上午9:30&#xff0c;AISHELL & SpeechHome CEO卜辉宣布研讨会开始&#xff0c;并简要介绍本次研讨会的筹备情况以及报告内容。随后&#xff0c;CCF语音对话与听觉专委会副主任、清华大学教授郑方&#xff0c…

Linux 一键部署TeamCity

前言 TeamCity 是一个通用的 CI/CD 软件平台,可实现灵活的工作流程、协作和开发实践。允许在您的 DevOps 流程中成功实现持续集成、持续交付和持续部署。 系统支持Centos7,8,9/Redhat7,8,9及复刻系列download TeamCity TeamCity 文档参考TeamCity downloaddownloadTeamCi…

[Unity]接入Firebase 并且关联支付埋点

首先 在这个下一下FireBase的资源 firebase11.0.6 然后导入Analytics Auth Crashlytics 其他的看着加就行 然后直接丢到Unity里面 接下来需要去Firebase里面下载 Google json 丢到 这个下面 然后就是脚本代码了 using System.Collections; using System.Collection…

一键批量管理HTML文档,轻松删除无用内容,提升网站性能!

随着互联网的快速发展&#xff0c;网站性能优化已经成为每个站长和开发者必须面对的问题。为了提升网站性能&#xff0c;我们需要对HTML文档进行高效的管理和优化。但是&#xff0c;手动管理大量HTML文件不仅效率低下&#xff0c;还容易出错。那么&#xff0c;有没有一种简单、…

2023 年人工智能研究与技术排名前 10 的国家

人工智能研究是一项全球性的工作。虽然美国和中国因其对人工智能的贡献而备受关注&#xff0c;但事实是&#xff0c;世界各国都在涉足这项技术&#xff0c;尝试新的突破&#xff0c;并吸引投资者的关注。 斯坦福大学的《2023年人工智能报告》估计&#xff0c;到 2022 年&#…

05-垃圾收集器ParNewCMS与底层三色标记算法详解

文章目录 垃圾收集算法分代收集理论标记-复制算法标记-清除算法标记-整理算法 垃圾收集器Serial收集器Parallel Scavenge收集器ParNew收集器CMS收集器 CMS的相关核心参数亿级流量电商系统如何优化JVM参数设置(ParNewCMS) 垃圾收集底层算法实现三色标记多标-浮动垃圾漏标-读写屏…

歌曲春节回家:李白的诗意与荆涛的歌声

歌曲春节回家&#xff1a;李白的诗意与荆涛的歌声 “春节回家&#xff0c;春节回家&#xff0c;又是一个春节到&#xff0c;漫天雪花飘。”随着歌手荆涛深情的嗓音&#xff0c;我们仿佛置身于那漫天飞雪的冬日&#xff0c;期待着与家人团聚的温馨时刻。这首歌曲不仅是对春节回…

数学建模笔记-拟合算法

内容&#xff1a;拟合算法 一.概念&#xff1a; 拟合的结果就是找到一个确定的曲线 二.最小二乘法&#xff1a; 1. 2.最小二乘法的二表示的是平方的那个2 3.求解最小二乘法&#xff1a; 三.评价拟合的好坏 1.总体评分和SST&#xff1a; 2.误差平方和SSE&#xff1a; 3.回…

机器学习:手撕 AlphaGo(一)

图 1-1: AphaGo 结构概览 1. 前言 AlphaGo 是一个非常经典的模型&#xff0c;不论从影响力还是模型设计上。它的技术迭代演进路径&#xff1a;AlphaGo&#xff0c;AlphaGoZero&#xff0c;AlphaZero&#xff0c;MuZero 更是十分精彩。相信有很多同学因为听了 AlphaGo 的故事对…

在Portainer创建Nginx容器并部署Web静态站点实现公网访问

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;…

Leetcod面试经典150题刷题记录 —— 矩阵篇

矩阵篇 1. 有效的数独2. 螺旋矩阵Python 3. 旋转图像Python额外开辟数组空间原地置换法 4. 矩阵置零5. 生命游戏Python 1. 有效的数独 题目链接&#xff1a;有效的数独 - leetcode 题目描述&#xff1a; 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验…

Android13 Wifi启动流程分析

Android13 Wifi启动流程分析 文章目录 Android13 Wifi启动流程分析一、正常开关wifi 启动流程1、WifiManager2、WifiServiceImpl3、ActiveModeWarden4、ConcreteClientModeManager5、WifiNative6、WifiVendorHal7、HalDeviceManager8、wifi.cpp 二、重启设备时自动开启wifi流程…