leetcode|刷算法 线段树原理以及模板

news2024/10/1 7:47:06
线段树出现的题目特征

线段树使用的题目。每次操作都要得到返回结果的。

比如

699. 掉落的方块 - 力扣(LeetCode)

2286. 以组为单位订音乐会的门票 - 力扣(LeetCode)

1845. 座位预约管理系统 - 力扣(LeetCode)

线段树的原理

线段树的原理就是使用递归的思想对数值进行更新。

比如查询6到10区间上的最大值。如果传统的需要遍历一遍。但是如果有一个node刚好记录了这个范围6到10上的最大值,那么直接将这里的node上的数值返回即可。

另外,除了最大值,还有一个范围上的累加和,使得一个范围上进行更新操作,一个范围上进行累加操作。

原数组 orginArr

范围1到n上,需要使用4*(n+1) 的长度来记录树 使用arrSum表示

节点使用node标识,范围左边是left,右边是right

根节点就是 node=1 left=1 right=n

任意节点的左节点就是 node*2 左边边界 left 右边边界 right/2

任意节点的右节点就是 node*2+1 左边边界 right/2+1 右边边界 right

当左边界,右边界相同时候。left也就是1-n范围上的这个值对应的 arrSum[node] 就是表示原数组的该值origin[left]

线段树的模板

模板改自左程云老师的模板,属于二次改造,就高级写法进行降级,使得易懂。对单字母的变量进行多个字母命名,使得看着没有这么眼花


    public static void main(String[] args) {
        int[] origin = {2, 1, 1, 2, 3, 4, 5};
        SegmentTree seg = new SegmentTree(origin);
        int start = 2;
        int end = 5;
        int value = 4;
        seg.add(start, end, value);  // start到end区间上统一增加值
        seg.update(start, end, value);  //  start到end区间上统一更新
        long sum = seg.querySum(start, end);  //  start到end区间求和
    }

这里入参会看着简洁些。start,end,value 也容易理解

另外的参数是left,right,node。

对于维护的数组,sumArr 树节点上的累加和 ,taskArr 树节点上的需要下发的增加数值的任务,changeArr 树节点上的需要下发的修改任务

taskArr[node] = 5 node的left和right。比如为6和10,也就是范围6到10上,都需要增加10

模板如下
public static void main(String[] args) {
    int[] origin = {2, 1, 1, 2, 3, 4, 5};
    SegmentTree seg = new SegmentTree(origin);
    int start = 2;
    int end = 5;
    int value = 4;
    seg.add(start, end, value);  // start到end区间上统一增加值
    seg.update(start, end, value);  //  start到end区间上统一更新
    long sum = seg.querySum(start, end);  //  start到end区间求和
    System.out.println(sum);
    System.out.println("对数器测试开始...");
    System.out.println("测试结果 : " + (test() ? "通过" : "未通过"));

}

// 线段树模板
public static class SegmentTree {
    int right;
    int left = 1;
    int[] arr;
    int[] sumArr;
    int[] taskArr;
    int[] changeArr;
    boolean[] updateArr;

    public SegmentTree(int[] origin) {
        right = origin.length;
        int length = right + 1;
        arr = new int[length];
        for (int i = 1; i < length; i++) {
            arr[i] = origin[i - 1];
        }
        sumArr = new int[length * 4];
        taskArr = new int[length * 4];
        // changeArr和updateArr是一起使用的,但是一般题目可以不需要updateArr,因为通常修改的数值是大于0的。可以用是否为0来判断
        changeArr = new int[length * 4];
        updateArr = new boolean[length * 4];
        // 构建线段树的 SumArr
        buildSumArr(left, right, 1);
    }

    public void buildSumArr(int left, int right, int node) {
        if (left == right) { // 一定要拆解到单个节点的时候才可以进行赋值
            sumArr[node] = arr[left];
            return;
        }
        int mid = (left + right) / 2;
        buildSumArr(left, mid, node * 2); // 左节点
        buildSumArr(mid + 1, right, node * 2 + 1); // 右节点
        // 求左右节点的和
        sumArr[node] = sumArr[node * 2] + sumArr[node * 2 + 1];
    }



    private void refresh(int root, int leftNum, int rightNum) {
        // update和change必须是一起出现的
        // task是单独出现的,而且上面操作的会将task给覆盖掉

        if (updateArr[root]) {  // 它以及下面的树需要更新
            updateArr[root * 2] = true;
            updateArr[root * 2 + 1] = true;

            changeArr[root * 2] = changeArr[root]; // 需要更新的值
            changeArr[root * 2 + 1] = changeArr[root]; // 需要更新的值

            taskArr[root * 2] = 0; // 既然都更新了,那么lazy的值就可以清空了
            taskArr[root * 2 + 1] = 0;

            sumArr[root * 2] = changeArr[root] * leftNum; // 更新求和信息
            sumArr[root * 2 + 1] = changeArr[root] * rightNum; // 更新求和信息
            updateArr[root] = false;  // 自身更新好了
        }
        if (taskArr[root] != 0) {     // 这个值是需要下发的值
            taskArr[root * 2] += taskArr[root]; // 自身需要下发的值,加上来自上面的值
            sumArr[root * 2] += taskArr[root] * leftNum; // 结算上面下发的值

            taskArr[root * 2 + 1] += taskArr[root];
            sumArr[root * 2 + 1] += taskArr[root] * rightNum;  // 同上
            taskArr[root] = 0;  // 需要下发的值更新
        }
    }

    public void update(int start, int end, int value) {
            // 这里的left,right,1非常重要,1代表的是节点的位置,而left和right代表的是改节点的管辖范围。这里是root开始,root为1
            // int left, int right, int node  代表这个节点的范围,每次会更新传递
            // 这里的left和right最终会聚合到一起,代表的是arr的位置
            update(start, end, value, left, right, 1);
        }

        public void update(int start, int end, int value, int left, int right, int node) {
            // 满足更新的条件
            if (start <= left && right <= end) {
                updateArr[node] = true;  // 它以及下面的树需要更新
                changeArr[node] = value;  //更新数字
                sumArr[node] = value * (right - left + 1); // 求和
                taskArr[node] = 0;
                return;
            }
            // 任务下发。
            int mid = (left + right) / 2;
            // 左树的个数,和右树的个数
            refresh(node, mid - left + 1, right - mid);
            if (start <= mid) {
                update(start, end, value, left, mid, node * 2);
            }
            if (end > mid) {
                update(start, end, value, mid + 1, right, node * 2 + 1);
            }
            sumArr[node] = sumArr[node * 2] + sumArr[node * 2 + 1];
        }

        // 添加
        public void add(int start, int end, int value) {
            add(start, end, value, left, right, 1);
        }

        public void add(int start, int end, int value, int left, int right, int node) {
            // 开始点和结束点,把树的左右范围给包括了
            if (start <= left && right <= end) {
                sumArr[node] += value * (right - left + 1); // 这里的结算逻辑非常重要,会根据不同的题目进行变换
                taskArr[node] += value;
                return;
            }
            int mid = (left + right) / 2;
            // 这里最要是对于changeArr和taskArr的操作
            refresh(node, mid - left + 1, right - mid);

            if (start <= mid) { // 如果开始位置比mid大,那么左边就没有下发的意义了,只要下发右边就行了
                add(start, end, value, left, mid, node * 2); // 左节点 这里和初始化的策略一样
            }
            if (end > mid) {
                add(start, end, value, mid + 1, right, node * 2 + 1); // 右节点
            }
            sumArr[node] = sumArr[node * 2] + sumArr[node * 2 + 1];
        }

        public long querySum(int start, int end) {
            return querySum(start, end, left, right, 1);
        }

        public long querySum(int start, int end, int left, int right, int node) {
            // 这里是返回当前节点,因为start和end已经被包围了。
            if (start <= left && right <= end) {
                return sumArr[node];
            }
            int mid = (left + right) / 2;
            refresh(node, mid - left + 1, right - mid);
            long ans = 0;
            if (start <= mid) { // 下发左边的
                ans += querySum(start, end, left, mid, node * 2);
            }
            if (end > mid) { // 下发右边的
                ans += querySum(start, end, mid + 1, right, node * 2 + 1);
            }
            return ans;
        }
    }
//=============== 下面是用于对比测试的代码

    public static class Right {
        public int[] arr;

        public Right(int[] origin) {
            arr = new int[origin.length + 1];
            for (int i = 0; i < origin.length; i++) {
                arr[i + 1] = origin[i];
            }
        }

        public void update(int L, int R, int C) {
            for (int i = L; i <= R; i++) {
                arr[i] = C;
            }
        }

        public void add(int L, int R, int C) {
            for (int i = L; i <= R; i++) {
                arr[i] += C;
            }
        }

        public long query(int L, int R) {
            long ans = 0;
            for (int i = L; i <= R; i++) {
                ans += arr[i];
            }
            return ans;
        }

    }


    public static int[] genarateRandomArray(int len, int max) {
        int size = (int) (Math.random() * len) + 1;
        int[] origin = new int[size];
        for (int i = 0; i < size; i++) {
            origin[i] = (int) (Math.random() * max) - (int) (Math.random() * max);
        }
        return origin;
    }

    public static boolean test() {
        int len = 100;
        int max = 1000;
        int testTimes = 5000;
        int addOrUpdateTimes = 1000;
        int queryTimes = 500;
        for (int i = 0; i < testTimes; i++) {
            int[] origin = genarateRandomArray(len, max);
            SegmentTree seg = new SegmentTree(origin);
            int N = origin.length;
            Right rig = new Right(origin);
            for (int j = 0; j < addOrUpdateTimes; j++) {
                int num1 = (int) (Math.random() * N) + 1;
                int num2 = (int) (Math.random() * N) + 1;
                int L = Math.min(num1, num2);
                int R = Math.max(num1, num2);
                int C = (int) (Math.random() * max) - (int) (Math.random() * max);
                if (Math.random() < 0.5) {
                    seg.add(L, R, C);
                    rig.add(L, R, C);
                } else {
                    seg.update(L, R, C);
                    rig.update(L, R, C);
                }
            }
            for (int k = 0; k < queryTimes; k++) {
                int num1 = (int) (Math.random() * N) + 1;
                int num2 = (int) (Math.random() * N) + 1;
                int L = Math.min(num1, num2);
                int R = Math.max(num1, num2);
                long ans1 = seg.querySum(L, R);
                long ans2 = rig.query(L, R);

                if (ans1 != ans2) {
                    System.out.println(k);
                    System.out.println(ans1 + "   " + ans2);
                    return false;
                }
            }
        }
        return true;
    }
总结

个人练习了3道线段树的题目。写出来非常不容易,带着模板都没这么简单可以做出来

更多的是要学会这里递归的思想,做到灵活运用。像是2286题目 和区间没有关系,每次都是需要细化到单点。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【韩顺平Java笔记】第4章:运算符

文章目录 61. 上一章总结62. 算术运算符介绍62.1 运算符介绍62.2 算术运算符介绍62.3 算术运算符一览 63. 算术运算符使用64. 算术运算符练习165. 算术运算符练习266. 67. 算术运算符练习3,468. 关系运算符介绍68.1 关系运算符介绍68.2 关系运算符一览 69. 关系运算符使用70. 逻…

2024暄桐“静定的滋养”|静坐篇之林曦老师聊静坐

我们都喜爱“静”&#xff0c;它是一种因能量充足而带来的稳定放松的状态。 正在报名中的暄桐《2024书法课程 第五阶“静定的滋养” | 从书法之美到生活之美——林曦老师的线上直播书法课》&#xff0c;除了书法进阶部分的内容之外&#xff0c;读书部分正是帮助我们加强对静定的…

Windows应急响应-PcShare远控木马

文章目录 应急背景木马查杀1.查看异常连接2.查看进程3.查看服务定位到注册表 开始查杀 入侵排查1.账户排查2.开机自启3.服务4.计划任务5.网络情况6.进程排查重启再排查一遍 应急背景 曲某今天想要装一款软件&#xff0c;通过网上搜索看到非官方网站进入后直接下载下来后进行安…

毕业设计选题:基于ssm+vue+uniapp的购物系统小程序

开发语言&#xff1a;Java框架&#xff1a;ssmuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;M…

DSPy101

DSPy 介绍 DSPy&#xff08;Declarative Self-improved Language Programs in Python&#xff09; 是一个用于系统化和增强在流水线内使用语言模型的框架&#xff0c;它通过数据驱动和意图驱动的系统来优化大型语言模型&#xff08;LLM&#xff09;的使用。 DSPy 的核心是模块…

我的笔记本电脑之前可以直接用音量键调节音量,后来需要fn键加音量键才能调节,这是为什么?

我的笔记本电脑之前可以直接用音量键调节音量&#xff0c;后来需要fn键加音量键才能调节&#xff0c;这是为什么&#xff1f; 直接按 FnEsc就能解除Fn的锁定

信息安全数学基础(24)模为奇数的平方剩余与平方非剩余

前言 在信息安全数学基础中&#xff0c;模为奇数的平方剩余与平方非剩余是数论中的一个重要概念&#xff0c;特别是在密码学和安全协议中扮演着关键角色。当模数为奇数时&#xff0c;我们通常关注的是模为奇素数的平方剩余与平方非剩余&#xff0c;因为奇合数的情况更为复杂且…

【数学分析笔记】第4章第2节 导数的意义和性质(2)

4. 微分 4.2 导数的意义与性质 4.2.3 单侧导数 f ′ ( x ) lim ⁡ Δ x → 0 f ( x Δ x ) − f ( x ) Δ x lim ⁡ x → x 0 f ( x ) − f ( x 0 ) x − x 0 f(x)\lim\limits_{\Delta x\to 0}\frac{f(x\Delta x)-f(x)}{\Delta x}\lim\limits_{x\to x_0}\frac{f(x)-f(x_0)…

Golang | Leetcode Golang题解之第448题找到所有数组中消失的数字

题目&#xff1a; 题解&#xff1a; func findDisappearedNumbers(nums []int) (ans []int) {n : len(nums)for _, v : range nums {v (v - 1) % nnums[v] n}for i, v : range nums {if v < n {ans append(ans, i1)}}return }

OceanBase企业级分布式关系数据库

简介 OceanBase 数据库是阿里巴巴和蚂蚁集团不基于任何开源产品&#xff0c;完全自研的原生分布式关系数据库软件&#xff0c;在普通硬件上实现金融级高可用&#xff0c;首创“三地五中心”城市级故障自动无损容灾新标准&#xff0c;具备卓越的水平扩展能力&#xff0c;全球首…

使用微服务Spring Cloud集成Kafka实现异步通信(消费者)

1、本文架构 本文目标是使用微服务Spring Cloud集成Kafka实现异步通信。其中Kafka Server部署在Ubuntu虚拟机上&#xff0c;微服务部署在Windows 11系统上&#xff0c;Kafka Producer微服务和Kafka Consumer微服务分别注册到Eureka注册中心。Kafka Producer和Kafka Consumer之…

Ajax ( 是什么、URL、axios、HTTP、快速收集表单 )Day01

AJAX 一、Ajax是什么1.1名词解释1.1.1 服务器1.1.2 同步与异步1. 同步&#xff08;Synchronous&#xff09;2. 异步&#xff08;Asynchronous&#xff09;3. 异步 vs 同步 场景4. 异步在 Web 开发中的常见应用&#xff1a; 1.2 URL 统一资源定位符1.2.1 URL - 查询参数1.2.2 ax…

经典RCU锁原理及Linux内核实现

经典RCU锁原理及Linux内核实现 RCU锁原理 RCU锁第一个特点就是适用于读很多写很少的场景&#xff0c;那它和读写锁有什么区别呢&#xff1f;区别就是RCU锁读者完全不用加锁&#xff08;多个写者之间仍需要竞争锁&#xff09;&#xff0c;而读写锁&#xff08;不管是读优先、写…

https://www.aitoolpath.com/ 一个工具数据库,目前储存了有2000+各种工具。每日更新

AI 工具爆炸&#xff1f;别怕&#xff0c;这个网站帮你整理好了&#xff01; 哇塞&#xff0c;兄弟们&#xff01;AI 时代真的来了&#xff01;现在各种 AI 工具跟雨后春笋似的&#xff0c;噌噌噌地往外冒。AI 写作、AI 绘画、AI 代码生成……简直是要逆天啊&#xff01; 可是…

XSS | XSS 常用语句以及绕过思路

关注这个漏洞的其他相关笔记&#xff1a;XSS 漏洞 - 学习手册-CSDN博客 0x01&#xff1a;干货 - XSS 测试常用标签语句 0x0101&#xff1a;<a> 标签 <!-- 点击链接触发 - JavaScript 伪协议 --><a hrefjavascript:console.log(1)>XSS1</a> <!-- 字…

智能制造--EAP设备自动化程序

EAP是设备自动化程序&#xff08;Equipment Automation Program&#xff09;的缩写&#xff0c;他是一种用于控制制造设备进行自动化生产的系统。EAP系统与MES系统整合&#xff0c;校验产品信息&#xff0c;自动做账&#xff0c;同时收集产品生产过程中的制程数据和设备参数数据…

Spring MVC__入门

目录 一、SpringMVC简介1、什么是MVC2、什么是SpringMVC 二、Spring MVC实现原理2.1核心组件2.2工作流程 三、helloworld1、开发环境2、创建maven工程3、配置web.xml4、创建请求控制器5、创建springMVC的配置文件6、测试HelloWorld7、总结 一、SpringMVC简介 1、什么是MVC MV…

git 报错git: ‘remote-https‘ is not a git command. See ‘git --help‘.

报错内容 原因与解决方案 第一种情况&#xff1a;git路径错误 第一种很好解决&#xff0c;在环境变量中配置正确的git路径即可&#xff1b; 第二种情况 git缺少依赖 这个情况&#xff0c;网上提供了多种解决方案。但如果比较懒&#xff0c;可以直接把仓库地址的https改成ht…

【Kotlin基于selenium实现自动化测试】初识selenium以及搭建项目基本骨架(1)

导读大纲 1.1 Java: Selenium 首选语言1.2 配置一个强大的开发环境 1.1 Java: Selenium 首选语言 Java 是开发人员和测试人员进行自动化 Web 测试的首选 Java 和 Selenium 之间的协同作用受到各种因素的驱动,从而提高它们的有效性 为什么Java经常被认为是Selenium的首选语言 广…

记录一次出现循环依赖问题

具体的结构设计&#xff1a; 在上面的图片中&#xff1a; UnboundBlackVerifyChain类中继承了UnboundChain类。但是UnboundChain类中注入了下面三个类。 Scope(“prototype”) UnboundLinkFlowCheck类 Scope(“prototype”) UnboundUserNameCheck类 Scope(“prototype”) Un…