引入
问题1 有一列长度为 n n n 的数,刚开始全是 0 0 0 。现在执行 m m m 次操作,每次可以执行一下操作之一:
- 将数列中的某个数加上某个数值
- 询问给定区间的所有数字的和
题目分析
根据题目我们很容易想到时间复杂度为 O ( n ) O(n) O(n) 的做法,但是这样做有一个问题,如果 n , m n,m n,m 的值非常大,大概去到 1 0 8 10^8 108 左右,处理的结果往往不那么令人满意。
线段树
线段树就是一棵二叉树,平衡二叉树。(注意:这里的每个区间都是左封闭右开放)根节点的两个子节点就是自己二分的结果,通过不断的二分,构造出这样一棵树,我们就叫他 线段树
。
线段树的操作
线段树,顾名思义,就是一棵树,所以对树的操作线段树也不能少。又因为他是一棵完全二叉树,所以有两种方式储存:静态链表储存和动态指针储存
。
树的储存
// 动态指针储存
struct Node {
int Left, Right;
Node *LeftChild, *RightChild;
int data; // 外纪(参数awa)
};
// 静态列表储存
#define NN 1000
int c[4*NN] = { 0 };
这里有个需要注意的地方:为什么c数组的空间需要 4 × N N 4 \times NN 4×NN 呢,因为这是一棵完全二叉树,但是在构建树的时候有可能出现上图这种情况,所以为了满足完全二叉树的要求,我们必须补全这棵树于是需要声明 4 × N N 4 \times NN 4×NN 的空间。
构建树
构建树的过程也是相当简单QAQ,因为树是由递归定义的,所以我们只需要稍微搞个递归就好啦~
// 建立一棵树
void build(Node* cur, int l, int r) {
cur->Left = 1;
cur->Right = r;
if (l + 1 < r) {
cur->LeftChild = new Node;
cur->RightChild = new Node;
build(cur->LeftChild, l, (l + r) >> 1);
build(cur->RightChild, (l + r) >> 1, r);
} else
cur->LeftChild = cur->RightChild = NULL;
}
修改树
同样,我们只需要通过递归来修改这棵树就好~~
// 修改树_1
void change(Node* cur, int x, int delta) {
if (cur->Left + 1 == cur->Right) {
cur->data += delta;
} else {
if (x < (cur->Left + cur->Right) >> 1)
change(cur->LeftChild, x, delta);
if (x > (cur->Left + cur->Right) >> 1)
change(cur->RightChild, x, delta);
cur->data =
cur->LeftChild->data + cur->RightChild->data; // 对接点的操作
}
}
查询树
有了修改,那么自然要有查询的操作,查询也很简单()。
// 查找区间和
int qurey(Node* cur, int l, int r) {
if (l <= cur->Left && cur->Right <= r)
return cur->data;
else {
int ans = 0;
if (l < (cur->Left + cur->Right) >> 1)
ans += qurey(cur->LeftChild, l, r);
if (r > (cur->Left + cur->Right) >> 1)
ans += qurey(cur->RightChild, l, r);
return ans;
}
}
例题
有了上面这些操作,接下来精灵王国请你帮忙解决一些问题(笑~
问题描述
精灵王的桌子上零散地放着若干个盒子,桌子的后方是一堵墙。如下图所示。现在从桌子的前方射来一束平行光,把盒子的影子投射到了墙上。问影子的总宽度是多少?
输入输出格式
输入格式:
第1行:盒子的个数N(1≤=N≤10000)。
第2…N+1行:每个盒子的起始位置S和结束位置T(1≤S, T≤100000)。
输出格式:
第1行:包含一个整数,表示影子的总宽度。
输入输出样例
输入样例#1:
4
1 2
3 5
4 6
5 6
输出样例#1:
4
加油!聪明的勇士,我相信你可以的!所以我就不写题解啦!再见!
高阶玩法
恭喜你,你成功解锁了线段树的高阶玩法嘿嘿~。通过上面的分析我们可以知道,线段树基础玩法对每个点的修改和查询和修改时间复杂度是
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n) ,确实是个好用的工具,但是!如果我们每次修改一片区间的值,时间复杂度就会去到
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n) ,还不如我直接暴力模拟呢(怒)。所以 聪明的你
一定想到了,我比你帅,可以通过标记节点来延迟修改来提高效率,这种方法我们把它叫做 lazy-tag
。
我们只需要修改一点代码就能实现这个功能:
- 首先是储存部分:
struct Node {
int Left, Right;
Node *LeftChild, *RightChild;
int data; // 外纪(参数awa)
int delta; // 延迟修改 <---就是这个东东
};
- 然后是修改树和查询树的部分:
// lazy-tag
void update(Node* cur) {
cur->LeftChild->data +=
cur->delta * (cur->LeftChild->Right - cur->LeftChild->Left);
cur->RightChild->data +=
cur->delta * (cur->RightChild->Right - cur->RightChild->Left);
cur->LeftChild->delta += cur->delta;
cur->RightChild->delta += cur->delta;
cur->delta = 0;
}
// 修改树
void lazy_tag_change(Node* cur, int l, int r, int delta) {
if (l <= cur->Left && cur->Right <= r) { // 区间被覆盖
cur->data += delta * (cur->Right - cur->Left);
cur->delta += delta;
} else {
if (cur->delta != 0)
update(cur);
if (l < (cur->Left + cur->Right) >> 1)
lazy_tag_change(cur->LeftChild, l, r, delta);
if (r > (cur->Left + cur->Right) >> 1)
lazy_tag_change(cur->RightChild, l, r, delta);
cur->data =
cur->LeftChild->data + cur->RightChild->data; // 对接点的操作
}
}
// 查询树
int lazy_tag_qurey(Node* cur, int l, int r) {
if (l <= cur->Left && cur->Right <= r)
return cur->data;
else {
if (cur->delta != 0)
update(cur);
int ans = 0;
if (l < (cur->Left + cur->Right) >> 1)
ans += lazy_tag_qurey(cur->LeftChild, l, r);
if (r > (cur->Left + cur->Right) >> 1)
ans += lazy_tag_qurey(cur->RightChild, l, r);
return ans;
}
}
这里我稍微的解释一下,这里每次扫描,只要扫描到左右区间能完全覆盖到的地方,就可以直接返回了,剩下了很多的不必要的计算,再在查询的时候直接更新,再加上就好。到这里,你的不承认,我真的比你帅你真的很聪明。
撤退!再见!