线段树介绍:为了实现高效对一段区间进行区间修改及查询,引入一个由区间线段组成的二叉树,每个节点中保存此段区间所具体所需要的值,如这段区间里的最大值或区间和等等。复杂度非常优秀只需要以o(logn)的复杂度(树的高度)就可以实现对区间操作。
1、线段树高度是:log2(区间长度)+1,如果2^n<区间长度<2^n+1,按照n+1计算,如图
线段树区间修改分为三步:
-
如果这个区间被完全包括在目标区间里面,直接更新这个区间的值,例如需要给这个区间每个数加10,并记录区间和,则将区间和+=(区间右边界-左边界+1)*10,同时加上一个懒标记,同时不再往下更新。懒标记即标记自己这段区间被更新了,下一次若需要继续在这个区间往下更新或查询其子区间,需先把懒标记释放给左右儿子,把儿子所维护的区间值更新,再继续往下操作,例如刚刚在双亲结点更新了区间同时记录了懒标记,则查询其子区间时必定先将子区间先每个数也加十才是正确的记录。(懒标记是线段树的精髓也是唯一难点,可以保证所有操作复杂度都为o(logn),后续会给出详细证明)。
-
如果这个区间的左儿子和目标区间有交集,那么更新左儿子,如果存在懒标记,则先懒标记下推,再更新左儿子
-
如果这个区间的右儿子和目标区间有交集,那么更新右儿子,如果存在懒标记,则先懒标记下推,再更新右儿子
线段树区间查询分为三步:
-
如果这个区间被完全包括在目标区间里面,直接返回这个区间的值
-
如果这个区间的左儿子和目标区间有交集,那么搜索左儿子,如果存在懒标记,则先懒标记下推,再更新左儿子
-
如果这个区间的右儿子和目标区间有交集,那么搜索右儿子,如果存在懒标记,则先懒标记下推,再更新右儿子
单点修改和单点查询即为区间的特殊情况,所以不再此赘述。
2、最坏时间复杂度?(99%的文章不会证明)
首先懒标记的好处是不需要重复操作,多次更新,一次下推,减少了无用功。
首先,对于某一个节点,我们所查询最坏的查询情况一定是横跨其左右孩子,这样需要继续去其左右孩子中查找我们所要查找的区间,对于如下这张图而言,如果我需要查找2-14这个区间,按照上述查询规则,我需要继续往左右孩子中分别查找,而对1-8和9-16这两个孩子节点来说,如果我想时间复杂度最高,肯定也是需要继续分裂为两个子区间去查找,而关键之处在于此时我在想从1-8分裂时,其右孩子区间一定是查询规则中的第一种情况(读者可以自行思考),按照这样的逻辑继续往下查询,每一行最多只会访问四个节点,即最多为o(4log2n)。
3、线段树为什么要开四倍空间?
4、关于线段树离散化
在此给出线段树模板
// 操作是给区间每一个数加d
// 询问是求某一区间和
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 100010;
int w[N];
int n, m;
struct Node
{
int l, r;
LL sum;
LL add; // 懒标记
}tr[N * 4];;
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.add += root.add, left.sum += (LL)(left.r - left.l + 1) * root.add;
right.add += root.add, right.sum += (LL)(right.r - right.l + 1) * root.add;
root.add = 0; // 删除懒标记
}
}
void build(int u, int l, int r)
{
if (l == r) tr[u] = {l, r, w[l], 0}; // 叶子节点
else
{
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 v)
{
// 结点在要修改的区间中
if (l <= tr[u].l && r >= tr[u].r)
{
tr[u].sum += (tr[u].r - tr[u].l + 1) * v;
tr[u].add += v; // 加上懒标记
}
else
{
pushdown(u); // 先传递懒标记
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(u << 1, l, r, v);
if (r > mid) modify(u << 1 | 1, l, r, v);
pushup(u); // 更新父亲
}
}
LL query(int u, int l, int r)
{
if (l <= tr[u].l && r >= tr[u].r) return tr[u].sum; // 返回区间信息
pushdown(u); // 也是先传递懒标记
LL v = 0;
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) v = query(u << 1, l, r);
if (r > mid) v += query(u << 1 | 1, l, r);
return v;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) scanf("%d", &w[i]); // 读入数组
build(1, 1, n); // 以1为根节点,1~n区间建树
char op[2];
int l, r, t;
// 读入修改和查询,q是查询,否则是修改
while (m -- )
{
scanf("%s%d%d", op, &l, &r);
if (*op == 'Q') printf("%lld\n", query(1, l, r));
else
{
scanf("%d", &t);
modify(1, l, r, t);
}
}
return 0;
}