线段树详解

@ 前言

这是本人的第一篇随笔,

肯定存在不足之处,欢迎大家指出问题

突然心血来潮,想总结一波线段树,故有此文

A 引例

现在给出一个问题:

给出 n 个数,n <= 100,和 m 个询问,每次询问区间 [l,r] 的和,并输出

显然,暴力可解,前缀和可以 O(1)解

修改题目:

给出 n 个数,n <= 1000000,和 m 个操作。每个操作可能有两种:1:在某个位置加上一个数;2:询问区间[l,r]的和,并输出。

显然前缀和不可解,暴力不可解(树状数组可解,但树状数组功能不够强大)

所以,我们需要一种强大的数据结构:线段树

这类问题的 m (询问次数)和 n (区间长度)往往是 10^5 数量级的。

线段树就是用来处理类似这样的,在序列上单点修改、区间询问(或者是区间修改、单点询问,甚至是区间修改、区间询问)的问题的一种数据结构。

相比与朴素算法 O(n^2) 的时间复杂度,线段树能在 O(nlogn) 的时间复杂度下解决问题。(但是有一点代码复杂度)

B 概念

线段树是一颗二叉(搜索)树,线段树上的每一个节点对应的是序列的一段区间,

或者说,叫做存储的是一个区间的信息:

 

白色[]里面是区间 橘色数字是节点编号(数组下标)

它的基本思想在于二分,每个节点可以维护很多信息,一般用结构体存储

--小分割线--

值得注意的是,它有一下两个性质:

  • 每个节点的左儿子区间范围为 [l,mid] ,右儿子区间范围为 [mid+1,r]
  • 对于节点 rt ,左孩子节点编号为 rt*2,右儿子节点编号为 rt*2+1
  • 共有 2^k-1个节点,其中k是树的深度

其实,第二、三个性质也是二叉树的性质

由第三个性质我们还可以知道数组的需要开 N*2 的空间

同时,考虑到标号也可能超过 2*N

(其实你可以画一颗线段树看看,比如一颗[1,10]的线段树)

比如:在第15号节点的时候,想调用它的左儿子30号节点,但我们的数组只开到了16,直接RE。

所以线段树的空间复杂度是 N*4

--小分割线--

我们不妨令线段树的高度(层数)为 h ,那么不难看出 h 只有 O(logn) 级别。

当我们需要维护的序列长度为 2 整数次幂的时候,线段树是一颗满二叉树(每个节点要么有两个儿子,要么是叶节点,且所有叶子节点深度相同)

其他情况下,线段树前 h-1 层是满二叉树,最后一层可能不满

C 线段树的基本操作

首先给出变量定义:

struct TREE{
    int l,r;  //l:左边界 r:右边界 
    int sum;  //sum:区间和 
    int mx,mn;//mx/mn:区间最大/最小值
}t[N*4];

(请思考,是否有什么办法,可以让我们的变量少一点)

线段树有以下五种基础操作:

建树、单点查询、单点修改、区间查询、区间修改

C1 建树

给你一个这样的序列 a :

 1 2 3 4 5 6 7

(当然可以不是有序的,也可以不是连续的)

对应的区间和是这样的:

 

 

这颗初始线段树显然是通过递归得到的

我们可以用build(rt)表示当前正在构建区间标号为rt的线段树

如果你思考了上面的问题:

是否有什么办法,可以让我们的变量少一点

很容易想到,我们可以把其中的 l r当成函数传参,也就是:

build(l,r,rt)表示当前要构建区间[l,r]的线段树,对应的编号为rt

之后,我们按以下几个步骤构造:

  1. 若 l==r,即我们访问到了叶节点,则我们可以直接构建(读入)
  2. 否则,新建一个节点,它的两个子节点如此构建build(l,mid,rt*2)和build(mid+1,r,rt*2+1)
  3. 返回时,对于区间[l,r],我们已经知道了它的左右儿子,可以通过子节点更新自己
struct TREE{
    int sum,mx,mn;
}t[N*4];

void pushup(int rt){//用儿子更新爸爸 
    t[rt].sum=t[rt*2].sum+t[rt*2+1].sum;
    t[rt].mn=min(t[rt*2].mn,t[rt*2+1].mn);
    t[rt].mx=max(t[rt*2].mx,t[rt*2+1].mx);
}

void build(int l,int r,int rt){//建树 
    if(l==r){//当前节点为叶子节点 
        scanf("%d",&t[rt].sum);
        t[rt].mn=t[rt].mx=t[rt].sum;
        return ;
    }
    int mid=(l+r)/2;
    build(l,mid,rt*2);//构造左子树
    build(mid+1,r,rt*2+1);//构造右子树
    pushup(rt);
}

 

因为节点个数是n*2级别的,所以这个过程的时间复杂度是O(n)

因为求min max sum在本质上是一致的,所以在后面都以求sum为例

C2 单点

首先我们需要找到那个点,比如点[5,5],大致过程是这样:(和建树差不多)

  • 若l==r,即我们访问的是叶节点,则世界返回这个节点的值
  • 否则,计算出mid,即(l+r)/2,根据中点值判断要找的点在 左/右 儿子
  • 如果是修改,因为儿子有所改变,所以还需要pushup

(注意到mid是向下取整)

1.最开始我们访问的是根节点:(橙色是区间编号),显然l!=r,5>mid,所以[5,5]在右儿子,rt=rt*2+1

2.访问[4,7],发现mid<=5,所以[5,5]在左儿子,rt=rt*2

3.访问[4,5]发现5>mid,所以[5,5]在右儿子,rt=rt*2+1

4.发现l==r,此时rt==13,找到了对应节点

C2.1 查询

分析过程如上,转化成代码是:

int query_node(int l,int r,int rt,int k){//查询 k 点 
    if(l==r)return t[rt].sum;
    int mid=(l+r)/2;
    if(k<=mid)return query_node(l,mid,rt*2,k);
    else return query_node(mid+1,r,rt*2+1,k);
}

C2.2 修改

这里的唯一区别在于最后还需要由下往上更新(叶节点的值被改变)

void change_node(int l,int r,int rt,int k,int val){//将 k 点增加 val 
    if(l==r){
        t[rt].sum+=val;
        return ; 
    }
    int mid=(l+r)/2;
    if(k<=mid)change_node(l,mid,rt*2,k,val);
    else change_node(mid+1,r,rt*2+1,k,val);
    pushup(rt);
}

 

1.正确性分析:因为如果不是目标位置,由if-else语句对目标位置定位,

逐步缩小目标范围,最后一定能且只能到达目标叶子节点

2.时间复杂度分析:

假设要修改[5]的值,可以发现,每层只有一个节点包含[5],所以修改了[5]之后,只需要每层更新一个节点就可以线段树每个节点的信息都是正确的,所以修改次数的最大值为层数。复杂度O(log2(n))

你可以像这样理解这段话:(还是上面那个图)

注意到mid是向下取整

找到一个节点:

显然,递归的次数=层数(这里是4层)。

这个4是怎么来的?线段树的本质是二叉树,所以就相当于我们每次递归都把区间分成了两份,一共分log(n)次(n是区间长度,不知道log的自行百度)

所以找到一个节点需要o(log(n))的复杂度

修改:

显然是o(1)可以忽略

更新:

在更新了一个节点后,我们显然需要把它所有的祖先节点都更新,有log(n)-1个祖先节点,算上修改的 1

所以修改一个节点需要o(log(n))的复杂度

(也许上面的分析有误,读者可以帮忙指正)

线段树的单次操作不是o(log(n))么?

是的,这也就是线段树常数大的原因,

所以我们说,线段树单次操作的复杂度是o(log(n))级别的

(区别单次操作和单点操作)

C3 区间

首先我们需要得到需要的那段区间的信息,比如对于区间[1,6],我们只需要知道:

(橙色字是区间对应的数组下标(rt))

对于现在查找的区间 l r,有这样三种情况:

([l,r]当前节点区间,[L,R]待查询区间)

1.当前节点区间的值全都是答案的一部分

 

 

这个时候我们可以直接return

2.当前节点的一部分是答案,应根据L R和区间中点(mid)的关系往下递归

 

 

3.当前节点区间包含了待查询的区间,应根据L R和区间中点(mid)的关系往下递归

 

 

4.那么会不会出现[l,r]与[L,R]完全无交集的情况?

不会的,因为我们是从根节点开始找,我们要找的区间必定是根节点的子孙

现在我们还需要讨论:

应根据L R和区间中点(mid)的关系往下递归

如何判断呢?

因为我们需要一个区间所有的信息,且对于线段树上的每个节点,都是独一无二的的,即不存在通过多种方法访问到一个节点的情况

所以只要[l,r]和[L,R]有交集,我们就可以放心地递归,而不用担心多次递归到同一个节点的问题

即:(注意mid是向下取整的)

if(L<=mid)do(左子树)
if(R>mid)do(右子树)

C3.1 查询

int query_range(int l,int r,int rt,int L,int R){//查询区间 [L,R] 
    if(L<=l&&r<=R)return t[rt].sum;
    //pushdown(l,r,rt);
    int mid=(l+r)/2,res=0;
    if(L<=mid)res+=query_range(l,mid,rt*2,L,R);
    if(R>mid)res+=query_range(mid+1,r,rt*2+1,L,R);
    return res;
}

 

(pushdown函数的作用在c3.2中讲,现在是可以忽略的)

C3.2 修改

有时候我们需要解决的不只是单点修改、区间询问,而是区间修改,区间询问。

此时单纯地利上面的操作已经无法解决问题了,因为我们没有办法高效地完成区间修改

如果将区间拆成一个个点进行修改或是在递归是访问所有被修改的叶子

那么最坏情况下修改操作的总复杂度就变成o(mnlogn),比朴素算法还要劣

 

引例

假设我们现在要对区间[1,4]的每个数都加3,并输出修改后[3,4]的值

修改前:

(橙色是编号,绿色是区间和)

修改后:

(红色是添加部分及更新后值)

q1:有没有更好的方法呢?

提示:线段树与枚举的很大不同之处在于它存储的是区间的信息

q2:还有一个问题:我们在查询区间[3,4]时,区间[0,1],[2,2]等节点是没有用的,也就是白改了

 

思考发现,时间的主要开销来源于对每一个节点的访问

所以,我们能不能只对少数点(一些父节点)进行修改,然后再访问某个特定节点的时候再根据它的祖先节点进行操作?

答案是显然的:可以

也就是说:修改的时候只修改对查询有用的点

 

Lazy Tag

懒惰标记的精髓在于:当我们需要用到这些子节点的信息时才进行更新。

比如在上面的操作:对区间[1,4]的每个数都加3,我们只需要引入一个lazy变量:

 

这时候,对于一个区间,比如[2,3],它的sum值应该更新为:lazy*(区间长度)+sum

如果我想获得点[2,2]的sum,我们需要把lazy下传

 

 

父节点lazy清零的原因是避免重复下传

这样我们可以容易地得到点[2,2]的sum=3+1*lazy=6,其中的1是区间长度

当然这些过程中还有众多细节

这样操作后,当我们访问到一个节点时,根节点到它的路径上的标记值,都已经通过标记下传更新到它的sum上了。

因此它的sum值就是现在它所对应的区间的和

对lazy的理解:(该段生动的理解转自:传送门)

  • 直观理解:“懒”标记,顾名思义,用到它才动,不用它就不动。
  • 作用:存储到这个节点的修改信息,暂时不把修改信息传到子节点。就像家长扣零花钱,你用的时候才给你,不用不给你。
  • 实现思路(重点):递归到这个节点时,只更新这个节点的状态,并把当前的更改值累积到标记中。注意是累积,可以这样理解:过年,很多个亲戚都给你压岁钱,但你暂时不用,所以都被你父母扣下了。
  • lazy标记的运用:当需要递归这个节点的子节点时,标记下传给子节点。这里不必管用哪个子节点,两个都传下去。就像你如果还有妹妹,父母给你们零花钱时总不能偏心吧
  • 下传操作:
    •  ①:当前节点的懒标记累积到子节点的懒标记中。
    •  ②:修改子节点状态。就是原状态+子节点区间点的个数*父节点传下来的懒标记。这就有疑问了,既然父节点都把标记传下来了,为什么还要乘父节点的懒标记,乘自己的不行吗?因为自己的标记可能是父节点多次传下来的累积,每次都乘自己的懒标记造成重复累积
    •  ③:父节点懒标记清0。这个懒标记已经传下去了,不清0后面再用这个懒标记时会重复下传。就像你父母给了你5元钱,你不能说因为前几次给了你10元钱, 所以这次给了你15元,那你不就亏大了。
void pushdown(int l,int r,int rt){
//标记下传
    if(!t[rt].lazy)return;
    int mid=(l+r)/2; 
    t[rt*2].lazy+=t[rt].lazy;
    t[rt*2+1].lazy+=t[rt].lazy;
    t[rt*2].sum+=t[rt].lazy*(mid-l+1);
    t[rt*2+1].sum+=t[rt].lazy*(r-mid);
    t[rt].lazy=0;
}


void change_range(int l,int r,int rt,int L,int R,int val){//将区间 [L,R] 增加 val 
    if(L<=l&&r<=R){
        t[rt].sum+=val*(r-l+1);
        t[rt].lazy+=val;
        return;
    }
    pushdown(l,r,rt);
    int mid=(l+r)/2; 
    if(L<=mid)change_range(l,mid,rt*2,L,R,val);
    if(R>mid)change_range(mid+1,r,rt*2+1,L,R,val);
    pushup(rt);
}

所以,回顾上面区间查询中出现的pushdown,作用就不言而喻了

 

标记永久化

另一种方法是不下传lazy标记,改为在询问过程中计算每个遇到的点对当前询问的影响

而为了保证询问的复杂度,子节点的影响需要在修改操作时就计算好

因此,在实际上,sum的值表示这个区间内所有数共同加上的值,sum表示这个区间内除了lazy之外其他值的和

需要注意的是区间的lazy值可能有一部分在祖先节点上,这在递归的时候累加即可

int query_range_keep(int l,int r,int rt,int L,int R){
    if(L<=l&&r<=R)return t[rt].sum+t[rt].lazy*(r-l+1);
    int mid=(l+r)/2,res=0;
    res=(min(r,R)-max(l,L)+1)*t[rt].lazy;
    if(L<=mid)res+=query_range_keep(l,mid,rt*2,L,R);
    if(mid<R)res+=query_range_keep(mid+1,r,rt*2+1,L,R);
    return res;
}

void change_range_keep(int l,int r,int rt,int L,int R,int val){
    if(L<=l&&r<=R){
        t[rt].lazy+=val;
        return;
    }
    t[rt].sum+=(min(r,R)-max(l,L)+1)*t[rt].lazy;
    int mid=(l+r)/2;
    if(L<=mid)change_range_keep(l,mid,rt*2,L,R,val);
    if(mid<R)change_range_keep(mid+1,r,rt*2+1,L,R,val);
} 

 


 

区间查询时间复杂度分析:(原文

 

 

 

 

 

乍一看很复杂,其实仔细看还是很容易理解的

D 区间合并

(链接指向作者本校oj ,某谷上也有)

经典区间合并问题:Hotel

D1 Hotel

 

 

D2 分析

看到题目是不是一脸蒙(这是牛干的事???)

这么大的数据量,枚举首先gg

这显然涉及到区间操作,考虑线段树:

简化题目:N个数的0,1序列,0表示无人,1表示入住

现在有两种操作:

  • 开房:寻找寻找连续的D个0的区间,然后都修改为1;
  • 退房:将区间[x,x+d-1]全部修改为0

将区间修改为1,0,这是线段树的基本操作:区间修改

所以现在的唯一问题在于如何利用线段树求解连续的d个0的区间

对于节点rt:

struct TREE{
    int lmax,rmax,sum;
    int len,fg;
};
  • lmax:左端点l开始最大连续0的长度.
  • rmax:右端点r开始最大连续0的长度
  • sum:区间[l,r]最大连续0的长度
  • len:区间长度
  • fg:1表示全部占用,2为全部为空

 

 

1.初始时:

t[rt].sum=t[rt].lmax=t[rt].rmax=t[rt].len=r-l+1;
t[rt].fg=0;

2.pushup

根据上图,可以发现:

即如果左儿子的lmax等于左儿子的长度,说明左儿子是一样的,还可以与本身的lmax合并

如果右儿子的rmax等于右儿子的长度,说明右儿子是一样的,还可以与本身的rmax合并

if(t[rt*2].len==t[rt*2].sum)
    t[rt].lmax=t[rt*2].len+t[rt*2+1].lmax;
else t[rt].lmax=t[rt*2].lmax;
if(t[rt*2+1].len==t[rt*2+1].sum)
    t[rt].rmax=t[rt*2+1].len+t[rt*2].rmax;
else t[rt].rmax=t[rt*2+1].rmax;

如何求解sum?

sum可以由三部分得到:

t[rt].sum=max(max(t[rt*2].sum,t[rt*2+1].sum),t[rt*2].rmax+t[rt*2+1].lmax);

3.pushdown

如果我们要开房,那么这个区间0的个数为0

如果我们要退房,那么这个区间0的个数为区间长度

if(t[rt].fg==0)return;
if(t[rt].fg==1){
    t[rt*2].fg=t[rt*2+1].fg=1;
    t[rt*2].sum=t[rt*2].lmax=t[rt*2].rmax=0;
    t[rt*2+1].sum=t[rt*2+1].lmax=t[rt*2+1].rmax=0;
    
}
if(t[rt].fg==2){
    t[rt*2].fg=t[rt*2+1].fg=2;
    t[rt*2].sum=t[rt*2].lmax=t[rt*2].rmax=t[rt*2].len;
    t[rt*2+1].sum=t[rt*2+1].lmax=t[rt*2+1].rmax=t[rt*2+1].len;
}
t[rt].fg=0;

4.查询

这里的区间查询也有了一定的变化,对于一个节点,它有三种情况:

  • 左儿子的sum>=需求长度
  • 左儿子的rmax加上右儿子的lmax>=需求长度
  • 右儿子的sum>=需求长度
  • 上述都不行

其实这类似求当前节点sum的过程(注意pushdown)

pushdown(rt);
if(l==r)return l;
int mid=(l+r)/2;
if(t[rt*2].sum>=len)return query(l,mid,rt*2,len);
if(t[rt*2+1].lmax+t[rt*2].rmax>=len)return mid-t[rt*2].rmax+1;
else return query(mid+1,r,rt*2+1,len);

5.修改

唯一需要注意的是对于每个信息都有改变以及pushdown的位置

6.不开longlong见祖宗

D3 代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=50005;

struct Tree{
    ll sum,len,fg;
    ll lmax,rmax;
}t[10*N];
ll n,m,opt,x,d;

inline void build(ll l,ll r,ll rt){
    t[rt].sum=t[rt].lmax=t[rt].rmax=t[rt].len=r-l+1;
    t[rt].fg=0;
    if(l==r)return ;
    ll mid=(l+r)/2;
    build(l,mid,rt*2);
    build(mid+1,r,rt*2+1);
}

inline void pushup(ll rt){
    if(t[rt*2].len==t[rt*2].sum)
        t[rt].lmax=t[rt*2].len+t[rt*2+1].lmax;
    else t[rt].lmax=t[rt*2].lmax;
    if(t[rt*2+1].len==t[rt*2+1].sum)
        t[rt].rmax=t[rt*2+1].len+t[rt*2].rmax;
    else t[rt].rmax=t[rt*2+1].rmax;
    t[rt].sum=max(max(t[rt*2].sum,t[rt*2+1].sum),t[rt*2].rmax+t[rt*2+1].lmax);
}

inline void pushdown(ll rt){
    if(t[rt].fg==0)return;
    if(t[rt].fg==1){
        t[rt*2].fg=t[rt*2+1].fg=1;
        t[rt*2].sum=t[rt*2].lmax=t[rt*2].rmax=0;
        t[rt*2+1].sum=t[rt*2+1].lmax=t[rt*2+1].rmax=0;
    }
    if(t[rt].fg==2){
        t[rt*2].fg=t[rt*2+1].fg=2;
        t[rt*2].sum=t[rt*2].lmax=t[rt*2].rmax=t[rt*2].len;
        t[rt*2+1].sum=t[rt*2+1].lmax=t[rt*2+1].rmax=t[rt*2+1].len;
    }
    t[rt].fg=0;
}

inline void change(ll a,ll b,ll fg,ll l,ll r,ll rt){
    pushdown(rt);
    if(a<=l&&r<=b){
        if(fg==1)t[rt].sum=t[rt].lmax=t[rt].rmax=0;
        else t[rt].sum=t[rt].lmax=t[rt].rmax=t[rt].len;
        t[rt].fg=fg;
        return;
    }
    ll mid=(l+r)/2;
    if(mid>=a)change(a,b,fg,l,mid,rt*2);
        if(mid<b)change(a,b,fg,mid+1,r,rt*2+1);
        pushup(rt);
}

inline ll query(ll l,ll r,ll rt,ll len){
    pushdown(rt);
    if(l==r)return l;
    ll mid=(l+r)/2;
    if(t[rt*2].sum>=len)return query(l,mid,rt*2,len);
    if(t[rt*2+1].lmax+t[rt*2].rmax>=len)return mid-t[rt*2].rmax+1;
    else return query(mid+1,r,rt*2+1,len);
}

int main(){
    scanf("%lld%lld",&n,&m);
    build(1,n,1);
    for(int i=1;i<=m;i++){
        scanf("%lld",&opt);
        if(opt==1){
            scanf("%lld",&d);
            if(t[1].sum<d)printf("0\n");
            else{
                ll left=query(1,n,1,d);
                printf("%lld\n",left);
                change(left,left+d-1,1,1,n,1);
            }
        }
        if(opt==2){
            scanf("%lld%lld",&x,&d);
            change(x,x+d-1,2,1,n,1);
        }
    }
    return 0;
}
View Code

E 扫描线

(链接是本校oj的,某谷上也有)

扫描线问题来源于这样一道题:「VOJ1056」图形面积 加强版

E1 题面

 

E2 分析

比如,我们需要计算这样三个矩形的面积:

 

暴力算法是这样的:用矩形面积和减去重叠的面积

显然,这样算非常的耗费时间,因为每个矩形都需要两两配对,查看互相之间是否有交集

于是,想象我们有这样几条线:

 

这几条线把我们要求的矩形面积分成了5块

对于每一块面积而言,我们只需要知道它的长和宽就可以了,

宽是很容易得到的,因为题目给出了每个矩形的左下角和右上角坐标

(请自动把数组d[]替换成x[])

我们一步步地得到每块的长:

STEP1:扫描到第一条扫描线:

新发现了一条边,对应的区间的覆盖次数全部加1

这个矩形的长就是左边所有不为0的区间的数量(len=9)

STEP2:扫描到第二条扫描线:

新发现了一条边,对应的区间全部加1,同时我们也得到了第一块面积=len * (x[2]-x[1]),ans+=第一块面积

STEP3:

这个时候其实你应该已经发现一些线段树的端倪了:区间修改,区间查询都需要应用

STEP4:

???

好像发现了什么不对:这条线是一个矩形的右边界

所以我们这个时候其实应该让对应的区间全部减1

我们把矩形的左边界叫做入边,右边界叫做出边

注意len是大于0的数量

STEP5:

STEP6:

 

E3 代码

为了储存每一条扫描线,我们可以:

struct LINE{
    int x,y,yy,fg;
    //x:边的横坐标 
    //y,yy:边的两个端点,长度为yy-y
    //fg:1表示入边,-1表示出边 
}line[N*2];

而线段树里需要这两个信息:

struct TREE{
    ll len; //cnt>0的长度 
    int cnt;//统计区间的覆盖次数 
}tree[N*10];

1.为了得到线段树的len值,我们还需要记录下y坐标的值,

需要注意纵坐标y有重复,需要去重排序。设去重排序后的序列为dy[],有t个。分成t-1段区间

那么第i段区间的左端点为dy[i],区间长度为dy[i-1]-dy[i]

int temp[2*N];//去重 
int dy[2*N];//纵坐标 

我们输入是这样的:

所以:

for(int i=1;i<=n;i++){
    scanf("%d%d%d%d",&a,&b,&c,&d);
    line[i*2-1]=LINE{a,b,d,1};//入边 
    line[i*2]=LINE{c,b,d,-1}; //出边 
    temp[i*2-1]=b;            //纵坐标 
    temp[i*2]=d;//都是从1开始存储 
}


void sorting(){//纵坐标去重排序 
    sort(temp+1,temp+1+n);
    dy[++t]=temp[1];
    for(int i=2;i<=n;i++){
        if(temp[i]!=temp[i-1])dy[++t]=temp[i];
    }
    t--;       //有t个点,分成t-1段区间 
}

2.对于每一条边line( x,y,yy,fg),相当于对覆盖的区间结点进行增加fg。如何找到y和yy对应哪一段区间?

二分查找

因为dy[]已经是去重且有序的(离散化),满二分条件

需要注意边界情况:

yy=half()-1,yy>y,表示第half()-1段区间,区间为[dy[half-1],dy[half()]]

int half(int y){//二分查找纵坐标对应的那一段区间 
    int l=1,r=t+1;
    while(l<=r){
        ll mid=(l+r)/2;
        if(dy[mid]==y)return mid;
        if(dy[mid]<y)l=mid+1;
        else r=mid-1;
    }
}

3.pushup

根据cnt覆盖次数更新节点:

当覆盖次数为0的时候,节点代表的区间不参与计算

void pushup(int l,int r,int rt){
    if(tree[rt].cnt>0)tree[rt].len=dy[r+1]-dy[l];//区间内所有点都被覆盖过 
    else tree[rt].len=tree[rt*2].len+tree[rt*2+1].len;
}

4.updata

在有了pushup操作后,我们就可以执行区间修改操作了

因为我们每条扫描线线的fg的值为1或-1,

所以每次的区间修改操作其实是把对应区间的cnt全部加上fg

void update(int l,int r,int rt,int a,int b,int fg){
    if(a>r||b<l)return;//第l~r段区间,是[dy[l],dy[r+1])
    if(a<=l&&r<=b){
        tree[rt].cnt+=fg;
        pushup(l,r,rt);//注意最小区间长度为 1,所以这里也要pushup 
        return;
    }
    int mid=(l+r)/2;
    update(l,mid,rt*2,a,b,fg);
    update(mid+1,r,rt*2+1,a,b,fg);
    pushup(l,r,rt);//回溯的时候更新len 
}

 

5.对于每一条扫描线:

for(int i=1;i<=n;i++){         //扫描每一条扫描线 
        if(i>1&&line[i].x!=line[i-1].x)ans+=(line[i].x-line[i-1].x)*tree[1].len;
        int x=half(line[i].y),y=half(line[i].yy)-1;
        update(1,t,1,x,y,line[i].fg);
    }

6.all

#include<bits/stdc++.h>
using namespace std;
const int N=2005;
typedef long long ll;

struct LINE{
    int x,y,yy,fg;
    //x:边的横坐标 
    //y,yy:边的两个端点,长度为yy-y
    //fg:1表示入边,-1表示出边 
}line[N*2];

struct TREE{
    ll len; //cnt>0的长度 
    int cnt;//统计区间的覆盖次数 
}tree[N*10];
int temp[2*N];//去重 
int dy[2*N];//纵坐标 
int n,t;

void sorting(){//纵坐标去重排序 
    sort(temp+1,temp+1+n);
    dy[++t]=temp[1];
    for(int i=2;i<=n;i++){
        if(temp[i]!=temp[i-1])dy[++t]=temp[i];
    }
    t--;       //有t个点,分成t-1段区间 
}

int cmp(LINE a,LINE b){
    return a.x<b.x?1:0;
}

int half(int y){//二分查找纵坐标对应的那一段区间 
    int l=1,r=t+1;
    while(l<=r){
        ll mid=(l+r)/2;
        if(dy[mid]==y)return mid;
        if(dy[mid]<y)l=mid+1;
        else r=mid-1;
    }
}

void build(int l,int r,int rt){//建树,意义在于多测清空,第rt个节点表示区间[dy[l],dy[r+1]] 
    tree[rt].cnt=tree[rt].len=0;
    if(l==r)return;
    int mid=(l+r)/2;
    build(l,mid,rt*2);
    build(mid+1,r,rt*2+1);
}

void pushup(int l,int r,int rt){
    if(tree[rt].cnt>0)tree[rt].len=dy[r+1]-dy[l];//区间内所有点都被覆盖过 
    else tree[rt].len=tree[rt*2].len+tree[rt*2+1].len;
}

void update(int l,int r,int rt,int a,int b,int fg){
    if(a>r||b<l)return;//第l~r段区间,是[dy[l],dy[r+1])
    if(a<=l&&r<=b){
        tree[rt].cnt+=fg;
        pushup(l,r,rt);//注意最小区间长度为 1,所以这里也要pushup 
        return;
    }
    int mid=(l+r)/2;
    update(l,mid,rt*2,a,b,fg);
    update(mid+1,r,rt*2+1,a,b,fg);
    pushup(l,r,rt);//回溯的时候更新len 
}

int main(){
    while(scanf("%d",&n)&&n){
        ll ans=0;
        t=0;
        int a,b,c,d;
        for(int i=1;i<=n;i++){
            scanf("%d%d%d%d",&a,&b,&c,&d);
            line[i*2-1]=LINE{a,b,d,1};//入边 
            line[i*2]=LINE{c,b,d,-1}; //出边 
            temp[i*2-1]=b;            //纵坐标 
            temp[i*2]=d;              //都是从1开始存储 
        }
        n*=2;                         //n个矩形,2n条边(出/入边) 
        sorting();                    //去重排序 
        build(1,t,1);                 //初始化 
        sort(line+1,line+1+n,cmp);    //注意矩形位置本来是无序的 
        for(int i=1;i<=n;i++){         //扫描每一条扫描线 
            if(i>1&&line[i].x!=line[i-1].x)ans+=(line[i].x-line[i-1].x)*tree[1].len;
            int x=half(line[i].y),y=half(line[i].yy)-1;
            update(1,t,1,x,y,line[i].fg);
        }
        printf("%lld\n",ans);
    }
    return 0;
}
View Code

F zkw线段树

这个。。。由于本人过于懒,所以只能挂链接了

(本来准备自己写的,但是咕咕咕了)

有趣的zkw线段树(超全详解) by Jμdge

G 例题

(里面的链接是作者本校oj链接,某谷上应该也有)

单点修改:

「HDU1166」敌兵布阵

[BZOJ5334] [Tjoi2018]数学计算

区间修改:

「POJ3468」A Simple Problem with Integer

「AHOI2009」维护序列

区间合并:

「USACO2008FEB」Hotel

扫描线:

「VOJ1056」图形面积 加强版

H 总结

 

 

问题:可能可以用线段树解决的问题

目标信息:由问题转换而成的,为了解决问题而需要统计的信息(可能不满足区间加法)。

点信息:每个点储存的信息

区间信息:每个区间维护的信息(线段树节点定义) (必须满足区间加法)区间信息包括 

统计信息标记

--------统计信息:统计节点代表的区间的信息,一般自下而上更新

--------标记:对操作进行标记(在区间修改时需要),一般自上而下传递,或者不传递

区间加法:实现区间加法的代码

查询:实现查询操作的代码

修改:实现修改操作的代码

符合区间加法的例子:数字之和——总数字之和 = 左区间数字之和 + 右区间数字之和

最大公因数(GCD)——总GCD = gcd( 左区间GCD , 右区间GCD );

最大值——总最大值=max(左区间最大值,右区间最大值)

H1 线段树的基本操作

#include<bits/stdc++.h>
using namespace std;
const int N=100000;

struct TREE{
    int sum,mx,mn;
    int lazy;
}t[N*4];

void pushup(int rt){           //自下而上更新 
    t[rt].sum=t[rt*2].sum+t[rt*2+1].sum;
}

void pushdown(int l,int r,int rt){//标记下传 
    if(!t[rt].lazy)return;
    int mid=(l+r)/2; 
    t[rt*2].lazy+=t[rt].lazy;
    t[rt*2+1].lazy+=t[rt].lazy;
    t[rt*2].sum+=t[rt].lazy*(mid-l+1);
    t[rt*2+1].sum+=t[rt].lazy*(r-mid);
    t[rt].lazy=0;
}

void build(int l,int r,int rt){//建树 
    if(l==r){                  //当前节点为叶子节点 
        scanf("%d",&t[rt].sum);
        return ;
    }
    int mid=(l+r)/2;
    build(l,mid,rt*2);         //构造左子树 
    build(mid+1,r,rt*2+1);     //构造右子树 
    pushup(rt);
}

int query_node(int l,int r,int rt,int k){//查询 k 点 
    if(l==r)return t[rt].sum;
    int mid=(l+r)/2;
    if(k<=mid)return query_node(l,mid,rt*2,k);
    else return query_node(mid+1,r,rt*2+1,k);
}

void change_node(int l,int r,int rt,int k,int val){//将 k 点增加 val 
    if(l==r){
        t[rt].sum+=val;
        return ; 
    }
    int mid=(l+r)/2;
    if(k<=mid)change_node(l,mid,rt*2,k,val);
    else change_node(mid+1,r,rt*2+1,k,val);
    pushup(rt);
}

int query_range_lazy(int l,int r,int rt,int L,int R){//查询区间 [L,R] 
    if(L<=l&&r<=R)return t[rt].sum;
    pushdown(l,r,rt);
    int mid=(l+r)/2,res=0;
    if(L<=mid)res+=query_range_lazy(l,mid,rt*2,L,R);
    if(R>mid)res+=query_range_lazy(mid+1,r,rt*2+1,L,R);
    return res;
}

void change_range_lazy(int l,int r,int rt,int L,int R,int val){//将区间 [L,R] 增加 val 
    if(L<=l&&r<=R){
        t[rt].sum+=val*(r-l+1);
        t[rt].lazy+=val;
        return;
    }
    pushdown(l,r,rt);
    int mid=(l+r)/2; 
    if(L<=mid)change_range_lazy(l,mid,rt*2,L,R,val);
    if(R>mid)change_range_lazy(mid+1,r,rt*2+1,L,R,val);
    pushup(rt);
}

int query_range_keep(int l,int r,int rt,int L,int R){
    if(L<=l&&r<=R)return t[rt].sum+t[rt].lazy*(r-l+1);
    int mid=(l+r)/2,res=0;
    res=(min(r,R)-max(l,L)+1)*t[rt].lazy;
    if(L<=mid)res+=query_range_keep(l,mid,rt*2,L,R);
    if(mid<R)res+=query_range_keep(mid+1,r,rt*2+1,L,R);
    return res;
}

void change_range_keep(int l,int r,int rt,int L,int R,int val){
    if(L<=l&&r<=R){
        t[rt].lazy+=val;
        return;
    }
    t[rt].sum+=(min(r,R)-max(l,L)+1)*t[rt].lazy;
    int mid=(l+r)/2;
    if(L<=mid)change_range_keep(l,mid,rt*2,L,R,val);
    if(mid<R)change_range_keep(mid+1,r,rt*2+1,L,R,val);
} 
View Code

H2 指针版线段树基操

仅作一个延伸

唯一的好处就是内存少一点

然后 转载

原文作者对它进行了封装,但是我想也应该不难理解(如果不了解指针的,还是算了)

良心的wyb甚至还加上了注释:

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

int read(){
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
    return f*x;
}
int n,m,a[N];

struct Segment{
    struct node{
        int l,r,sum,add;
        node *L,*R;
        node(int l,int r):l(l),r(r),sum(0),add(0),L(NULL),R(NULL){}
                                       //这段是c++特性,作用是初始化 
        int mid(){return (l+r)>>1;}    //获取 mid
        int len(){return r-l+1;}       //获取区间长度
        void up(){sum=(L->sum+R->sum);} //自下而上更新
        void down(){                   //标记下传 
            L->sum+=add*(L->len());
            R->sum+=add*(R->len()); 
            L->add+=add;
            R->add+=add;
        }
    }*root;
    
    void build(node *&k,int l,int r){
        k=new node(l,r);             //在申请空间的同时完成初始化
        if(l==r){
            k->sum=a[l];
            return;
        } 
        build(k->L,k->l,k->mid());
        build(k->R,k->mid()+1,k->r);
        k->up();
    }
    
    void change(node *k,int l,int r,int val){
        if(l<=k->l&&k->r<=r){
            k->sum+=val*(k->len());
            k->add+=val;
            return;
        }
        if(k->add)k->down(); 
        if(l<=k->mid())change(k->L,l,r,val);
        if(r>=k->mid())change(k->R,l,r,val);
        k->up();
    }
    
    int ask(node *k,int l,int r){
        if(l<=k->l&&k->r<=r)return k->sum;
        if(k->add)k->down();
        int res=0;
        if(l<=k->mid())res+=ask(k->L,l,r);
        if(r>=k->mid())res+=ask(k->R,l,r);
        return res;
    }
}A;

int main(){
    n=read();m=read();
    for(int i=1;i<=n;i++)a[i]=read();
    A.build(A.root,1,n);
    for(int i=1;i<=m;i++){
        int opt,x,y,z;
        opt=read();
        if(opt==1){
            x=read();y=read();z=read();
            A.change(A.root,x,y,z);
        }else{
            x=read();y=read();
            cout<<A.ask(A.root,x,y)<<endl;
        }
    }
    return 0;
}
View Code

H3 区间合并 & 扫描线

区间合并:

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

const int N=50005;
struct Tree{
    int sum,len,fg;
    int lmax,rmax;
}t[10*N];
int n,m,opt,x,d;

void build(int l,int r,int rt){
    t[rt].sum=t[rt].lmax=t[rt].rmax=t[rt].len=r-l+1;
    t[rt].fg=0;
    if(l==r)return ;
    int mid=(l+r)/2;
    build(l,mid,rt*2);
    build(mid+1,r,rt*2+1);
}
void pushup(int rt){
    if(t[rt*2].len==t[rt*2].sum)
        t[rt].lmax=t[rt*2].len+t[rt*2+1].lmax;
    else t[rt].lmax=t[rt*2].lmax;
    if(t[rt*2+1].len==t[rt*2+1].sum)
        t[rt].rmax=t[rt*2+1].len+t[rt*2].rmax;
    else t[rt].rmax=t[rt*2+1].rmax;
    t[rt].sum=max(max(t[rt*2].sum,t[rt*2+1].sum),t[rt*2].rmax+t[rt*2+1].lmax);
}
void pushdown(int rt){
    if(t[rt].fg==0)return;
    if(t[rt].fg==1){
        t[rt*2].fg=t[rt*2+1].fg=1;
        t[rt*2].sum=t[rt*2].lmax=t[rt*2].rmax=0;
        t[rt*2+1].sum=t[rt*2+1].lmax=t[rt*2+1].rmax=0;
    }
    if(t[rt].fg==2){
        t[rt*2].fg=t[rt*2+1].fg=2;
        t[rt*2].sum=t[rt*2].lmax=t[rt*2].rmax=t[rt*2].len;
        t[rt*2+1].sum=t[rt*2+1].lmax=t[rt*2+1].rmax=t[rt*2+1].len;
    }
    t[rt].fg=0;
}
void change(int a,int b,int fg,int l,int r,int rt){
    pushdown(rt);
    if(a<=l&&r<=b){
        if(fg==1)t[rt].sum=t[rt].lmax=t[rt].rmax=0;
        else t[rt].sum=t[rt].lmax=t[rt].rmax=t[rt].len;
        t[rt].fg=fg;
        return;
    }
    int mid=(l+r)/2;
    if(mid>=a)change(a,b,fg,l,mid,rt*2);
    if(mid<b)change(a,b,fg,mid+1,r,rt*2+1);
    pushup(rt);
}
int query(int l,int r,int rt,int len){
    pushdown(rt);
    if(l==r)return l;
    int mid=(l+r)/2;
    if(t[rt*2].sum>=len)return query(l,mid,rt*2,len);
    if(t[rt*2+1].lmax+t[rt*2].rmax>=len)return mid-t[rt*2].rmax+1;
    else return query(mid+1,r,rt*2+1,len);
}
int main(){
    scanf("%d%d",&n,&m);
    build(1,n,1);
    for(int i=1;i<=m;i++){
        scanf("%d",&opt);
        if(opt==1){
            scanf("%d",&d);
            if(t[1].sum<d)printf("0\n");
            else{
                int left=query(1,n,1,d);
                printf("%d\n",left);
                change(left,left+d-1,1,1,n,1);
            }
        }
        if(opt==2){
            scanf("%d%d",&x,&d);
            change(x,x+d-1,2,1,n,1);
        }
    }
    return 0;
}
View Code

扫描线

#include<bits/stdc++.h>
using namespace std;
const int N=2005;
typedef long long ll;

struct LINE{
    int x,y,yy,fg;
    //x:边的横坐标 
    //y,yy:边的两个端点,长度为yy-y
    //fg:1表示入边,-1表示出边 
}line[N*2];

struct TREE{
    ll len; //cnt>0的长度 
    int cnt;//统计区间的覆盖次数 
}tree[N*10];
int temp[2*N];//去重 
int dy[2*N];//纵坐标 
int n,t;

void sorting(){//纵坐标去重排序 
    sort(temp+1,temp+1+n);
    dy[++t]=temp[1];
    for(int i=2;i<=n;i++){
        if(temp[i]!=temp[i-1])dy[++t]=temp[i];
    }
    t--;       //有t个点,分成t-1段区间 
}

int cmp(LINE a,LINE b){
    return a.x<b.x?1:0;
}

int half(int y){//二分查找纵坐标对应的那一段区间 
    int l=1,r=t+1;
    while(l<=r){
        ll mid=(l+r)/2;
        if(dy[mid]==y)return mid;
        if(dy[mid]<y)l=mid+1;
        else r=mid-1;
    }
}

void build(int l,int r,int rt){//建树,意义在于多测清空,第rt个节点表示区间[dy[l],dy[r+1]] 
    tree[rt].cnt=tree[rt].len=0;
    if(l==r)return;
    int mid=(l+r)/2;
    build(l,mid,rt*2);
    build(mid+1,r,rt*2+1);
}

void pushup(int l,int r,int rt){
    if(tree[rt].cnt>0)tree[rt].len=dy[r+1]-dy[l];//区间内所有点都被覆盖过 
    else tree[rt].len=tree[rt*2].len+tree[rt*2+1].len;
}

void update(int l,int r,int rt,int a,int b,int fg){
    if(a>r||b<l)return;//第l~r段区间,是[dy[l],dy[r+1])
    if(a<=l&&r<=b){
        tree[rt].cnt+=fg;
        pushup(l,r,rt);//注意最小区间长度为 1,所以这里也要pushup 
        return;
    }
    int mid=(l+r)/2;
    update(l,mid,rt*2,a,b,fg);
    update(mid+1,r,rt*2+1,a,b,fg);
    pushup(l,r,rt);//回溯的时候更新len 
}

int main(){
    while(scanf("%d",&n)&&n){
        ll ans=0;
        t=0;
        int a,b,c,d;
        for(int i=1;i<=n;i++){
            scanf("%d%d%d%d",&a,&b,&c,&d);
            line[i*2-1]=LINE{a,b,d,1};//入边 
            line[i*2]=LINE{c,b,d,-1}; //出边 
            temp[i*2-1]=b;            //纵坐标 
            temp[i*2]=d;              //都是从1开始存储 
        }
        n*=2;                         //n个矩形,2n条边(出/入边) 
        sorting();                    //去重排序 
        build(1,t,1);                 //初始化 
        sort(line+1,line+1+n,cmp);    //注意矩形位置本来是无序的 
        for(int i=1;i<=n;i++){         //扫描每一条扫描线 
            if(i>1&&line[i].x!=line[i-1].x)ans+=(line[i].x-line[i-1].x)*tree[1].len;
            int x=half(line[i].y),y=half(line[i].yy)-1;
            update(1,t,1,x,y,line[i].fg);
        }
        printf("%lld\n",ans);
    }
    return 0;
}
View Code

H4 zkw线段树基本操作

#include<bits/stdc++.h>
using namespace std;
const int N=100005;

int n,m,q;
int sum[N*4],add[N*4];

void build(int n){
    for(m=1;m<=n;m<<=1);
    for(int i=m+1;i<=m+n;i++)scanf("%d",&sum[i]);
    for(int i=m-1;i;i--)sum[i]=sum[i<<1]+sum[i<<1|1];
}

void updata_node(int x,int v){
    x+=m,sum[x]+=v;
    for(;x>1;x>>=1)sum[x]+=v;
}

void updata_range(int s,int t,int v){
    int lc=0,rc=0,len=1;
    for(s+=m-1,t+=m+1;s^t^1;s>>=1,t>>=1,len<<=1){
        if(s&1^1)add[s^1]+=v,lc+=len;
        if(t&1)  add[t^1]+=v,rc+=len;
        sum[s>>1]+=v*lc,sum[t>>1]+=v*rc;
    }
    for(lc+=rc;s;s>>=1)sum[s>>1]+=v*lc;
}

int query_node(int x,int ans=0){
    for(x+=m;x;x>>=1)ans+=sum[x];
    return ans;
}

int query_range(int s,int t){
    int lc=0,rc=0,len=1,ans=0;
    for(s+=m-1,t+=m+1;s^t^1;s>>=1,t>>=1,len<<=1){
        if(s&1^1)ans+=sum[s^1]+len*add[s^1],lc+=len;
        if(t&1)  ans+=sum[t^1]+len*add[t^1],rc+=len;
        if(add[s>>1])ans+=add[s>>1]*lc;
        if(add[t>>1])ans+=add[t>>1]*rc;
    }
    for(lc+=rc,s>>=1;s;s>>=1)if(add[s])ans+=add[s]*lc;
    return ans;
}
View Code

H5 线段树对比树状数组

(这里指的是普通线段树)

线段树使用了分治思想,利用了区间的合并性质,预处理的时间复杂度达到了 O(N) ,单次操作的时间复杂度达到 O(logn)

树状数组可以实现单点修改、区间求和;如果将序列进行差分,那么树状数组还可以实现区间加值和单点询问。

而线段树除了可以实现树状数组数据的操作,还可以通过标记下传或标记永久化进行区间修改、区间询问。

但树状数组的编程较线段树简单,常数也比较小,能承受的数据规模在 10^6 级。

I 拓展

这些内容由于本人太懒,所以就摆在这里了

线段树的扩展

可持久化线段树

J 参考资料

线段树详解 by 岩之痕

浅谈线段树 by 日拱一卒 功不唐捐

线段树(区间查询,区间修改)——标记永久化版 by 蓝蓝的天堂

【题解】「USACO 2008 FEB」Hotel(线段树) by 王老师

【详解】线段树扫描线 by 王老师

有趣的zkw线段树(超全详解) by Jμdge

线段树(指针板子)by 淺_念

!!!完结!!!

posted @ 2020-11-21 13:44  _Famiglistimo  阅读(312)  评论(0编辑  收藏  举报