您的位置:首页 > 其它

线段树的又一篇详解

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)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息