Ps: 本文代码部分和部分题解未经测试就是口胡的,不保证完一全定正出确锅,如有错误请联系博主更改
Introduction
- 什么是线段树
线段树(Segment Tree)是一种基于分治思想的二叉树结构,用于区间上信息的统计,相较于基于二进制进行区间划分的树状数组而言,线段树是一种更加通用的数据结构,而且代码量仅比树状数组大亿点点
本文将会侧重于进阶内容,如果你没学过或者不熟悉线段树请看这里线段树学习笔记
- 什么时候用到线段树
统计量可以合并,修改量可以合并,通过修改量可直接修改统计量
满足区间加法即可使用线段树维护信息
普通线段树
储存
这里使用结构体储存线段树,我不太喜欢开一堆数组
在理想的情况下,(N)个叶子节点的满二叉树有(N+frac{N}{2}+frac{N}{4}+cdots+2+1=2N-1)个节点,当时由于使用这种存储方法会在最后一层产生空余,所以数组要开到(4N)
struct SegmentTree
{
int l,r;//该节点的区间
int dat;//维护的信息
}t[N*4];//4N!
建树
在区间([1,n])上建立线段树,注意最后要从下往上传递信息
void build(int p,int l,int r)
{
t[p].l=l,t[p].r=r;
if(l==r){t[p].dat=a[l];return ;}
int mid=(l+r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
t[p].dat=max(t[p<<1].dat,t[p<<1|1].dat);
}
单点修改
相当于修改区间([x,x]),只需要递归找到代表这个区间的叶子节点,然后修改,再从下往上更新一下即可
复杂度为(O(log n))
int change(int p,int x,int y)
{
if(t[p].l==t[p].r){t[p].dat=y;return;}
int mid=(t[p].l+t[p].r)>>1;
if(x<=mid) change(p<<1,x,y);
else change(p<<1|1,x,y);
t[p].dat=max(t[p<<1].dat,t[p<<1|1].dat);
}
区间查询
查询是会把询问的区间([l,r])在线段树上分成(log n)个节点,所以复杂度为(O(log n))
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)>>1;
int ret=-(1<<30);
if(l<=mid) val=max(val,ask(p<<1,l,r));
else val=max(val,ask(p<<1|1,l,r));
return ret;
}
例题
CH4301 Can you answer on these queries III
维护一个序列,支持单点修改和查询区间([x,y])中的最大连续子段和
问题在于如何维护最大连续字段和,或者说怎么存下最大连续子段和能让它满足区间加法,这也是用线段树维护某个东西时要考虑的核心问题
考虑最大连续字段和的形成方式,可能是整个区间([l,r])的和,可能是紧靠(l)这边的一段连续和,可能是紧靠(r)这边的一段连续和
所以我们可以在线段树中额外维护区间和(sum),左端最大连续子段(lmax)和和右端连续子段和(rmax),以及最大连续字段和(dat)
只需要在从下往上更新的地方注意这几个量的关系,进行维护即可求解,转移的时候有点像区间(dp)
特别注意:此时如果(x>y),请交换(x,y)
代码见CH4301 Can you answer on these queries III
CH4302 Interval GCD
维护序列,实现区间加和询问区间的最大公约数
最大公约数怎么维护啊,(qwq)
老祖宗写的《九章算术》告诉我们(gcd(x,y)=gcd(x,y-x)),实际上这个性质对三个数仍然成立,即(gcd(x,y,z)=gcd(x,y-x,z-y)),更实际上,该性质对任意多整数的都成立。(我不会整理,不过书上提示用数学归纳法)
因此我们只需要维护差分数组的区间最大公约数即可,这样就满足区间加法了。
至于区间加就按照差分数组的经典办法处理即可
延迟标记
又称懒标记,是一种"延迟“的思想。
懒标记提供了线段树中从上往下传递信息的方式,实现起来也很简单只需要再额外记录一个标记,在向下的操作中不断检查是否存在标记,并且不断下放即可
代码和普通线段树的相差不大,就是多了一个下放
void pushdown()
{
if(t[p].lazy)
{
t[p<<1].dat+=t[p].lazy*(t[p<<1].r-t[p<<1].l+1);
t[p<<1|1].dat+=t[p].lazy*(t[p<<1|1].r-t[p<<1|1].l+1);
t[p<<1].lazy+=t[p].lazy;t[p<<1|1].lazy+=t[p].lazy;
t[p].lazy=0;
}
}
其他操作不再赘述
多延迟标记的处理
当线段树要维护多个标记时,如何处理标记之间的影响和如何下传标记成了问题。
解决多标记问题,我们需要定一个标记优先级与执行顺序。那么每当要打一个新标记时,都要考虑对其执行顺序后的标记影响。处理好这一点就能解决问题。
- 有关标记优先级
在处理多标记问题时,我们可以把操作当运算符一样分优先级。其中遵循的原则就是:
在任何一个时刻, 节点所记录的信息在囊括自己和子孙节点的所有标记的前提下是正确的。
例题
维护序列,支持区间加和区间乘
先乘后加即可
维护一个序列支持区间加减,区间取负,询问区间选出 (k(<=20)) 个不同的数字求积,所有的
方案的和。 (nle5e4)
对于第三个询问,线段树每个区间维护 (dp[x]) 表示选 (x)个
数字的答案,那么:(dp[x+y]+=dp[x]cdot dp[y],xin lson,yin rson)
对于前两个操作:
首先区间加,假设选出(a_1cdots a_k),加上(v),从(a_1cdot a_2 cdot a_3...a_k)变成了((a_1+v)(a_2+v)...(a_k+v))这就(dp[p]*v^{k-p})会对(dp[k])有影响
区间取反对奇偶性进行讨论即可
扫描线
用于求(n)个矩形的面积并,矩形周长,多边形面积等问题。
扫描线的思想如下
用一条竖直的直线(不一定,也可以别的方向)从左到右扫过整个坐标系,直线上被并集图形覆盖的长度只会在每个矩形的左右边界处发生改变,也就是将整个并集图形分割成(2*n)个段,每一段覆盖直线的长度是固定的,所以各段面积很容易求得,各段面积求和即为并集图形的面积
注意事项
- 用来维护高的线段树和一般的线段树有所不同,他的叶子节点是满足([l,r],l+1=r)的
- 如果坐标有小数(或值域过大)
- 大概这些,想到再加
例题
Atlantis(面积并)
(n)个矩形放在一起,求它们的面积并
/*
@ author:pyyyyyy/guhl37
-----思路------
-----debug-------
太久没写其他头文件了,都忘了
memset必须用#include <cstring>
*/
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#define ls (rt << 1)
#define rs (rt << 1 | 1)
using namespace std;
const int N = 2020;
int cover[N];
double len[N], yy[N];
struct SegmentTree{
double x;//边x的坐标
double upy, downy;//边上方的y坐标,下方的y坐标
int inout;//标记是出边还是入边
}line[N];
int cmp(SegmentTree &a, SegmentTree &b){
return a.x < b.x;
}
void pushup(int rt, int l, int r)
{
if(cover[rt]) len[rt] = yy[r] - yy[l];
else if(l + 1 == r) len[rt] = 0;//到达叶节点
else len[rt] = len[ls] + len[rs];
}
void update(int rt, int yl, int yr, int val, int l, int r)
{
// if(yl > r || yr < l) return ;
if(yl <= l && yr >= r)
{
cover[rt] += val;
pushup(rt, l, r);
return ;
}
if(l + 1 == r) return ;
int mid = (l + r) >> 1;
if(yl <= mid) update(ls, yl, yr, val, l, mid);
if(yr > mid) update(rs, yl, yr, val, mid, r);//注意这里,不是mid+1,因为这里的线段树要进入[1,2][2,3]这样的区间
pushup(rt, l, r);
}
void add(int cnt, double x, double by, double ay, int val)
{
line[cnt].downy = ay;
line[cnt].upy = by;
line[cnt].x = x;
line[cnt].inout = val;
}
int n, T;
double ax, ay, bx, by;//注意别用y1
int main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
while(scanf("%d",&n),n)
{
memset(line, 0, sizeof(line));
memset(yy, 0, sizeof(yy));
int cnt=0;
for(int i = 1; i <= n; ++i)
{
scanf("%lf %lf %lf %lf",&ax, &ay, &bx, &by);
add(++cnt, ax, by, ay, 1); yy[cnt] = ay;
add(++cnt, bx, by, ay, -1); yy[cnt] = by;
}
sort(yy + 1,yy + cnt + 1);
sort(line + 1, line + cnt + 1, cmp);
int Len = unique(yy + 1, yy + cnt + 1) - yy - 1;
double ans = 0;
for(int i = 1; i <= cnt; ++i)
{
ans += len[1] * (line[i].x - line[i - 1].x);
int yl, yr, val;
yl = lower_bound(yy + 1, yy + Len + 1, line[i].downy) - yy;
yr = lower_bound(yy + 1, yy + Len + 1, line[i].upy) - yy;
val = line[i].inout;
update(1, yl, yr, val, 1, Len);
}
printf("Test case #%d
Total explored area: %.2f
", ++T, ans);
}
return 0;
}
HDU1828 Picture(周长并)
(n)个矩形放在一起,求覆盖的周长并。
求周长的并,比求面积的并稍微复杂一点
- 方法一:先求(x)轴再求(y)轴
要解决两个部分:横线和竖线
竖线很简单就是每次扫描线变化的长度
横线会从被扫描线分割的(2*n)个小矩形的一端延伸到下一个(这一个)矩形的一端,实际就是维护横线是由哪几个线段组成的
- 方法二:在求(x)轴的同时求(y)轴
确实有这种办法,但是我不会
HDU 1225 (面积交)
(n)个矩形放在一起,求覆盖的面积交
不会,告辞
poj 2482 Stars in Your Window
平面直角坐标系中有很多点,每个点有权值,问用(wcdot h)的矩形能圈住的点权值之和最大是
可以将问题转换平面上有若干区域,每个区域都有一个权值,求在那个坐标上重叠的区域权值和最大
然后用扫描线取出每个区域的左右边界,然后在纵坐标上建立线段树,维护区间最大值即可
动态开点与线段树合并
权值线段树
我们知道,普通线段树维护的信息是数列的区间信息,比如区间和、区间最大值、区间最小值等等。在维护序列的这些信息的时候,我们更关注的是这些数本身的信息,换句话说,我们要维护区间的最值或和,我们最关注的是这些数统共的信息。而权值线段树维护一列数中数的个数。
例如序列:(1,1,2,2,3,4,4,5)
建立权值线段树如下图
开局只有一个根,
节点全靠建。
你如何实现线段树?
不建出整个线段树,而是在最初只建立一个根节点,代表整个区间,当需要访问线段树的某棵子树时(某个子区间时)时,再建立代表这个子区间的节点。是一类特殊的线段树
与普通线段树的不同:
- 存储结构不再是完全二叉树,而是通过保存节点编号(指针)
- 不保存每个节点代表的区间,而是在每次递归访问时作为参数进行传递
动态开点线段树的结构和新建一个节点
struct SementTree{
int lc, rc;//左右孩子的编号
int dat;//以区间最大值为例
}t[N << 2];
int root, tot;
int build(){
tot++;
t[tot].lc = t[tot].rc = t[rot].dat = 0;
return tot;
}
//在main函数中
tot = 0;
root = build();
在(val)位置上的值加(delta),同时维护区间最大值
void insert(int p, int l, int r, int val, int delta){
if(l == r){
t[p].dat += delta;
return ;
}
int mid = (l + r) >> 1;
if(val <= mid){
if(!t[p].lc) t[p].lc = build();
insert(t[p].lc, l, mid, val, delta);
}
else{
if(!t[p].rc) t[p].rc = build();
insert(t[p].rc, mid + 1, r, val, delta);
}
t[p].dat = max(t[t[p].lc].dat, t[t[p].rc].dat);
}
一棵维护值域([1,n])的动态开点线段树在经历(m)次单点操作后,节点的数量规模为(O(mlog n)),最终至多有(2n-1)个节点
线段树合并
如果有若干棵线段树,他们都维护相同的值域([1,n]),那么他们对于各个子区间的划分显然是一致的。假设有 (m)次单点修改操作,每次操作在一棵线段树上执行,所有操作都执行完成后,求所有线段树的某个区间和
这里问题就可以用线段树合并来解决,具体步骤如下
使用两个指针(p,q)从两个根节点除法,以递归的方式同步遍历两颗线段树。即(p,q)始终代表相同的节点
- 若(p,q)之一为空,则以非空的那个作为合并后的节点。
- 若(p,q)都不为空,则递归合并两颗左子树和两颗右子树,然后删除节点(q),以(p)为合并后的节点,自底向上更新信息。若到达叶子节点,则直接把两个最值相加即可
int merge(int p, int q, int l, int r){
if(!p) return q;
if(!q) return p;
if(l == r){
t[p].dat += t[q].dat;
return p;
}
int mid = (l + r) >> 1;
t[p].lc = merge(t[p].lc, t[p].lc, l, mid);
t[p].rc = merge(t[p].rc, t[p].rc, mid + 1, r);
t[p].dat = max(t[t[p].lc].dat, t[t[p].rc].dat);
return p;
}
复杂度为(O(mlog n))(不证明了我不会懒)
例题
(n) 座房屋,并形成一个树状结构。然后救济粮分(m) 次发放,每次选择两个房屋 ((x,y)),然后对于 (x)到(y)的路径上(含(x)和(y))每座房子里发放一袋 (z) 类型的救济粮。
然后深绘里想知道,当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。
紫色的板子,水题(+1)
每个节点维护一棵权值线段树,下标为救济粮种类,区间维护数量最多的救济粮编号。
然后树上点差分思想,最后统计时自底向上做树上前缀和、线段树合并即得当前节点信息。
代码见P4556 雨天的尾巴
参考资料
以下内容质量颇高,建议阅读
《算法竞赛进阶指南》
《Segment Tree》
《线段树从入门到弃疗》
《解决动态统计问题的两把利刃》