高级数据结构之线段树

线段树是一种用来维护区间信息的数据结构。

线段树可以在 O ( l o g N ) O(logN) O(logN)的时间复杂度内实现单点修改、区间修改、区间查询(区间求和、求区间最大值、求区间最小值)等操作。

使用条件

线段树维护的信息很多可以认为满足含幺半群的性质的信息(封闭性、可结合、存在幺元)。

一般满足这样的性质——一些数域上的加法与乘法自然,考虑二元的 m a x ( x , y ) max(x,y) max(x,y)运算,此时幺元为 − ∞ -\infty y也满足这样的性质。

数组大小:

线段树高度为 ⌈ l o g N ⌉ \lceil logN \rceil logN,即 l o g N + 1 = n − 1 logN+1=n-1 logN+1=n1

根据等比序列求和公式 2 n − 1 = 4 ∗ 2 n − 1 2^n-1=4*2^n-1 2n1=42n1约等于 4 N 4N 4N

线段树基本四个操作:
  1. pushup:两个儿子算当前结点信息。

  2. build:将一段区间初始化成线段树。

  3. modify:单点修改、区间修改(懒标记)

  4. query:查询操作——查询区间信息。

    一个最简单的例子

    struct Node{
        int l, r;
        int v;   //最大值
    }tr[N * 4];
    

    pushup操作

    void pushup(int u)  // 由子节点的信息,来计算父节点的信息
    {
        tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
    }
    

    build操作

    void build(int u, int l, int r) {
        tr[u] = {l, r};
        if(l == r) return;
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PuRGfajx-1666616420858)(C:\Users\muhan\Desktop\v2-c2d11b12c87b6a7076e3df0bb3585423_b.gif)]

    query操作(三种情况)

    //查询以u为根节点,区间[l, r]中的最大值
    int query(int u, int l, int r) {
        //      Tl-----Tr
        //   L-------------R   
        //1.不必分治,直接返回
        if(tr[u].l >= l && tr[u].r <= r) return tr[u].v;
    
        int mid = tr[u].l + tr[u].r >> 1;
        int v = 0;
        //     Tl----m----Tr
        //        L-------------R 
        //2.需要在tr的左区间[Tl, m]继续分治
        if(l <= mid) v = query(u << 1, l, r);
    
        //     Tl----m----Tr
        //   L---------R 
        //3.需要在tr的右区间(m, Tr]继续分治
        if(r > mid) v = max(v, query(u << 1 | 1, l, r));
    
        //     Tl----m----Tr
        //        L-----R 
        //2.3涵盖了这种情况
        return v;
    }
    

    modify操作(单点修改)

    //u为结点编号,更新该结点的区间最大值
    void modify(int u, int x, int v) {
        if(tr[u].l == tr[u].r) tr[u].v = v;  //叶节点,递归出口
        else {
            int mid = tr[u].l + tr[u].r >> 1;
            //分治处理左右子树, 寻找x所在的子树
            if(x <= mid) modify(u << 1, x, v);
            else modify(u << 1 | 1, x, v);
            //回溯,拿子结点的信息更新父节点, 即pushup操作
            tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
        }
    }
    

最多两条链(保证时间复杂度2*logn)


例题1:246. 区间最大公约数 - AcWing题库

更相减损术其实是欧几里得算法的一个特例。即 g c d ( a − n b , b ) = g c d ( a , b ) gcd(a-nb,b)=gcd(a,b) gcd(anb,b)=gcd(a,b)

( a , b , c ) = ( ( a , b ) , ( b , c ) ) = ( ( a , b − a ) , ( b , c − b ) ) = ( a , b − a , c − b ) (a,b,c)=((a,b),(b,c))=((a,b-a),(b,c-b))=(a,b-a,c-b) (a,b,c)=((a,b),(b,c))=((a,ba),(b,cb))=(a,ba,cb)

由于 ( b − a , b ) = ( a , b − a ) (b-a,b)=(a,b-a) (ba,b)=(a,ba)所以 ( a , b , c ) = ( a , b − a , c − b ) (a,b,c)=(a,b-a,c-b) (a,b,c)=(a,ba,cb)

有了这个式子说名可以通过维护序列的差分来达到求 g c d gcd gcd同样的效果。

比如求 ( a , b , c ) (a,b,c) (a,b,c)只需要知道现在 a a a的值,然后知道 ( b − a , c − b ) (b−a,c−b) (ba,cb)的gcd,再求一个公约数就行了。

  • 差分就可以把区间加减变成单点加减。可以用没有lazy的线段树来做。
  • 再维护一个差分,做成树状数组或者线段树,用来维护每个数的值。

g c d ( a , b ) = g c d ( a , − b ) gcd(a,b)=gcd(a,-b) gcd(a,b)=gcd(a,b)

在数值加减的过程中可能会产生负数,而约定gcd是没有负数的,所以需要用这个式子来搞定负数。
具体来说,就是在每次查询或者更新的时候,如果遇到了负数,就把它取反。
注意只能对结果取反而不能直接把线段树的负数叶子节点取反。因为直接把叶子取反会对今后的加减操作造成影响。

虽然 g c d ( a , b ) = g c d ( a , − b ) gcd(a,b)=gcd(a,−b) gcd(a,b)=gcd(a,b)但是 ( a + 1 , b ) (a+1,b) (a+1,b) ( − a + 1 , b ) (−a+1,b) (a+1,b)不一定相等。


int n, m;
LL w[N];
struct Node
{
    int l, r;
    LL sum, d;
}tr[N * 4];

LL gcd(LL a, LL b)
{
    return b ? gcd(b, a % b) : a;
}

void pushup(Node &u, Node &l, Node &r)
{
    u.sum = l.sum + r.sum;
    u.d = gcd(l.d, r.d);
}

void pushup(int u)
{
    pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
}

void build(int u, int l, int r)
{
    if (l == r)
    {
        LL b = w[r] - w[r - 1];
        tr[u] = {l, r, b, b};
    }
    else
    {
        tr[u].l = l, tr[u].r = r;
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

void modify(int u, int x, LL v)
{
    if (tr[u].l == x && tr[u].r == x)
    {
        LL b = tr[u].sum + v;
        tr[u] = {x, x, b, b};
    }
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, v);
        else modify(u << 1 | 1, x, v);
        pushup(u);
    }
}

Node query(int u, int l, int r)
{
    if (tr[u].l >= l && tr[u].r <= r) return tr[u];
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if (r <= mid) return query(u << 1, l, r);
        else if (l > mid) return query(u << 1 | 1, l, r);
        else
        {
            auto left = query(u << 1, l, r);
            auto right = query(u << 1 | 1, l, r);
            Node res;
            pushup(res, left, right);
            return res;
        }
    }
}

####懒标记及其更新标记

对于区间修改,考虑引入一个名叫"lazy tag"的东西。

首先,懒标记的作用是记录每次、每个结点要更新的值,也就是 δ \delta δ,但线段树的优点不在于全记录(太慢了 q w q qwq qwq),而在于传递式记录:

我们需要在每次区间的查询修改时pushdown一次,以免重复或者充图或者爆炸 q w q qwq qwq

那么对于Pushdown而言,其实就是纯粹的Pushup操作的逆向思维(不是逆向操作):因为修改信息存在父节点上,所以要由父节点向下传导lazy tag

那么问题来了:如何传导pushdown呢?这里很有意思,开始回溯时执行pushup,因为时向上传导信息;那我们如果要让它向下更新,就调整顺序,在向下递归的时候pushdown就好了。

void Pushdown(int k){    //更新子树的lazy值,这里是RMQ的函数,要实现区间和等则需要修改函数内容
    if(lazy[k]){    //如果有lazy标记
        lazy[k<<1] += lazy[k];    //更新左子树的lazy值
        lazy[k<<1|1] += lazy[k];    //更新右子树的lazy值
        t[k<<1] += lazy[k];        //左子树的最值加上lazy值
        t[k<<1|1] += lazy[k];    //右子树的最值加上lazy值
        lazy[k] = 0;    //lazy值归0
    }
}

//递归更新区间 updata(L,R,v,1,n,1);
void updata(int L,int R,int v,int l,int r,int k){    //[L,R]即为要更新的区间,l,r为结点区间,k为结点下标
    if(L <= l && r <= R){    //如果当前结点的区间真包含于要更新的区间内
        lazy[k] += v;    //懒惰标记
        t[k] += v;    //最大值加上v之后,此区间的最大值也肯定是加v
    }
    else{
        Pushdown(k);    //重难点,查询lazy标记,更新子树
        int m = l + ((r-l)>>1);
        if(L <= m)    //如果左子树和需要更新的区间交集非空
            update(L,R,v,l,m,k<<1);
        if(m < R)    //如果右子树和需要更新的区间交集非空
            update(L,R,v,m+1,r,k<<1|1);
        Pushup(k);    //更新父节点
    }
}
//递归方式区间查询 query(L,R,1,n,1);
int query(int L,int R,int l,int r,int k){    //[L,R]即为要查询的区间,l,r为结点区间,k为结点下标
    if(L <= l && r <= R)    //如果当前结点的区间真包含于要查询的区间内,则返回结点信息且不需要往下递归
        return t[k];
    else{
        Pushdown(k);    /**每次都需要更新子树的Lazy标记*/
        int res = -INF;    //返回值变量,根据具体线段树查询的什么而自定义
        int mid = l + ((r-l)>>1);    //m则为中间点,左儿子的结点区间为[l,m],右儿子的结点区间为[m+1,r]
        if(L <= m)    //如果左子树和需要查询的区间交集非空
            res = max(res, query(L,R,l,m,k<<1));
        if(R > m)    //如果右子树和需要查询的区间交集非空,注意这里不是else if,因为查询区间可能同时和左右区间都有交集
            res = max(res, query(L,R,m+1,r,k<<1|1));

        return res;    //返回当前结点得到的信息
    }
}
线段树(扫描线)

例题:247. 亚特兰蒂斯 - AcWing题库

对于给定的n(n<=100000)个平行于XY轴的矩形,求他们的面积并 。

这是一个二维的问题,如果我告诉你这道题使用线段树解决,你该如何入手呢,首先线段树是一维的,所以我们需要化二维为一维,所以我们可以使用x的坐标或者y的坐标建立线段树,另一坐标用来进行枚举操作。

​ 我们用x的坐标来建树的化,那么我们把矩阵平行于x轴的线段舍去,则变成了

img

每个矩形都剩下两条边,定义x坐标较小的为入边(值为+1),较大为出边(值为-1),然后用x的升序,记第i条线段的x坐标即为X[i]

img

​ 接下来将所有矩形端点的y坐标进行重映射(也可以叫离散化),原因是坐标有可能很大而且不一定是整数,将原坐标映射成小范围的整数可以作为数组下标,更方便计算,映射可以将所有y坐标进行排序去重,然后二分查找确定映射后的值,离散化的具体步骤下文会详细讲解。如图所示,蓝色数字表示的是离散后的坐标,即1、2、3、4分别对应原先的5、10、23、25(需支持正查和反查)。假设离散后的y方向的坐标个数为m,则y方向被分割成m-1个独立单元,下文称这些独立单元为“单位线段”,分别记为<1-2>、<2-3>、< 3-4>。

img

​ 以x坐标递增的方式枚举每条垂直线段,y方向用一个长度为m-1的数组来维护“单位线段”的权值,如图所示,展示了每条线段按x递增方式插入之后每个“单位线段”的权值。

当枚举到第i条线段时,检查所有“单位线段”的权值,所有权值大于零的“单位线段”的实际长度之和(离散化前的长度)被称为“合法长度”,记为L,那么(X[i] - X[i-1]) * L,就是第i条线段和第i-1条线段之间的矩形面积和,计算完第i条垂直线段后将它插入,所谓"插入"就是利用该线段的权值更新该线段对应的“单位线段”的权值和(这里的更新就是累加)。

img

如图四-4-6所示:红色、黄色、蓝色三个矩形分别是3对相邻线段间的矩形面积和,其中红色部分的y方向由<1-2>、<2-3>两个“单位线段”组成,黄色部分的y方向由<1-2>、<2-3>、❤️-4>三个“单位线段”组成,蓝色部分的y方向由<2-3>、❤️-4>两个“单位线段”组成。特殊的,在计算蓝色部分的时候,<1-2>部分的权值由于第3条线段的插入(第3条线段权值为-1)而变为零,所以不能计入“合法长度”。
  以上所有相邻线段之间的面积和就是最后要求的矩形面积并。

img

(经典用法、以下看一看就好555)

线段树板子

struct Node{
    int l,r;
   	//需要维护的信息和懒标记
}tr[N*4];

void pushup(int u){
    //利用左右儿子信息维护当前节点的信息
}

void pushdown(int u){
    //将懒标记下传
} 

void build(int u,int l,int r){
    if(l==r)tr[u]={l,r};
    int mid=l+r>>1;
    build(u<<1,1,mid),build(u<<1|1,mid+1,r);
    pushup(u);
}

void update(int u,int l,int r,int d){
    if(tr[u].l>=l&&tr[u].r<=r){
        //修改区间
    }else{
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)update(u<<1,l,r,d);
        if(r>mid)update(u<<1|1,l,r,d);
    }
}

int query(int u,int l,int r){
    if(tr[u].l>=l&&tr[u].r<=r){
        return ;//需要返回补充值
    }else{
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        int res=0;
        if(l<=mid)res=query(u<<1,l,r);
        if(r>mid)res+=query(u<<1|1,l,r);
        return res;
    }
}

单点改,区间最大值

struct Node{
    int l,r;
    int mx;//选哟维护的信息和懒标记
}
int n,m,p;

void pushup(int u){
    tr[u].mx=max(tr[u<<1].mx,tr[u<<1|1].mx);
}
void build(int u,int l,int r){
    if(l==r)tr[u]={l,r,0};
    else{
        tr[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        //pushup(u);
    }
}
void update(int u,int x,int d){
    if(tr[u].l==x&&tr[u].r==x){
        tr[u].mx=d;//修改区间
    }else{
        //pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(x<=mid)update(u<<1,x,d);
        if(x>mid)ipdate(u<<1|1,x,d);
        pushup(u);
    }
}
int query(int u,int l,int r){
    if(tr[u].l>=l&&tr[u].r<=r){
        return tr[u].mx;//需要补充返回值
    }else{
        //pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        int res=0;
        if(l<=mid)res=max(res,query(u<<1,l,r));
        if(l>mid)res=max(res,query(u<<1|1,l,r));
        return res;
    }
}

单点改,最大连续字段和

struct Node{
    int l,r;
    int sum,tmx,lmx,rmx;//需要维护的信息和懒标记
}tr[N*4];
int n,m;
int w[N];
void pushup(Node&root,Node&left,Node&right){
    root.sum=left.sum+right.sum;
    root.tmx=max({left.tmx,right.tmx,left.rmx+right.lmx});
    root.lmx=max({left.lmx,left.sum+right.lmx});
    root.rmx=max({right.rmx,right.sum+left.rmx});
}
void pushup(int u){
    pushup(tr[u],tr[u<<1],tr[u<<1|1]);//利用左右儿子信息维护当前节点的信息
}
void update(int u,int x,int d){
    if(tr[u].l==x&&tr[u].r==x){
        tr[u]={x,x,d,d,d,d}//修改区间
    }else{
        //pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(x<mid)update(u<<1,x,d);
        if(x>=mid)update(u<<1|1,x,d);
        pushup(u);
    }
}
Node query(int u,int l,int r){
    if(tr[u].l>=l&&tr[u].r<=r){
        return tr[u];
    }else{
        //pushdown(u);
        int mid=tr[u].l+tr[u].r>>q;
        if(r<=mid)return query(u<<1,l,r);
        else if(l>mid)return query(u<<1|1,l,r);
        else{
            Node left=query(u<<1,l,r);
            Node right=query(u<<1|1,l,r);
            Node res;
            pushup(res,left,right);
            return res;
        }
    }
}

区间加,区间最大gcd

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

const int N=500010;
typedef long long ll;
struct Node{
    int l,r;
    ll sum,gcd;
}tr[N*4];
int n,m;
ll w[N];
ll gcd(ll a,ll b){
    return b?gcd(b,a%b):a;
}
void pushup(Node&root,Node&left,Node&right){
    root.sum=left.sum+right.sum;
    root.gcd=gcd(left.gcd,right.gcd);
}
void pushup(int u,int l,int r){
    pushup(tr[u],tr[u<<1],tr[u<<1|1]);
    //利用左右节点维护当前节点信息。
}
void build(int u,int l,int r){
    if(l==r)tr[u]={l,r,w[r]-w[r-1],w[r]-w[r-1]};
    else{
        tr[u]={l,r};
        int mid=r+l>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void update(int u,int x,ll d){
    if(tr[u].l==x&&tr[u].r==x){
        tr[u].sum+=d;
        tr[u].gcd=tr[u].xum;
    }else{
        //pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(x<=mid)update(u<<1,x,d);
        if(x>mid)update(u<<1|1,x,d);
        pushup(u);
    }
}
Node query(int u,int l,int r){
    if(tr[u].l>=l&&tr[u].r<=r)return tr[u];//需要补充返回值
    else{
        //pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(r>=mid)return query(u<<1,l,r);
        if(r<mid)return query(i<<1|1,l,r);
        else{
            Node res;
            Node left=query(u<<1,l,r);
            Node right=query(u<<1|1,l,r);
            pushup(res,left,right);
            return res;
        }
    }
}

区间加、区间和

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
typedef long long ll;
struct Node{
    int l,r;
    ll add,sum;
}tr[N*4];
int w[N];
int n,m;
void pushup(int u){
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;//利用左右儿子信息维护当前节点的信息
}
void pushdown(int u){
    if(tr[u].add){
        tr[u<<1].add+=tr[u].add;
        tr[u<<1].sum+=1ll*(tr[u<<1].r-tr[u<<1].l+1)*tr[u].add;
        tr[u<<1|1].add+=tr[u].add;
        tr[u<<1|1].sum+=1ll*(tr[u<<1|1].r-tr[u<<1|1].l+1)*tr[u].add;
        tr[u].add=0;
    }
}
void build(int u,int l,int r){
    if(l==r)tr[u]={l,r,0,w[l]};
    else{
        tr[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void update(int u,int l,int r,int d){
    if(tr[u].l>=l&&tr[u].r<=r){
        tr[u].add+=d;
        tr[u].sum+=(tr[u].r-tr[u].l+l)*d;
    }else{
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)update(u<<1,l,r,d);
        if(r>mid)update(u<<1|1,l,r,d);
        pushup(u);
    }
}
ll query(int u,int l,int r){
    if(tr[u].l>=l&&tr[u].r<=r){
        return tr[u].sum;
    }else{
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        ll res=0;
        if(l<=mid)res=query(u<<1,l,r);
        if(r>mid)res+=query(u<<1|1,l,r);
        return res;
    }
}

区间加乘、区间和

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

const int N=1e5+10;
struct Node{
    int l,r;
    int sum,add,mul;//需要维护的信息和懒标记
}tr[N*4];
int n,m,p;
int w[N];
typedef long long ll;

void pushup(int u){
    tr[u].sum=(tr[u<<1].sum+tr[u<<1|1].sum)%p;
}
void eval(Node &root,int mul,int add){
    root.sum=(1ll*root.sum*mul+((ll)(root.r)-root.l+1)*add)%p;
    root.mul=1ll*root.mul*mul%p;
    root.add=(1ll*root.add*mul+add)%p;
}
void pushdown(int u){
    auto &root=tr[u];
    eval(tr[u<<1],root.mul,root.add);
    eval(tr[u<<1|1],root.mul,root.add);
    root.mul=1;
    root.add=0;
}
void build(int u,int l,int r){
    if(l==r)tr[u]={l,r,w[l],0,1};
    else{
        tr[u]={l,r,0,0,1};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}

void update(int u,int l,int r,int m,int d){
    if(tr[u].l>=l&&tr[u].r<=r){
        eval(tr[u],m,d);
    }else{
       	pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值