普通线段树

线段树

一、作用

树状数组类似,用来处理数组区间查询和元素更新操作。

树状数组类似,用来处理数组区间查询和元素更新操作。

树状数组类似,用来处理数组区间查询和元素更新操作。

重要的事情说三遍!!!!

相较于树状数组,线段树还可以进行区间最大值,最小值的查询,更加的灵活。

复杂度

更新(update):O(logn)

查询(query):O(logn)

二、实现

1、初识线段树

线段树在逻辑上使用完全二叉树的数据存储结构来实现树结点的存储,这个存储结构可以参考堆的存储逻辑。

特点:线段树这棵二叉树每一个结点对应一个区间的信息(信息的意思是指:区间的最大值、最小值、和等等)

性质

  • 根结点代表整个区间为整个数组信息
  • 每一个中间结点代表一个区间array[i,j]信息
  • 每一个叶子结点代表一个由单个元素组成的区间信息
  • 用数组存储时下标关系符合完全二叉树关于父结点孩子结点之间的下标关系

不妨我们以array={1,2,3,4,5,6}为原数组,构建一个求区间和的线段树,模样如下

在这里插入图片描述

  • 黄色圆圈圈中的是原数组的元素,下标依次由蓝色的序号在结点下进行了标记
  • 整棵树就是一棵线段树,存储在Tree[]数组中,每个结点左边的绿色序号表示了结点在Tree中的存储下标
  • 红色的区间表示了当前结点表示哪个区间的区间和

2、构建

线段树的构建基本类似于其他树的构建,也是用递归的方法进行构建。

对于线段树的存储,由于线段树在存储逻辑上是 类似于完全二叉树 的一种形式,所以我们选择使用数组进行存储,并且我们数组的下标从1开始

使用线段树求区间最大最小值区间和的代码有所不同

在这,

我们以求区间最小值为目标建立线段树数组

我们以求区间最小值为目标建立线段树数组

我们以求区间最小值为目标建立线段树数组

结构体

struct SegTreeNode
{
    long long value;
    int addMark;// 延迟标记(核心!!)在update函数中体现
}Tree[MAXNUM];// 定义线段树
  1. 先给出线段树的create函数
// node是当前结点
// begin是当前处理区间的开始
// end是当前处理区间的结尾
// 处理在本例中的意思时取区间最小值
// Tree[]是线段树结点存储的数组
// array[]是给出的一个普通数组
void create(int node, int begin, int end)  
{  
    Tree[node].addMark = 0;
    // 若begin和end重合,说明该区间只有一个结点元素,那么在这个区间内最小值肯定是这个唯一的结点
    if (begin == end) {
        Tree[node].value = Array[begin];
    } else {
      	// 递归完成当前结点node下左右子树的区间最小值计算
        create (2*node, begin, (begin+end)/2);
        create(2*node+1, (begin+end)/2+1, end);
        /*
         * 计算完当前结点node左右子树的区间最小值
         * 就开始计算现在这个node结点左右两个子树最小值是什么
         */
	    Tree[node].value = min(Tree[node*2].value, Tree[node*2+1].value);
        /*
         * 为什么当前结点node的值是左右子结点的最小值?
         * 道理很简单,node的左右结点就是node左区间和右区间的最小值
         * 所以node所在的区间的最小值就是左右子结点中的最小值
         */
    }  
}
main调用:create(1, 0, n-1); // Tree[1]记录的是array数组区间为[0,n-1]的最小值
  1. 查询query函数
long long query(int root, int nowLeft, int nowRight, int queryLeft, int queryRight)
{
    //查询区间和当前节点区间没有交集
    if(queryLeft > nowRight || queryRight < nowLeft)
    return INFINITE;
    //当前节点区间包含在查询区间内
    if(queryLeft <= nowLeft && queryRight >= nowRight)
        return Tree[root].value;
    pushDown(root);
    //分别从左右子树查询,返回两者查询结果的较小值
    int mid = (nowLeft + nowRight) / 2;
    return min(query(root*2, nowLeft, mid, queryLeft, queryRight), query(root*2 + 1, mid + 1, nowRight, queryLeft, queryRight));
}
  1. 然后是线段树的update函数(区间修改)
// 将延迟标记向子结点进行传递
void pushDown(int root)
{
    if(Tree[root].addMark != 0)
    {
        /* 
         * 设置左右孩子节点的标志域,因为孩子节点可能被多次延迟标记又没有向下传递
         * 所以是 “+=”
         */
        Tree[root*2].addMark += Tree[root].addMark;
        Tree[root*2+1].addMark += Tree[root].addMark;
        /*
         * 根据标志域设置孩子节点的值。因为我们是求区间最小值,因此当区间内每个元
         * 素加上一个值时,区间的最小值也加上这个值
         */
        Tree[root*2].value += Tree[root].addMark;
        Tree[root*2+1].value += Tree[root].addMark;
        // 传递后,当前节点标记域清空
        Tree[root].addMark = 0;
    }
}
// update函数
// nowLeft是现在的判断区间的起始位置
// nowRight是现在的判断区间的结束位置
// updateLeft是更新区间的起始位置
// updateRight是更新区间的结束位置
void update(int root, int nowLeft, int nowRight, int updateLeft, int updateRight, int addVal) {
    //更新区间和当前节点区间没有交集
    if (updateLeft > nowRight || updateRight < nowLeft)
        return;
    //当前节点区间包含在更新区间内
    if (updateLeft <= nowLeft && updateRight >= nowRight) {
        Tree[root].addMark += addVal;
        Tree[root].value += addVal;
        return;
    }
    pushDown(root); //延迟标记向下传递
    //更新左右孩子节点
    int nowMid = (nowLeft + nowRight) / 2;
    update(root*2, nowLeft, nowMid, updateLeft, updateRight, addVal);
    update(root*2+1, nowMid + 1, nowRight, updateLeft, updateRight, addVal);
    //根据左右子树的值回溯更新当前节点的值
    Tree[root].value = min(Tree[root * 2 + 1].value, Tree[root * 2 + 2].value);
}

三、例题

洛谷:P3372

链接:https://www.luogu.com.cn/problem/P3372

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  1. 将某区间每一个数加上 kk
  2. 求出某区间每一个数的和。

输入格式

第一行包含两个整数 n, mn,m,分别表示该数列数字的个数和操作的总个数。

第二行包含 nn 个用空格分隔的整数,其中第 ii 个数字表示数列第 ii 项的初始值。

接下来 mm 行每行包含 33 或 44 个整数,表示一个操作,具体如下:

  1. 1 x y k:将区间 [x, y][x,y] 内每个数加上 kk
  2. 2 x y:输出区间 [x, y][x,y] 内每个数的和。

输入输出样例

输入 #1

5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4

输出 #1

11
8
20

题目大意:给一个大小为n的数组,再给m条指令,若指令为1,将[x,y]内的数字全部加上k;若指令为2,求出区间[x,y]的区间和

分析:这是一道线段树模板题,根据题目的意思,我们需要去维护一个求区间和的线段树,然后根据不同的指示进行操作

代码:

#include <iostream>
#include <vector>
#include<cstdio>
using namespace std;
// 线段树结构体
struct segTree{
    long long value;
    int addMark;
};
//Array数组是题目给定数组
//Tree是进行维护的线段树数组
vector<long long> Array;
vector<segTree> Tree;
int n, m;

/*
 * 线段树求区间和模板
 * 与求最大最小值的模板略有不同
 */

//创建线段树
void create(int node, int begin, int end)
{
    Tree[node].addMark = 0;
    if (begin == end)
    {
        Tree[node].value = Array[begin];
    } else {
        int mid = (begin+end)/2;
        create (2*node, begin, mid);
        create(2*node+1, mid+1, end);
        // 此处与求最大最小值线段树模板不同,很容易理解,不赘述
        Tree[node].value = Tree[node*2].value + Tree[node*2+1].value;
    }
}
//相较于求最值时的pushDown函数,增加了左右区间:l和r。原因在函数内注释中
void pushDown(int root,int l,int r)
{
    if(Tree[root].addMark != 0)
    {
        int mid=l+r>>1;
        Tree[root*2].addMark += Tree[root].addMark;
        Tree[root*2+1].addMark += Tree[root].addMark;
        //此处通过延迟标记对子结点进行更新有所不同
        //当结点所表示区间有x个子结点就需要更新x个addMark,因此代码如下
        Tree[root*2].value += Tree[root].addMark*(mid-l+1);
        Tree[root*2+1].value += Tree[root].addMark*(r-mid);
        Tree[root].addMark = 0;
    }
}
//更新线段树
void update(int root, int nowLeft, int nowRight, int updateLeft, int updateRight, int addVal) {
    if (updateLeft > nowRight || updateRight < nowLeft)
        return;
    if (updateLeft <= nowLeft && updateRight >= nowRight) {
        Tree[root].addMark += addVal;
        //当结点所表示区间有x个子结点就需要更新x个addMark,因此代码如下
        Tree[root].value += addVal*(nowRight-nowLeft+1);
        return;
    }
    pushDown(root,nowLeft,nowRight);
    int nowMid = (nowLeft + nowRight) / 2;
    update(root*2, nowLeft, nowMid, updateLeft, updateRight, addVal);
    update(root*2+1, nowMid + 1, nowRight, updateLeft, updateRight, addVal);
    //求区间和。相比于求最值线段树,结点更新函数需要变化
    Tree[root].value = Tree[root*2].value + Tree[root*2+1].value;
}
long long query(int root, int nowLeft, int nowRight, int queryLeft, int queryRight)
{
    if(queryLeft > nowRight || queryRight < nowLeft)
        return 0;
    if(queryLeft <= nowLeft && queryRight >= nowRight)
        return Tree[root].value;
    pushDown(root,nowLeft,nowRight);
    int mid = (nowLeft + nowRight) / 2;
    //求区间和。相比于求最值线段树,结点更新函数需要变化
    return query(root*2, nowLeft, mid, queryLeft, queryRight) + query(root * 2 + 1, mid + 1, nowRight, queryLeft, queryRight);
}
int main()
{
    cin >> n >> m;
    Array.resize(n+10);
    //因线段树使用堆(完全二叉树)的数组存储方法,因此,当有n个结点进行存储的时候,需要4*n个空间
    Tree.resize(4*n+10);
    int temp;
    for(int i = 1; i <= n; i++)
        cin >> Array[i];
    create(1, 1, n);
    int state, x, y, k;
    for(int i = 0; i < m; i++)
    {
        cin >> state;
        if(state == 1) {
            cin >> x >> y >> k;
            update(1, 1, n, x, y, k);
        } else {
            cin >> x >> y;
            printf("%lld\n", query(1, 1, n, x, y));
        }
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值