线段树的又一篇详解
2017-12-31 15:56
190 查看
概念
线段树,它的每一个节点都保存了一条线段,可以高效地解决连续区间的动态查询问题,能基本保持每个操作的复杂度为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个数字表示
d9a3
数列第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)
相关文章推荐
- 线段树内容详解
- 一篇相当不错的js function详解
- busybox 详解(一篇讲busybox很详细的文章)
- 【每日一篇】Map接口详解
- hdu 1166 敌兵布阵(线段树详解)
- zkw线段树详解
- 【bzoj3065】: 带插入区间K小值 详解——替罪羊套函数式线段树
- hdu3265Posters(线段树+离散化+扫描线详解 )
- 一篇非常经典的springMVC注解实现方式详解
- [后续] 一篇文章详解性能评估难点
- java多线程之wait notify详解,start于run区别,wait与sleep区别一篇通,附例:生产者消费者。
- 线段树 入门详解
- 一篇不错的Java异常详解文章
- POJ 1151 Atlantis 线段树求矩形面积并 方法详解
- 一篇文章全吃透—史上最全YYModel的使用详解
- Makefile详解--学习linux下Gtk开发看到的一篇详细的讲解Makefile的文章,转来分享
- hdu1394之线段树详解
- 一篇关于apache commons类库的详解[转]
- 数据结构——线段树详解(超详细)
- 线段树详解