首页 > 其他分享 >线段树 __ 复习

线段树 __ 复习

时间:2022-10-18 17:23:29浏览次数:80  
标签:__ 复习 int 线段 tr sum 区间 节点

线段树的结构

为什么叫线段树?
因为它是把原序列以及其子序列(一个个线段)组织成一棵树的形式。树的根节点为原序列,子节点依次对半分序列,直到叶节点,叶节点是单个数,也没办法再往下分了。

例如[1,7]
image

可以发现:

  • 原本的区间,在最底层其实是一个元素构成的叶节点。

  • 从根节点向下递归地过程中,节点的长度逐渐缩小,类似一个不断二分的过程,最后确定为一个点。

  • 整棵线段树中,所有节点的个数是小于4n的(n是区间、线段的长度、也即叶节点个数)

还可以看出线段树将近是(最后一层不符合)一颗满二叉树,因此可以直接采用堆存储方式。
因此某一段 u 的

  • 父节点是 u>>1
  • 左儿子是 u<<1
  • 右儿子是 u<<1|1

又因为一颗线段树最多是一颗满二叉树,而满二叉树的最后一层是 \(n\) 个点,前面的点数是 \(n−1\) ,所以一共要 \(2×n−1\) 的空间
但由于线段树有可能最后一层节点还有子节点,比如说 \(n=10\) 的时候,如图:
image
最后一层是多出来的,而最后一层节点最多 \(2×n\) 个节点,最坏情况下就最右边两个节点,最右下角的一个节点的编号是 \(2×n−1+2×n=4×n−1\) ,所以线段树一般开 \(4×n\)。

存储的属性是:(一般而言)

  • 结点所表示区间(线段)的左端点L
  • 结点所表示区间(线段)的右端点R
  • 结点所表示区间(线段)的和sum

代码

//一般使用结构体来存储线段树,空间大小开四倍
struct Node{
    int l,r;  //维护的区间
    int v;   //维护的信息
} tree[N*4];

建树

思路
递归遍历初始区间:

  • 如果这个节点不是叶子节点,那么就分别遍历左子树和右子树

  • 如果是叶子节点
    把表示的区间记录下来,
    把线段树维护的信息也记录下来,维护的信息在叶子节点上基本上就是这个数本身

  • 递归回来的时,利用儿子节点的信息更新父亲节点的信息

时间复杂度 \(O(logn)\)

代码push_up+build

//利用它的两个儿子来算一下它的当前节点信息
void push_up(int u)
{
    tr[u].sum=tr[u<<1].sum + tr[u<<1|1].sum;//左儿子 u<<1 ,右儿子 u<<1|1
}

/*第一个参数,当前节点编号,第二个参数,左边界,第三个参数,右边界*/
void build(int u,int l,int r)
{
    //如果当前已经是叶节点了,那我们就直接赋值就可以了
    if(l==r)tr[u]={l,r,w[r]};
    else
    {
        tr[u]={l,r};//存储当前点表示的区间

        int mid=l+r>>1;//计算边界
        build(u<<1,l,mid);//递归左儿子
        build(u<<1|1,mid+1,r);//递归右儿子

        push_up(u);//做完两个儿子之后push_up,更新一下当前节点信息
    }
}

查询

思路
我们从根节点(根节点一定包含所有点,既包含修改区间)出发,一直往下走,直到当前区间中的元素全部都是被修改元素。

  • 当左区间包含整个被修改区间时,我们就递归到左区间;
  • 当右区间包含整个被修改区间时,我们就递归到右区间;
    否则区间的样子就如下图所示:
    image

此时该怎么办呢?
我们可以发现,被修改区间中的元素间,两两之间都不会产生影响。
所以,我们可以把被修改区间分解成两段,

  • 使得其中的一段完全在左区间,
  • 另一端完全在右区间。

很明显,直接在 \(mid\) 的位置将该区间切开是最好的。如下图所示:
image

如果当前区间包含了查找区间的话就直接返回值。

时间复杂度 \(O(nlogn)\)

代码query

query操作,用来查询某一段区间内的信息,

查询最大值:

//从u(根节点)节点开始查询[l,r]区间内的某一信息
int query(int u,int l,int r){
    if(tree[u].l>=l&&tree[u].r<=r) return tree[u].v;  //说明这一段的信息已经被完全包含,因此不需要继续向下递归,直接返回即可
    //否则需要判断该递归那一边
    //用 res 来表示一下我们的最大值
    int res=0;
    int mid=tree[u].l+tree[u].r >> 1;
    if(l<=mid) res=max(res,query(u<<1,l,r));  //看一下我们当前区间的中点和左边有没有交集
    if(mid<r) res=max(res,query(u<<1|1,l,r));  //看一下我们当前区间的中点和左边有没有交集
//切记是mid<r,无等号
    return res;
}

查询区间和:

int query(int u,int l,int r)
{
    if(l<=tr[u].l&&tr[u].r<=r)return tr[u].sum;
    int mid=tr[u].l+tr[u].r>>1;

    //先判断一下和左边有没有交集
    int sum=0;//用 sum 来表示一下我们的总和
    if(mid>=l)//看一下我们当前区间的中点和左边有没有交集
        sum+=query(u<<1,l,r);

    if(r>=mid+1)//看一下我们当前区间的中点和右边有没有交集
        sum+=query(u<<1|1,l,r);

    return sum;
}

单点修改

我们通过二分查找的形式找到要修改的点,然后把找的过程上的链都修改一下。

代码

用来修改某一叶子节点并更新其所有父节点

void modify(int u,int x,int v){   //从u节点开始递归查找,将编号为x的节点的值修改为v
    if(tree[u].l==x&&tree[u].r==x) tree[u].v=v;
    else{
        int mid=tree[u].l+tree[u].r>>1;
        //看一下 x 是在左半边还是在右半边
        if(x<=mid) modify(u<<1,x,v);
        else modify(u<<1|1,x,v);
        push_up(u);
    }
}

区间修改

引入懒标记和push_down。

懒标记含义:
修改区间包含当前区间,就在这个节点上打个标记。
如果一个点上打的一个懒标记,那么表示这个点的所有子节点都要变化这个懒标记(可以是加除 max , min , GCD 等等),

注意:

  1. 懒标记不包含当前点!
  2. 如果一个点有懒标记,要先把他的懒标记下传给他的儿子。
    (不向下传会遇到:左半部分+x,右半部分+y的情况,导致数据不匹配)

代码push_down+modify

区间加法为例

void push_down (int u) { //下传标记函数
    auto &root = tr[u],&left = tr[u << 1],&right = tr[u << 1 | 1];  //加了引用符号只要修改这个变量就会修改他被赋值的变量
    if (root.sum_tag) {  //有懒标记才下传
        left.tag += root.tag,left.sum += (LL)(left.r - left.l + 1) * root.tag;  //这里的子节点要加上懒标记,因为懒标记不包括当前节点
        right.tag += root.tag,right.sum += (LL)(right.r - right.l + 1) * root.tag;  //同理
        root.tag = 0;  //懒标记记得清零
    }
}
void modify (int u,int l,int r,int d) { //当前遍历到的点下标是u,要将区间[l,r]增加d
    if (l <= tr[u].l && tr[u].r <= r) {
        tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * d;
        tr[u].sum_tag += d;  //一个点所有的子节点都加上d,而前一行时加上根节点的数,因为不包括根节点。
        return ;
    }
    push_down (u);  //一定要分裂,只要记牢在递归左右区间之前,就要分裂
    int mid = tr[u].l + tr[u].r >> 1;  //注意时tr[u].l和tr[u].r
    if (l <= mid) modify (u << 1,l,r,d);  //左边有修改区间,就递归左半边
    if (r >= mid + 1) modify (u << 1 | 1,l,r,d);  //右边有修改区间,就递归右半边
    push_up (u);  //要记得要把这个点的信息更新一下
}

单点修改模板代码

AcWing 1275. 最大数

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 200010;

int m, p;
struct Node
{
    int l, r;
    int v;  // 区间[l, r]中的最大值
}tr[N * 4];

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

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);
}

int query(int u, int l, int r)
{
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].v;   // 树中节点,已经被完全包含在[l, r]中了

    int mid = tr[u].l + tr[u].r >> 1;
    int v = 0;
    if (l <= mid) v = query(u << 1, l, r);
    if (r > mid) v = max(v, query(u << 1 | 1, l, r));

    return v;
}

void modify(int u, int x, int v)
{
    if (tr[u].l == x && tr[u].r == x) tr[u].v = v;
    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);
    }
}


int main()
{
    int n = 0, last = 0;
    scanf("%d%d", &m, &p);
    build(1, 1, m);

    int x;
    char op[2];
    while (m -- )
    {
        scanf("%s%d", op, &x);
        if (*op == 'Q')
        {
            last = query(1, n - x + 1, n);
            printf("%d\n", last);
        }
        else
        {
            modify(1, n + 1, ((LL)last + x) % p);
            n ++ ;
        }
    }

    return 0;
}

区间修改模板代码

243. 一个简单的整数问题2

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 100010;

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

void pushup(int u)
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

void pushdown(int u)
{
    auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
    if (root.add)
    {
        left.add += root.add, left.sum += (LL)(left.r - left.l + 1) * root.add;
        right.add += root.add, right.sum += (LL)(right.r - right.l + 1) * root.add;
        root.add = 0;
    }
}

void build(int u, int l, int r)
{
    if (l == r) tr[u] = {l, r, w[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 modify(int u, int l, int r, int d)
{
    if (tr[u].l >= l && tr[u].r <= r)
    {
        tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * d;
        tr[u].add += d;
    }
    else    // 一定要分裂
    {
        pushdown(u);
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r, d);
        if (r > mid) modify(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;

    pushdown(u);
    int mid = tr[u].l + tr[u].r >> 1;
    LL sum = 0;
    if (l <= mid) sum = query(u << 1, l, r);
    if (r > mid) sum += query(u << 1 | 1, l, r);
    return sum;
}


int main()
{
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);

    build(1, 1, n);

    char op[2];
    int l, r, d;

    while (m -- )
    {
        scanf("%s%d%d", op, &l, &r);
        if (*op == 'C')
        {
            scanf("%d", &d);
            modify(1, l, r, d);
        }
        else printf("%lld\n", query(1, l, r));
    }

    return 0;
}

原文

作者:incra
链接:https://www.acwing.com/file_system/file/content/whole/index/content/6505356/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
作者:Perf
链接:https://www.acwing.com/solution/content/61919/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

作者:Elegant
链接:https://www.acwing.com/solution/content/40394/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

标签:__,复习,int,线段,tr,sum,区间,节点
From: https://www.cnblogs.com/kingwz/p/16803322.html

相关文章

  • FFmpeg:FIFO + PIPE
    FIFO将某些内容流式传输到stdout管道,即使在临时故障(网络中断)的情况下也继续以实时速率处理流,并尝试无限期地每秒恢复流式传输。ffmpeg-re-stream_loop-1-ihttps://f......
  • Oracle 左连接、右链接(+)
    用(+)来实现,这个+号可以这样来理解: +表示补充,即哪个表有加号,这个表就是匹配表。如果加号写在右表,左表就是全部显示,所以是左连接。显示所有工作人员的停车记录,包括无记录......
  • java--String类
     java--String类入门详细介绍,转载java实现一个String类,转载 ......
  • 网页在线客服代码-侧边悬浮在线客服/QQ/微信/电话代码
    什么是网页在线客服代码?在线客服系统是通过独立应用程序或嵌入式软件进行的近实时消息交换。早期互联网发展还不流行的时候,那时候的网页结构还比较单一,很多企业的网站上只......
  • vue3实战-完全掌握ref、reactive
    知道大家使用Vue3的时候有没有这样的疑惑,“ref、rective都能创建一个响应式对象,我该如何选择?”,“为什么响应式对象解构之后就失去了响应式?应该如何处理?”今天咱们就来......
  • Serverless 遇到 FinOps: Economical Serverless
    Serverless遇到FinOps:EconomicalServerless摘要:本文基于FunctionGraph在Serverless领域的FinOps探索和实践,提出业界首个Serverless函数总成本估计模型Key......
  • 从混合云到分布式云 (上篇)
      一、混合云混合云是一种云的使用模式,即用户同时使用私有云和公有云。在Flexera发布的《StateoftheCloudReport2022》报告中,类似且比较容易混淆的云使用模式还......
  • Kubernetes全栈架构师:基于世界500强的k8s实战(最新V1.21版本)
    Kubernetes全栈架构师:基于世界500强的k8s实战(最新V1.21版本) Kubernetes市场行情Kubernetes作为成熟的容器编排工具,在国内外很多公司、世界500强等企业已经落地使用,......
  • 2022-10-18 uniapp h5端 通过腾讯提供的api并输入对应的经纬度 获取城市
    首先说明一下这是h5端,是的,他娘的h5端。然后先用uni.getLocation(我用的是wgs84)获取到经纬度,什么?你告诉我pc端无法获取,老是报什么网络错误的错误,连手机端也是这样??哦多茄~~......
  • 驱动开发:内核特征码扫描PE代码段
    在笔者上一篇文章《驱动开发:内核特征码搜索函数封装》中为了定位特征的方便我们封装实现了一个可以传入数组实现的SearchSpecialCode定位函数,该定位函数其实还不能算的上简......