线段树
线段树是一种基于分治思想的的二叉树结构用于在区间上进行信息统计。与二进制位(2的次幂)进行区间划分的树状数组相比,线段树是一种更加通用的结构
性质:
- 线段树的每个节点都代表一个区间
- 线段树具有唯一的根节点,代表区间是整个区间统计范围,如[1, n]
- 线段树的每个叶节点都代表一个长度为1的区间[x, x]
- 对于内部节点[l, r],它的左子节点是[l, mid],右子节点是[mid + 1, r],其中mid = (l + r) / 2(向下取整)
附一张图:(来自算法进阶指南)
上图就是展示了一颗线段树,可以发现,除了树的最后一层就是完全二叉树,树的深度为O(log N)。
用途
线段树的基本用途是对序列进行维护,支持查询与修改操作
线段树的建树
给定一个长度为N的序列a,我们可以在区间[1, N]上建立一棵线段树,每个叶节点[i, i]保存a[i]的值,然后利用线段树的子节点去更新父节点,以区间最大值为例,dat(l, r) = max{a[l], a[l + 1], … , a[r - 1], a[r]},
显然dat(l, r) = max(dat(l, mid), dat(mid + 1, r))
代码:
struct Node
{
int l ,r, dat;
}tr[4 * N];
void build(int u, int l, int r)
{
if(l == r)
{
tr[u] = {l, r, dat};
return ;
}
tr[u] = {l, r};
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
pushup(u); //子节点更新父节点
}
线段树的单点修改
单点修改是一条形如 “C x v” 的指令,表示吧a[x]的值修改为 v.
在线段树种,根节点是各种操作的入口,我们需要从根节点出发,递归找到区间[x, x]的叶节点,然后返回从下往说更新[x, x]以及它的所有祖先节点,
代码:
void modify(int u, int x, int y)
{
if(tr[u].l == tr[u].r)
{
tr[u].dat = y;
return ;
}
int mid = tr[u].l + tr[u].r >> 1;
if(x <= mid) modify(u << 1, x, y);
else modify(u << 1 | 1, x, y);
pushup(u);
}
线段树的区间查询
区间查询是一条形如 “Q l r” 的指令,例如查询序列a在区间[l, r]的最大值,
我们只需从根节点出发,递归执行一下过程:
- 诺[l, r] 完全覆盖了当前节点代表的区间,则立即回溯,并且该节点的dat值为候选答案。
- 若左子节点与[l, r]有重叠部分,则递归访问左子节点
- 若右子节点与[l, r]有重叠部分,则递归访问右子节点
代码:
int ask(int u, int l, int r)
{
if(l <= tr[u].l && r >= tr[u].r) return tr[u].dat;
int mid = tr[u].l + tr[u].r >> 1;
int v = -(1 << 30); // 负无穷大
if(l <= mid) v = max(v, ask(u << 1, l, r)); // 与左子节点有交集
if(r > mid) v = max(v, ask(u << 1 | 1, l, r)); // 与有子节点有交集
return v;
}
pushup函数
这是自定义更新的函数:有子节点更新父节点,有最大值为例
代码:
void pushup(int u)
{
tr[u].dat = max(tr[u << 1].dat, tr[u << 1 | 1].dat);
}
例题1:
AcWing 1275. 最大数
AcWing 245. 你能回答这些问题吗
懒标记(延迟标记)
在线段树中,上述中有说单点修改,那么线段树该如何进行区间修改呢?
一个一个改? 不,这样时间复杂度就是O(N long N),这是我们不能接受的,所以我们引进了懒标记这个概念,
什么是懒标记呢?正如命名那样,就是先不动,打个标记,等到快到时间了,根据标记在往下动,
含义:该节点曾经被修改,其子节点尚未被更新
以一个例题来继续吧:AcWing 243. 一个简单的整数问题2
代码:
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long LL;
const int N = 100010;
struct Node
{
int l, r;
LL sum, add;
}tr[4 * N];
int a[N], n, m;
void pushup(int u)
{
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
// 懒标记的操作
void pushdown(int u)
{
auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
if(root.add) //节点有标记
{
left.sum += (LL)(left.r - left.l + 1) * root.add; // 更新左子节点
left.add += root.add; // 给左子节点打标记
right.sum += (LL)(right.r - right.l + 1) * root.add; // 更新右子节点
right.add += root.add; // 给右子节点打标记
root.add = 0; // 清除u的标记
}
}
void build(int u, int l, int r)
{
if(l == r)
{
tr[u] = {l, r, a[l], 0};
return ;
}
tr[u] = {l, r};
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
pushup(u);
}
void modify(int u, int l, int r, int d)
{
if(l <= tr[u].l && r >= tr[u].r)
{
tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * d; // 更新节点信息
tr[u].add += d; // 个节点打标记
return ;
}
/* 这里一定要分裂,假设一个去区间已经加了10,后来该区间后半部分又要加上5又要加上,
则要继续往下找,又因为后面要更新pushup,所以要先pushdown(分裂),不然后半部分就会
加不上,也就是说如果没有完全包含的话,就要先powhdown一遍
*/
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if(l <= mid) modify(u << 1, l, r, d); // 和左区间有交集
if(r > mid) modify(u << 1 | 1, l, r, d); // 和右区间有交集
pushup(u);
}
LL ask(int u, int l, int r)
{
if(l <= tr[u].l && r >= tr[u].r) return tr[u].sum;
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
LL sum = 0;
if(l <= mid) sum += ask(u << 1, l ,r);
if(r > mid) sum += ask(u << 1 | 1, l, r);
return sum;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
build(1, 1, n);
char op[2];
int l, r, d;
while(m--)
{
scanf("%s%d%d", op, &l, &r);
if(*op == 'C')
{
scanf("%d", &d);
modify(1, l, r, d);
}
else printf("%lld\n", ask(1, l, r));
}
return 0;
}
懒标记的拓展例题:AcWing 1277. 维护序列