首页 > 其他分享 >线段树(超详解)

线段树(超详解)

时间:2024-10-12 19:48:12浏览次数:3  
标签:lazy qr int 线段 tree mid 详解 ql

线段树(超详解)

Author :铜陵一中 缪语博

在网上看了几个讲线段树的,都感觉不咋地,自己琢磨了几天,大致弄明白了。于是趁兴写了一篇关于线段树的文章,希望拯救那些看 o i − w i k i oi-wiki oi−wiki看不懂的 o i e r oier oier。

前言

在阅读本文之前,你需要明确:

  • 本人码风可能与你不同,请多谅解。
  • 有可能我的代码用到什么 d e f i n e define define可能和晚上的“简单”违背,请不要误解。写多了你就会知道,这样写真的很方便。

命名规则:

  • p p p:当前的线段树的节点。
  • l , r l,r l,r:当前线段树的左右区间范围。
  • q l , q r ql,qr ql,qr:目标线段树的左右区间范围。

C h a p t e r 1 Chapter 1 Chapter1 干嘛要用线段树?

Put simply,就是区间操作。题目中出现区间,大概率就是线段树了。有人问:我直接一个数组不就行了吗?

N o , N o , N o , s l o w ! No,No,No,slow! No,No,No,slow!

假设有 1 0 6 10^6 106个操作,每一次操作你都要修改,求和,求最大值,更新

T L E ! TLE! TLE!

线段树就是来解决这个问题的。

但是为什么呢?

一个类似前缀和的思想。思考这样一个问题:假如你做人口普查,铜陵市政府统计了铜陵市的人口。现在安徽省政府来统计,还需要统计铜陵市的人口吗?

显然不需要,直接把铜陵市政府的数据拿来用不就行了吗?

同理,你已经算出了某个区间的数据,你直接拿来用就可以了,干嘛还要再算一遍?你是嫌 1 s 1s 1s的时间限制短了?

那么问题来了,怎么操作呢?

C h a p t e r 2 Chapter 2 Chapter2 什么是线段树?怎么建线段树?

在学习之前,你得有一些树的基本知识,比如说:

  • 什么是树(废话)。
  • 在一棵完全二叉树中(根节点编号为 1 1 1),节点 p p p的左儿子的编号为 2 p 2p 2p,右儿子的编号为 2 p + 1 2p+1 2p+1。

先来了解一下线段树为什么快。

试问:怎么查找最快?

二分。

对!线段树就可以理解为“二分”,二分区间,这样查找就会变得很快,直降 O ( l o g n ) O(logn) O(logn)

这样,我们就可以开始建树了。

建树过程(OI-Wiki上写得已经够详细了,移步一下吧)。

链接OI-Wiki

好的,默认你已经知道了线段树长什么样子了。

对于一个非叶子节点 p p p,其均有一个左子树和右子树,刚刚才讲过,左儿子的编号为 2 p 2p 2p,右儿子的编号为 2 p + 1 2p+1 2p+1。

为了方便起见,我们使用 d e f i n e define define来简便定义左子树和右子树。

#define ls (p << 1)
#define rs (p << 1 | 1)
  • 其中, (p << 1)(p << 1 | 1)的意思分别是 2 × p 2\times p 2×p和 2 × p + 1 2\times p + 1 2×p+1,这样定义更加简便快速。

于是我们开始建树。

首先,对于一个区间 [ l , r ] [l,r] [l,r],如果访问时, l = r l = r l=r,那么其就是叶子结点,否则就不是叶子结点(废话)。我习惯于将 l , r l,r l,r放在参数里传递,而不是用结构体来定义,我认为这样可能会简便一些。

有人问:那节点的区间长度如何定义叻?

用一个siz数组不就行了吗?

以建立一棵求区间和的线段树为例。

  • 这里还有一个定义,就是 m i d mid mid的定义,也使用宏定义:#define mid ((l + r) >> 1)

#define N 100001
#define ll long long
#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

int n, m;
int a[N];
ll tree[N << 2];
int siz[N << 2];
int lazy[N << 2];

void build(int p, int l, int r) {
    lazy[p] = 0;
    if(l == r)  {
        return ;
    }
    build(ls, l, mid);
    build(rs, mid + 1, r);
}
  • 这里的 l a z y lazy lazy数组你暂时可以不用管,这是以后要讲到的。

C h a p t e r 3 Chapter 3 Chapter3 线段树的初始化

简单了,加几行就行了。

首先是叶子结点的数据,直接放区间(节点)所对应的值就好了。

然后是非叶子结点的维护,用一个upd函数来更新tree的值,用一个upds函数来更新siz的大小。


void upd(int p) {
    tree[p] = tree[ls] + tree[rs];
}

void upds(int p) {
    siz[p] = siz[ls] + siz[rs];
}

void build(int p, int l, int r) {

    lazy[p] = 0;

    if(l == r)  {
        siz[p] = 1;
        tree[p] = a[l];
        return ;
    }

    build(ls, l, mid);
    build(rs, mid + 1, r);

    upd(p);
    upds(p);
}

C h a p t e r 4 Chapter 4 Chapter4 线段树的查询

还是以建立一棵求区间和的线段树为例。

泰见但辣!

如果当前的区间 [ l , r ] [l,r] [l,r]被完全包含于查询区间 [ q l , q r ] [ql, qr] [ql,qr],直接加和即可。

如果没有被完全包含,拆成它的左子树和右子树,不断缩小范围就行了。

这里理解一个问题:我怎么知道应该拆左子树还是拆右子树?

比如说,当有这样一种情况:

[ l , r ] = [ 4 , 7 ] [l,r]=[4,7] [l,r]=[4,7], [ q l , q r ] = [ 3 , 5 ] [ql,qr]=[3,5] [ql,qr]=[3,5]

发现没有被完全包含,其中, q r ≥ l qr \geq l qr≥l,所以可以看它的左子树和右子树。如果左子树满足条件,就既搜左子树,又搜右子树,反复递归,直至区间被完全覆盖。如果只有右子树满足条件,就只搜右子树,直至区间被完全覆盖。

ll qry(int p, int l, int r, int ql, int qr) {
    if(ql <= l && r <= qr) {
        return tree[p];
    }

    ll sum = 0;

    if(ql <= mid) {
        sum += qry(ls, l, mid, ql, qr);
    }
    if(qr > mid) {
        sum += qry(rs, mid + 1, r, ql, qr);
    }

    return sum;
}

C h a p t e r 5 Chapter 5 Chapter5 懒标记

很重要的一部分,一定要反复看,比较难理解。

什么是懒标记?

就是懒(废话)。

为什么?

想一想,如果我每一次增加区间的值,每一次更新都全部下放到子树,那时间复杂度就是无法估量的。所以,只有碰到查询时,或者要更改这个区间的一部分的时候才会全部下放到子树,并且是下放到儿子结点,这样做会更快(想一想,为什么)。

定义: l a z y [ p ] lazy[p] lazy[p]表示当结点 p p p的 t r e e tree tree值已经更新时,其儿子结点还没有下放的数值。可能很少有文章强调当结点 p p p的 t r e e tree tree值已经更新时,但是这个地方理解很重要!这样可以使你的思路更加清晰。

于是,我们得到了一个下放结点 p p p的懒标记的代码:

void pushd(int p) {
    tree[ls] += lazy[p] * siz[ls];
    tree[rs] += lazy[p] * siz[rs];

    lazy[ls] += lazy[p];
    lazy[rs] += lazy[p];

    lazy[p] = 0;
}

在更新中的具体代码下节讲,在查询中的放在结尾的代码里,自行理解。

C h a p t e r 6 Chapter 6 Chapter6 更新区间

还是以上面那个例子为例(有语病吗?),更新区间是将区间内所有的值加 k k k。

现在就很好理解了。

  1. 如果当前结点被完全覆盖,直接将 t r e e tree tree值加上 s i z [ p ] × k siz[p] \times k siz[p]×k即可。
  2. 如果没有,继续拆。
  3. 注意下放懒标记!
void mdf(int p, int l, int r, int ql, int qr, int k) {
    if(ql <= l && r <= qr) {
        tree[p] += 1ll * siz[p] * k;
        lazy[p] += k;
        return;
    }
    pushd(p);
    if(ql <= mid) {
        mdf(ls, l, mid, ql, qr, k);
    }
    if(qr > mid) {
        mdf(rs, mid + 1, r, ql, qr, k);
    }
    upd(p);
}

C h a p t e r 7 Chapter 7 Chapter7 终章 C o d e Code Code

#include <bits/stdc++.h>

#define N 100001
#define ll long long
#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

using namespace std;

int n, m;
int a[N];
ll tree[N << 2];
int siz[N << 2];
int lazy[N << 2];

void upd(int p) {
    tree[p] = tree[ls] + tree[rs];
}

void upds(int p) {
    siz[p] = siz[ls] + siz[rs];
}

void pushd(int p) {
    tree[ls] += lazy[p] * siz[ls];
    tree[rs] += lazy[p] * siz[rs];

    lazy[ls] += lazy[p];
    lazy[rs] += lazy[p];

    lazy[p] = 0;
}

void build(int p, int l, int r) {

    lazy[p] = 0;

    if(l == r)  {
        siz[p] = 1;
        tree[p] = a[l];
        return ;
    }

    build(ls, l, mid);
    build(rs, mid + 1, r);

    upd(p);
    upds(p);
}

void mdf(int p, int l, int r, int ql, int qr, int k) {
    if(ql <= l && r <= qr) {
        tree[p] += 1ll * siz[p] * k;
        lazy[p] += k;
        return;
    }
    pushd(p);
    if(ql <= mid) {
        mdf(ls, l, mid, ql, qr, k);
    }
    if(qr > mid) {
        mdf(rs, mid + 1, r, ql, qr, k);
    }
    upd(p);
}

ll qry(int p, int l, int r, int ql, int qr) {
    if(ql <= l && r <= qr) {
        return tree[p];
    }
    pushd(p);

    ll sum = 0;

    if(ql <= mid) {
        sum += qry(ls, l, mid, ql, qr);
    }
    if(qr > mid) {
        sum += qry(rs, mid + 1, r, ql, qr);
    }

    return sum;
}

int main() {

    cin >> n >> m;

    for(int i = 1; i <= n; ++i) {
        cin >> a[i];
    }

    build(1, 1, n);

    while(m--) {
        int op, x, y, k;

        cin >> op >> x >> y;

        if(op == 1) {
            cin >> k;
            mdf(1, 1, n, x, y, k);
        }
        else {
            cout << qry(1, 1, n, x, y) << endl;
        }
    }

    return 0;
}

也希望看完这篇文章的你能点一个大大的赞,给一个大大的支持!

My Website

My Zhihu

完结撒花!

标签:lazy,qr,int,线段,tree,mid,详解,ql
From: https://blog.csdn.net/MYB20091111/article/details/142864846

相关文章

  • 冒泡排序 (Bubble Sort) 详解
    一、概述冒泡排序(BubbleSort)是一种基础的排序算法,属于交换排序的一种。它通过重复遍历要排序的数列,比较相邻的元素并交换它们的位置,逐步将最大的(或最小的)元素“冒泡”到数列的末端,从而完成排序。冒泡排序的工作原理非常直观易懂,尽管它的性能并不算最优,但作为入门级的排序算法,它能......
  • 智慧农业模型一一详解:DSSAT、APSIM、DNDC模型、WOFOST与PCSE、AquaCrop农水、SWAP、作
    目录全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用双碳目标下DNDC模型建模方法及在土壤碳储量、温室气体排放、农田减排、土地变化、气候变化中的实践技术应用AquaCrop模型农业水资源管理及代码解析WOFOST模型与PCSE模型实践技术应用基于R语言APSIM......
  • 万字详解AI实践,零手写编码用AI完成开发 + 数据清洗 + 数据处理 的每日新闻推荐,带你快
    用AI+dify完成前后端开发+数据处理和数据清洗。引言数据获取和数据处理dify构建workflow进行数据清洗前端页面构建和前后端交互总结引言AI时代对开发人员的加强是非常明显的,一个开发人员可以依靠AI横跨数个自己不熟悉的领域包括前后端、算法等。让我们来做个实践,全程......
  • Nuxt.js 应用中的 ready 事件钩子详解
    title:Nuxt.js应用中的ready事件钩子详解date:2024/10/12updated:2024/10/12author:cmdragonexcerpt:ready钩子是Nuxt.js中一个重要的生命周期事件,它在Nuxt实例初始化完成后被调用。当Nuxt已经准备好并准备开始处理请求或渲染页面时,这一钩子会被触发。cate......
  • 地图瓦片详解:快速提升地图加载速度的关键技术
    地图瓦片(MapTiles)是指将一张完整的大型地图或影像切割成若干个小的矩形图块,每个图块(瓦片)代表地图的一部分。地图瓦片技术被广泛用于网络地图和地理信息系统(GIS)中,主要目的是为了提升地图的加载速度和用户体验,特别是在多级缩放和大规模数据处理的情况下。地图瓦片的基本概念地图......
  • 电商新动力:SpringBoot购物推荐网站开发详解
    2相关技术2.1MYSQL数据库MySQL是一个真正的多用户、多线程SQL数据库服务器。是基于SQL的客户/服务器模式的关系数据库管理系统,它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等,非常适用于Web站点或者其他......
  • 网络安全学习路线图(2024版详解)
    近期,大家在网上对于网络安全讨论比较多,想要学习的人也不少,但是需要学习哪些内容,按照什么顺序去学习呢?其实我们已经出国多版本的网络安全学习路线图,一直以来效果也比较不错,本次我们针对市场需求,整理了一套系统的网络安全学习路线图,供大家学习参考。希望大家按照路线图进行系......
  • 三、Spring Boot集成Spring Security之securityFilterChain过滤器链详解
    二、默认过滤器链1、默认配置系统启动日志2、默认配置的过滤器及顺序如下org.springframework.security.web.session.DisableEncodeUrlFilterorg.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilterorg.springframework.security.web.c......
  • 电脑快速切换IP地址命令是什么?详解与实践
    有时,出于安全考虑或测试需要,我们可能需要快速切换电脑的IP地址。虽然这一过程在初学者看来可能略显复杂,但通过简单的命令和步骤,即使是普通用户也能轻松实现。本文将详细介绍在Windows系统中快速切换IP地址的几种方法,特别是通过命令提示符来执行的操作。一、IP地址与网络环境......
  • 神经网络之卷积篇:详解经典网络(Classic networks)
    详解经典网络首先看看LeNet-5的网络结构,假设有一张32×32×1的图片,LeNet-5可以识别图中的手写数字,比如像这样手写数字7。LeNet-5是针对灰度图片训练的,所以图片的大小只有32×32×1。实际上LeNet-5的结构和上篇博客的最后一个范例非常相似,使用6个5×5的过滤器,步幅为1。由于使用了6......