线段树算法(C++/C)

目录​​​​​​​

一、线段树算法的概念

二、为什么需要线段树

三、线段树算法的实现

(1)建树

(2)查询

(3)修改

(4)综合代码,求区间和

(5)综合代码,求区间最大值

四、Lazy标记


一、线段树算法的概念

线段树(Segment Tree)是一种基于二分思想的数据结构,常常用于处理区间查询和区间修改。线段树的常用操作包括建树、查询、修改。

线段树的建树过程可以使用递归实现,也可以使用非递归实现(通常使用栈来实现)。

线段树的查询和修改基本都是从根节点开始,往下遍历到叶子节点或者与查询区间(或修改区间)不相交的节点为止。线段树相关问题经常需要使用懒惰标记(Lazy Tag)来优化。

线段树常用于以下场景:区间最值查询、区间求和、区间修改等

二、为什么需要线段树

考虑这样两个场景:

  1. 对于一个长度为 n 的数组,现在给定 l,r 让你求 l 到 r 所有元素的和,有多个这样的询问.
  2. 对于一个长度为 n 的数组,现在对数组的第 k 个元素进行修改后,给定 l,r 让你求 l 到 r 所有元素的和,有多个这样的询问.

大家看到第一种情况的时候,这不就是前缀和,是的.第二种情况呢,前缀和还能不能用,显然每次修改之后,前缀和就不能使用了,所以又退化为 O(n) 的时间复杂度了.

此时我们就需要用到我们的线段树了.

对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。最后的子节点数目为 N,即整个线段区间的长度。

我们看一下 1-10 的线段树是如何存储的.

三、线段树算法的实现

(1)建树

void build(int p, int l, int r) {
    t[p].l = l, t[p].r = r; // 节点p代表区间[l,r]
    if (l == r) { t[p].dat = a[l]; return; } // 叶节点
    int mid = (l + r) / 2; // 折半
    build(p*2, l, mid); // 左子节点[l,mid],编号p*2
    build(p*2+1, mid+1, r); // 右子节点[mid+1,r],编号p*2+1
    // t[p].dat = max(t[p*2].dat, t[p*2+1].dat); // 从下往上传递信息
    t[p].dat = t[p*2].dat+t[p*2+1].dat; // 从下往上传递信息
}

(2)查询

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) / 2;
    int val = 0;
    if (l <= mid) val = val+ ask(p*2, l, r); // 左子节点有重叠
    if (r > mid) val = val+ask(p*2+1, l, r); // 右子节点有重叠
    return val;
}

(3)修改

void change(int p, int x, int v) {
    if (t[p].l == t[p].r) { t[p].dat = v; return; } // 找到叶节点
    int mid = (t[p].l + t[p].r) / 2;
    if (x <= mid) change(p*2, x, v); // x属于左半区间
    else change(p*2+1, x, v); // x属于右半区间
    // t[p].dat = max(t[p*2].dat, t[p*2+1].dat); // 从下往上更新信息
    t[p].dat = t[p*2].dat+t[p*2+1].dat;
}

(4)综合代码,求区间和

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

const int SIZE=11;

int a[11]={0,1,2,3,4,5,6,7,8,9,10}; //原始数据

struct SegmentTree {
    int l, r;
    int dat;
} t[SIZE * 4]; // struct数组存储线段树

void build(int p, int l, int r) {
    t[p].l = l, t[p].r = r; // 节点p代表区间[l,r]
    if (l == r) { t[p].dat = a[l]; return; } // 叶节点
    int mid = (l + r) / 2; // 折半
    build(p*2, l, mid); // 左子节点[l,mid],编号p*2
    build(p*2+1, mid+1, r); // 右子节点[mid+1,r],编号p*2+1
    // t[p].dat = max(t[p*2].dat, t[p*2+1].dat); // 从下往上传递信息
    t[p].dat = t[p*2].dat+t[p*2+1].dat; // 从下往上传递信息
}

void change(int p, int x, int v) {
    if (t[p].l == t[p].r) { t[p].dat = v; return; } // 找到叶节点
    int mid = (t[p].l + t[p].r) / 2;
    if (x <= mid) change(p*2, x, v); // x属于左半区间
    else change(p*2+1, x, v); // x属于右半区间
    // t[p].dat = max(t[p*2].dat, t[p*2+1].dat); // 从下往上更新信息
    t[p].dat = t[p*2].dat+t[p*2+1].dat;
}

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) / 2;
    int val = 0;
    if (l <= mid) val = val+ ask(p*2, l, r); // 左子节点有重叠
    if (r > mid) val = val+ask(p*2+1, l, r); // 右子节点有重叠
    return val;
}
int main()
{
    //建树从根节点一点一点往下建立,所以第一个参数就是1号编号
    build(1,1,10);
    //查询区间[4,7]的和,第一个参数是1的原因是查询要从根节点开始递归
    int ans=ask(1,4,7);
    cout<<ans;
    //修改位置4的值变为25,第一个参数是1的原因是修改也要从根节点开始一步一步往下进行修改
    change(1,4,25);
    ans=ask(1,4,7);
    cout<<ans;
    return 0;

}

(5)综合代码,求区间最大值

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

const int SIZE=11;

int a[11]={0,1,2,3,4,5,6,7,8,9,10}; //原始数据

struct SegmentTree {
    int l, r;
    int dat;
} t[SIZE * 4]; // struct数组存储线段树

void build(int p, int l, int r) {
    t[p].l = l, t[p].r = r; // 节点p代表区间[l,r]
    if (l == r) { t[p].dat = a[l]; return; } // 叶节点
    int mid = (l + r) / 2; // 折半
    build(p*2, l, mid); // 左子节点[l,mid],编号p*2
    build(p*2+1, mid+1, r); // 右子节点[mid+1,r],编号p*2+1
    // t[p].dat = max(t[p*2].dat, t[p*2+1].dat); // 从下往上传递信息
    t[p].dat = max( t[p * 2].dat , t[p * 2 + 1].dat); // 从下往上传递信息
}

void change(int p, int x, int v) {
    if (t[p].l == t[p].r) { t[p].dat = v; return; } // 找到叶节点
    int mid = (t[p].l + t[p].r) / 2;
    if (x <= mid) change(p*2, x, v); // x属于左半区间
    else change(p*2+1, x, v); // x属于右半区间
    // t[p].dat = max(t[p*2].dat, t[p*2+1].dat); // 从下往上更新信息
    t[p].dat = max( t[p * 2].dat , t[p * 2 + 1].dat);
}

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) / 2;
    int val = 0;
    if (l <= mid) val = max(val, ask(p * 2, l, r)); // 左子节点有重叠
    if (r > mid) val =  max(val, ask(p * 2 + 1, l, r)); // 右子节点有重叠
    return val;
}
int main()
{
    build(1,1,10);
    int ans=ask(1,4,7);
    cout<<ans;
    change(1,4,25);
    ans=ask(1,4,7);
    cout<<ans;
    return 0;

}

四、Lazy标记

这种类型的题目,一般都是这样问的:如果每次是对一个区间进行修改,比如让 l,r 区间内的每个值都加 30.然后求和。

如果我们换成对于点的修改,那么时间复杂就太高了.那我们怎么办呢?

我们可以使用 Lazy 标记的方式,进行处理,什么是 Lazy 标记?

若在一次修改操作中发现节点 p 所代表的区间 [pl​,pr​] 被修改区间 [l,r] 完全覆盖,并且随后的查询操作没有利用到范围 [l,r] 的子区间作为候选答案,那么对节点 p 及其子树进行的更新操作将是没有实际效果的。此情况下,我们需要考虑优化算法,避免对整棵子树进行无意义的更新。

在执行修改指令时,如果发现存在 l < pl​ < pr​ < r 的情况,可以立即返回,并在回溯之前向节点 p 添加一个 Lazy 标记,用于表示 '该节点曾被修改,但其子节点尚未被更新'。

在后续的指令中,若需要向下递归至节点 p,应检查节点 p 是否带有标记。如果存在标记,应根据标记信息更新节点 p 的两个子节点,并为这两个子节点添加标记。然后清除节点 p 的标记。

除了在修改指令中直接划分成的 O(logN) 个节点外,对任意节点的修改都延迟到 '在后续操作中递归进入它的父节点时' 再执行。这样一来,每条查询或修改指令的时间复杂度都降低到了 O(logN)。我们将这些标记称为 '延迟标记',它们提供了线段树中从上往下传递信息的方式。通过延迟标记的设计,我们能够更加高效地处理线段树操作。

这种 '延迟' 的思想是设计算法与解决问题时的一个重要思路,它充分利用了操作的特性,避免了不必要的计算,并提升了算法的效率。延迟标记的应用为线段树操作提供了一种优化策略,使得算法的时间复杂度得以降低。

那我们该如何设计呢.

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

const int SIZE = 11;

int a[11] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 原始数据

struct SegmentTree
{
    int l, r;
    long long sum, add;
} tree[SIZE * 4]; // struct数组存储线段树

void build(int p, int l, int r)
{
    tree[p].l = l, tree[p].r = r;
    if (l == r)
    {
        tree[p].sum = a[l];
        return;
    }
    int mid = (l + r) / 2;
    build(p * 2, l, mid);          // 构建左子树
    build(p * 2 + 1, mid + 1, r);  // 构建右子树
    tree[p].sum = tree[p * 2].sum + tree[p * 2 + 1].sum;  // 更新节点的区间和
}

void spread(int p)
{
    // 下传延迟标记
    if (tree[p].add)
    {
        tree[p * 2].sum += tree[p].add * (tree[p * 2].r - tree[p * 2].l + 1);
        tree[p * 2 + 1].sum += tree[p].add * (tree[p * 2 + 1].r - tree[p * 2 + 1].l + 1);
        tree[p * 2].add += tree[p].add;     // 左子树打延迟标记
        tree[p * 2 + 1].add += tree[p].add; // 右子树打延迟标记
        tree[p].add = 0;                    // 清除延迟标记
    }
}

void change(int p, int l, int r, int d)
{
    if (l <= tree[p].l && r >= tree[p].r)
    {
        // 完全覆盖节点的区间
        tree[p].sum = (long long)d * (tree[p].r - tree[p].l + 1);  // 更新节点的区间和
        tree[p].add += d;                                         // 打延迟标记
        return;
    }
    spread(p);
    int mid = (tree[p].l + tree[p].r) / 2;
    if (l <= mid)
        change(p * 2, l, r, d);         // 修改左子树
    if (r > mid)
        change(p * 2 + 1, l, r, d);     // 修改右子树
    tree[p].sum = tree[p * 2].sum + tree[p * 2 + 1].sum;  // 更新节点的区间和
}

long long ask(int p, int l, int r)
{
    if (l <= tree[p].l && r >= tree[p].r)
        return tree[p].sum;
    spread(p);
    int mid = (tree[p].l + tree[p].r) / 2;
    long long val = 0;
    if (l <= mid)
        val += ask(p * 2, l, r);          // 查询左子树
    if (r > mid)
        val += ask(p * 2 + 1, l, r);      // 查询右子树
    return val;
}

int main()
{
    build(1, 1, 10);
    int ans = ask(1, 4, 7);
    cout << ans << endl;
    change(1, 4, 5, 10);
    ans = ask(1, 4, 7);
    cout << ans << endl;
    return 0;
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/27374.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python暑假自律打卡学习班,免费,速来(2)

小朋友们好&#xff0c;大朋友们好&#xff01; 我是猫妹&#xff0c;一名爱上Python编程的小学生。 和猫妹学Python&#xff0c;一起趣味学编程。 很快就放暑假了&#xff0c;还有20多天吧&#xff01; 猫妹对这个暑假相当期待啊&#xff0c; 想想今年的五一劳动节有多火爆…

Openharmony添加编译自己应用

介绍一下Openharmony如何在庞大的编译构建系统中&#xff0c;增添自己想编译的内容。不定期更新~&#x1f438; gn官方文档&#xff1a; https://gn.googlesource.com/gn//main/docs/quick_start.md https://gn.googlesource.com/gn//master/docs/reference.md openharmony官…

Redis 消息队列 Stream

tip&#xff1a;作为程序员一定学习编程之道&#xff0c;一定要对代码的编写有追求&#xff0c;不能实现就完事了。我们应该让自己写的代码更加优雅&#xff0c;即使这会费时费力。 &#x1f495;&#x1f495; 推荐&#xff1a;体系化学习Java&#xff08;Java面试专题&#…

C语言写网络爬虫总体思路

使用C语言编写爬虫可以实现网络数据的快速获取和处理&#xff0c;适用于需要高效处理海量数据的场景。与其他编程语言相比&#xff0c;C语言具有较高的性能和灵活性&#xff0c;可以进行底层操作和内存管理&#xff0c;适合处理较复杂的网络请求和数据处理任务。 但是&#xf…

Redis学习总结(二)

AOF 为什么是在执行完命令之后记录日志&#xff1f; 关系型数据库&#xff08;如 MySQL&#xff09;通常都是执行命令之前记录日志&#xff08;方便故障恢复&#xff09;&#xff0c;而 Redis AOF 持久化机制是在执行完命令之后再记录日志。AOF 记录日志过程为什么是在执行完命…

【LeetCode】HOT 100(7)

题单介绍&#xff1a; 精选 100 道力扣&#xff08;LeetCode&#xff09;上最热门的题目&#xff0c;适合初识算法与数据结构的新手和想要在短时间内高效提升的人&#xff0c;熟练掌握这 100 道题&#xff0c;你就已经具备了在代码世界通行的基本能力。 目录 题单介绍&#…

chatgpt赋能python:Python创建SEO文章的指南

#Python创建SEO文章的指南 在当今数字化世界中&#xff0c;SEO&#xff08;搜索引擎优化&#xff09;对于拥有一个成功的在线业务至关重要。SEO文章不仅可以帮助提高网站的排名&#xff0c;还可以吸引更多的访问者并提高转化率。在本文中&#xff0c;我们将介绍如何使用Python…

数据分析第17课seaborn绘图

关系型绘图 seaborn.relplot() 这个函数功能非常强大,可以用来表示多个变量之间的关联关系。默认情况下是绘制散点图(散点图是看到变量与变量之间相关性最优的一个图形),也可以绘制线性图,具体绘制什么图形是通过kind参数来决定的。实际上以下两个函数就是relplot的特例…

2023网安面试题170道,轻松应对面试

最近有不少小伙伴跑来咨询&#xff1a; 想找网络安全工作&#xff0c;应该要怎么进行技术面试准备&#xff1f; 工作不到 2 年&#xff0c;想跳槽看下机会&#xff0c;有没有相关的面试题呢&#xff1f; 为了更好地帮助大家高薪就业&#xff0c;今天就给大家分享两份网络安全工…

(1Gbit)MT28EW01GABA1LPC-0SIT、MT28EW01GABA1HPC-0SIT FLASH - NOR 存储器

MT28EW01GABA1LPC-0SIT、MT28EW01GABA1HPC-0SIT 1Gbit并行NOR闪存器件具有较高的密度、就地执行 (XiP) 性能和架构灵活性&#xff0c;可满足汽车、消费类和移动产品的设计要求。该器件非常适合用于GPS/导航、汽车后视摄像头、手机、智能手机和电子阅读器。该器件还具有较宽的温…

使用OpenFlow和Ryu控制器实现网络交换机的软件定义网络(SDN)控制

使用OpenFlow和Ryu控制器实现网络交换机的软件定义网络&#xff08;SDN&#xff09;控制 &#xff08;1&#xff09;环境介绍 硬件环境&#xff1a;系统最低要求为2个CPU 、2 GB内存。 拓扑介绍&#xff1a;云平台具体安装拓扑如图5-4所示。 图5-4 云平台安装拓扑 搭建云平…

NodeJS MongoDB⑦

文章目录 ✨文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持&#x1f618;前言Node&MongoDB 第一步 连接数据库 第二步 创建User Mongodb模型 第三步 简单使用 Mongodb命令 第四步 规范使用 Mongodb命令 &#xff08…

解数独--难的一批

1题目 编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例图&#xff09; 数…

RHEL7同步ntp时间

RHEL7同步ntp时间 RHEL7同步ntp时间测试ntp服务器是否可用抓包分析ntp 查看NTP同步情况ntp服务器配置文件将ntp配置迁移到chronytimedatectl设置时区和时间设置UTC或RTC时间查看所有可用时区查看当前时区设置系统时区启用夏令时timedatectl时间同步timedatectl修改当前日期时间…

基于ADME的分子过滤和 lead-likeness标准

T002 基于ADME的分子过滤和 lead-likeness标准 项目来源于TeachOpenCADD 本文目标 在药物设计的背景下&#xff0c;重要的是通过例如它们的物理化学性质来过滤候选分子。 在这个教程中&#xff0c;从 ChEMBL ( Talktorial T001 )获得的化合物将按照 Lipinsik 的五法则进行…

卷积编码和维特比译码

文章目录 卷积编码维特比译码 卷积编码 卷积码是一种非分组码&#xff0c;通常适用于前向纠错。在分组码中&#xff0c;编码器产生的 n 个码元的一个码组&#xff0c;完全决定于这段时间中 k 比特输入信息。这个码组中的监督位仅监督本码组中 k 个信息位。卷积码在编码时虽然也…

【马蹄集】第十四周作业

第十四周作业 目录 MT2134 泡泡MT2135 调整队伍MT2141 快排变形MT2142 逆序MT2143 线段树 MT2134 泡泡 难度&#xff1a;黄金    时间限制&#xff1a;1秒    占用内存&#xff1a;128M 题目描述 小码哥小时候喜欢吹泡泡&#xff0c;有一次他吹出了 n n n 个一样小的泡泡&…

SSM-Spring+SpringMVC+MyBatis框架的水果商城网站

项目介绍 主要功能&#xff1a; 前端用户购物端&#xff1a; ①角色信息&#xff1a;用户注册、用户登录、个人中心 ②个人中心&#xff1a;基本信息、我的订单、商品收藏、修改密码 ③首页管理&#xff1a;公告、留言、折扣大促销、热门商品 ④商品详情&#xff1a;收藏、加入…

使用阿里云OSS实现图片文件上传

说明&#xff1a;注册用户时&#xff0c;经常会用到上传头像。文件的上传/接收与一般文本数据不同。 一、创建Demo页面 先准备一个Demo页面 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>图片上传…

影响电磁铁磁力大小的因素有哪些

影响电磁铁磁力大小的因素主要有四个&#xff0c;一是缠绕在铁芯上线圈的圈数&#xff0c;二是线圈中电流的强度&#xff0c;三是缠绕的线圈与铁芯的距离&#xff0c;四是铁芯的大小形状。 首先要了解电磁铁的磁性是如何产生的&#xff0c;通电螺线管的磁场&#xff0c;由毕奥&…