线段树
一、作用
与树状数组类似,用来处理数组的区间查询和元素更新操作。
与树状数组类似,用来处理数组的区间查询和元素更新操作。
与树状数组类似,用来处理数组的区间查询和元素更新操作。
重要的事情说三遍!!!!
相较于树状数组,线段树还可以进行区间最大值,最小值的查询,更加的灵活。
复杂度
更新(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];// 定义线段树
- 先给出线段树的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]的最小值
- 查询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));
}
- 然后是线段树的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
题目描述
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 kk。
- 求出某区间每一个数的和。
输入格式
第一行包含两个整数 n, mn,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 nn 个用空格分隔的整数,其中第 ii 个数字表示数列第 ii 项的初始值。
接下来 mm 行每行包含 33 或 44 个整数,表示一个操作,具体如下:
1 x y k
:将区间 [x, y][x,y] 内每个数加上 kk。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;
}