概念
线段树,它的每一个节点都保存了一条线段,可以高效地解决连续区间的动态查询问题,能基本保持每个操作的复杂度为O(logn)。
定义
我们定义一个结构体p来存储这个树
struct P {
int left, right, mid, value, lazy;
//lazy用于后期的lazy tag,前半篇文章不会讲到
} p[MAXN * 8];
//MAXN表示最大的原数组长度
//一般开出来的数组不会超过原长度的4倍
//由于后期由于要用上lazy tag所以开8倍
建树
我们定义一个函数build来完成建树操作(将每一个节点的值都设为0
)
void build();
分别用变量root、left、right来表示当前要build的节点、这个节点的左端点和右端点,即
void build(int root, int left, int right);
调用时,即
build(1, 1, n);
//n即数组长度
然后就开始写这个函数
void build(int root, int left, int right) {
p[root].left = left;
p[root].right = right;
p[root].mid = (left + right) / 2; //也可以写成(left + right) >> 1
p[root].value = 0; //给当前节点赋值
if (p[root].left == p[root].right) return ; //结束递归
int mid = (left + right) / 2;
build(root * 2, left, mid);
build(root * 2 + 1, mid + 1, right);
//上面是为了方便理解,为了效率也可以这样写
//int mid = (left + right) >> 1;
//build(root << 1, left, mid);
//build((root << 1) + 1, mid + 1, right);
}
单点修改
我们现在打一个修改单一变量的代码,相信会了这个以后,区间修改也不是问题(至少对我来说是这样)
void add(int root, int k, int value) {
//root表示当前修改的节点,k表示当前修改的变量(在原数组中)的下标,value表示增加的值
p[root].value += value; //增加当前节点的值
if (p[root].left == p[root].right) return ; //结束递归
if (k <= p[root].mid) add(root * 2, k, value); //在左孩子这边
else add(root * 2 + 1, k, value); //在右孩子这边
//也可以写成
//if (k <= p[root].mid) add(root >> 1, k, value);
//else add((root >> 1) + 1, k, value);
}
调用的时候,只需要
add(1, k, value);
//k就是下标,value就是要增加的值
//由于这个点肯定包含到1-n这一条线段中,所以直接从第一个点开始增加即可
对于Luogu的线段树模板题一,需要对数组初始化,可以这么写
int n, a[MAXN];
scanf("%d", &n);
build(1, 1, n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
add(1, i, a[i]);
}
区间修改
看懂单点修改后,区间修改应该很简单了吧
注意区间修改的话要添加的值也应该是一个区间的
void add(int root, int left, int right, int value) {
//root表示当前修改的节点,left表示左边,right表示右边,value表示增加的值
p[root].value += value * (right - left + 1); //增加当前节点的值
if (p[root].left == p[root].right) return ; //结束递归
if (right <= p[root].mid) add(root * 2, left, right, value); //在左孩子这边
else if (left > p[root].mid) add(root * 2 + 1, left, right, value); //在右孩子这边
else {
add(root * 2, left, p[root].mid, value);
add(root * 2 + 1, p[root].mid + 1, right, value);
//分段
}
//优化方式这里不放了
}
(看懂了吗?)
区间查询
int find(int root, int left, int right) {
//定义同上
if (left == p[root].left && right == p[root].right) return p[root].value; //找到目标,结束递归
if (right <= p[root].mid) return find(root * 2, left, right); //在左孩子这边
else if (left > p[root].mid) return find(root * 2 + 1, left, right); //在右孩子这边
else return find(root * 2, left, p[root].mid) + find(root * 2 + 1, p[root].mid + 1, right);
//进一步递归,优化同样不打了
} //分段
调用的时候只要
printf("%d\n", find(1, k, k)); //单点查询
printf("%d\n", find(1, left, right)); //区间查询
练手(一)
那么你已经学会了基本的一些操作,打道Luogu的模板题尝试一下吧
链接地址:LuoguP3372线段树(一)
题目描述
如题,已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数加上x
2.求出某区间每一个数的和
输入格式
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3或4个整数,表示一个操作,具体如下
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k
操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和
输出格式
输出包含若干行整数,即为所有操作2的结果。
输入样例
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
输出样例
11
8
20
代码
#include <bits/stdc++.h>
#define MAXN 100001
using namespace std;
int n, m, a[MAXN];
struct P {
int left, right, mid, value;
} p[MAXN * 8]; //定义
void build(int root, int left, int right) {
p[root].left = left;
p[root].right = right;
p[root].value = 0;
if (left == right) return ;
int mid = p[root].mid = (left + right) >> 1;
build(root << 1, left, mid);
build((root << 1) + 1, mid + 1, right);
} //建树
void add(int root, int left, int right, int value) {
p[root].value += value * (right - left + 1);
if (p[root].left == p[root].right) return ;
if (right <= p[root].mid) add(root * 2, left, right, value);
else if (left > p[root].mid) add(root * 2 + 1, left, right, value);
else {
add(root * 2, left, p[root].mid, value);
add(root * 2 + 1, p[root].mid + 1, right, value);
}
} //修改
int find(int root, int left, int right) {
if (left == p[root].left && right == p[root].right) return p[root].value;
if (right <= p[root].mid) return find(root * 2, left, right);
else if (left > p[root].mid) return find(root * 2 + 1, left, right);
else return find(root * 2, left, p[root].mid) + find(root * 2 + 1, p[root].mid + 1, right);
} //查询
int main() {
scanf("%d%d", &n, &m);
build(1, 1, n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
add(1, i, i, a[i]);
} //建树
int k, x, y, z;
for (int i = 1; i <= m; i++) {
scanf("%d", &k);
if (k == 1) {
scanf("%d%d%d", &x, &y, &z);
add(1, x, y, z); //修改
} else {
scanf("%d%d", &x, &y);
printf("%d\n", find(1, x, y)); //查询
}
}
return 0;
}
提交上后TLE70分,这又是为什么呢?
线段树优化 - lazy tag
如果你自己思考一下可以发现,这样的线段树处理其实是非常浪费时间的。因为每一次的修改操作都要一层一层地执行下去,但很多时候这个修改是不会被使用到的。所以我们就需要lazy tag
来进行优化。
如果你已经一路看到这里,建议你先喝杯咖啡放松一下(不要问我为什么)。
原理
lazy tag
的原理其实很好理解。本来线段树的修改需要递归到最后一层,现在如果发现当前节点所表示的一整条线段都需要修改,就打上lazy tag
,放在那里,等到查询到的时候再计算
打标记(pushdown
)操作
void pushdown(int root, int value) {
//由于是整条线段所以没有必要传递左指针和右指针
p[root].lazy += value; //注意不是=(等于)
p[root].value += value * (p[root].right - p[root].left + 1);
}
区间修改(带lazy tag
)
void add(int root, int left, int right, int value) {
//root表示当前修改的节点,left表示左边,right表示右边,value表示增加的值
//原代码:p[root].value += value * (right - left + 1); //增加当前节点的值
//原代码:if (p[root].left == p[root].right) return ; //结束递归
if (p[root].left == left && p[root].right == right) {
//整条线段都需要覆盖
pushdown(root, value); //去打lazy tag
return ;
}
p[root].value += value * (right - left + 1);
//下面和原来一样
if (right <= p[root].mid) add(root * 2, left, right, value); //在左孩子这边
else if (left > p[root].mid) add(root * 2 + 1, left, right, value); //在右孩子这边
else {
add(root * 2, left, p[root].mid, value);
add(root * 2 + 1, p[root].mid + 1, right, value);
//分段
}
}
调用方法和原来一样
区间查询(带lazy tag
)
int find(int root, int left, int right) {
if (p[root].lazy) {
//发现lazy tag
pushdown(root * 2, p[root].lazy); //给左孩子打上
pushdown(root * 2 + 1, p[root].lazy); //给右孩子打上
p[root].lazy = 0; //清除lazy tag
}
if (left == p[root].left && right == p[root].right) return p[root].value; //找到目标,结束递归
if (right <= p[root].mid) return find(root * 2, left, right); //在左孩子这边
else if (left > p[root].mid) return find(root * 2 + 1, left, right); //在右孩子这边
else return find(root * 2, left, p[root].mid) + find(root * 2 + 1, p[root].mid + 1, right); //分段
}
修改后的代码
#include <bits/stdc++.h>
#define MAXN 100001
using namespace std;
int n, m;
long long a[MAXN];
struct P {
int left, right, mid;
long long lazy, value;
} p[MAXN * 8]; //定义
void build(int root, int left, int right) {
p[root].left = left;
p[root].right = right;
p[root].value = 0;
if (left == right) return ;
int mid = p[root].mid = (left + right) >> 1;
build(root << 1, left, mid);
build((root << 1) + 1, mid + 1, right);
} //建树
void pushdown(int root, long long value) {
//由于是整条线段所以没有必要传递左指针和右指针
p[root].lazy += value; //注意不是=(等于)
p[root].value += value * (p[root].right - p[root].left + 1);
}
void add(int root, int left, int right, long long value) {
if (p[root].left == left && p[root].right == right) {
//整条线段都需要覆盖
pushdown(root, value); //去打lazy tag
return ;
}
p[root].value += value * (right - left + 1);
if (right <= p[root].mid) add(root * 2, left, right, value);
else if (left > p[root].mid) add(root * 2 + 1, left, right, value);
else {
add(root * 2, left, p[root].mid, value);
add(root * 2 + 1, p[root].mid + 1, right, value);
}
} //修改
long long find(int root, int left, int right) {
if (p[root].lazy) {
//发现lazy tag
pushdown(root * 2, p[root].lazy); //给左孩子打上
pushdown(root * 2 + 1, p[root].lazy); //给右孩子打上
p[root].lazy = 0; //清除lazy tag
}
if (left == p[root].left && right == p[root].right) return p[root].value;
if (right <= p[root].mid) return find(root * 2, left, right);
else if (left > p[root].mid) return find(root * 2 + 1, left, right);
else return find(root * 2, left, p[root].mid) + find(root * 2 + 1, p[root].mid + 1, right);
} //查询
int main() {
scanf("%d%d", &n, &m);
build(1, 1, n);
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
add(1, i, i, a[i]);
} //建树
int k, x, y, z;
for (int i = 1; i <= m; i++) {
scanf("%d", &k);
if (k == 1) {
scanf("%d%d%d", &x, &y, &z);
add(1, x, y, z);
} else {
scanf("%d%d", &x, &y);
printf("%lld\n", find(1, x, y));
}
}
return 0;
}
AC100分 (需要开long long)