线段树学习笔记
线段树学习笔记
作为一大重点,我将仔细讲解线段树。
开始讲解
板子支持区间查询和区间修改,维护区间和。
我们考虑使用这样建一颗树:
然后我们使用 $ w_u $ 表示结点 $ u $ 表示的区间和。比如说结点 $ 2 $ 表示 $ 1 $ 到 $ 3 $ 的区间和,即 $ w_2 = a_1 + a_2 + a_3 $。
例子举完力,我们看代码实现。
合并
合并是线段树的一大难点,但是区间和的话直接加起来就好了。
const int Maxn = 5e5 + 10; int w[Maxn * 4]; inline void pushup (int u) { w[u] = w[u * 2] + w[u * 2 + 1]; }
建树
这部分也比较简单,就是找到叶子结点就把 $ w $ 的值赋值为原数组的值。否则就合并就行了。
inline void build (int u, int l, int r) { if (l == r) { w[u] = a[l]; return ; } int mid = (l + r) >> 1; build (u * 2, l, mid), build (u * 2 + 1, mid + 1, r); pushup (u); }
单点查询
单点查询的话我们考虑一直查找,$ x $ 表示第 $ x $ 个元素,找到就返回,否则的话就分别在左边和右边查找。这里我们根据 $ x $ 的位置判断查找左儿子还是右儿子。
inline int search (int u, int l, int r, int x) { if (l == r) { return w[u]; } int mid = (l + r) >> 1; if (x <= mid) return search (u * 2, l, mid, x); else return search (u * 2 + 1, mid + 1, r, x); }
单点修改
单点修改是一个道理,我们把查询的部分改成修改就行了,代码见下面。这里要注意,修改以后要进行合并,以更新所有的数据。
inline void modify (int u, int l, int r, int x, int y) { if (l == r) { w[u] += y; return ; } int mid = (l + r) >> 1; if (x <= mid) modify (u * 2, l, mid, x, y); else modify (u * 2 + 1, mid + 1, r, x, y); pushup (u); }
我们会注意到,单点查询和修改的复杂度全都是 $ log $ 的。接下来我们看区间修改和查询。
区间相交的判断
我们再修改区间之前,我们要先写一个区间相交的判断。
代码很简单。
inline bool Inrange (int L, int R, int l, int r) { return (l <= L && R <= r); } inline bool OutofRange (int L, int R, int l, int r) { return (R < l || L > r); }
区间查询
区间查询的话,我们考虑把一个区间分成几部分。比如说上图,$ 1 $ 到 $ 4 $ 的区间和可以表示为 $ 1 $ 到 $ 3 $ 和 $ 4 $,如果当前区间被完全包含,就直接返回 $ w $ 值即可。如果不包含,返回 $ 0 $。否则递归左右儿子。
代码如下:
inline int query (int u, int L, int R, int l, int r) { if (Inrange (L, R, l, r)) return w[u]; if (OutofRange (L, R, l, r)) return 0; int M = (L + R) >> 1; return query (u * 2, L, M, l, r) + query (u * 2 + 1, M + 1, R, l, r); }
区间修改
区间修改的话我们要引入懒标记。懒标记的话就是延迟,也就是说我们再修改的时候不需要立刻马上修改,可以标记一下放在那里,然后等回头要查询了再修改。
然后我们先写一个打标记的函数。
int tag[Maxn * 4]; inline void make_tag (int u, int x, int len) { tag[u] += x; w[u] += x * len; }
然后我们考虑标记下放,也就是处理当前节点,然后把标记下放到左右儿子。
inline void pushdown (int u, int L, int R) { int M = (L + R) >> 1; make_tag (u * 2, M - L + 1, tag[u]); make_tag (u * 2 + 1, R - M, tag[u]); tag[u] = 0; }
我们在区间查询的代码里面添加一下标记下放。
inline int query (int u, int L, int R, int l, int r) { if (Inrange (L, R, l, r)) return w[u]; if (OutofRange (L, R, l, r)) return 0; pushdown (u, L, R); int M = (L + R) >> 1; return query (u * 2, L, M, l, r) + query (u * 2 + 1, M + 1, R, l, r); }
接下来就是区间修改力。我们再修改的时候和查询是一样的。完全包含就直接打标记,如果是不相交就直接 return。否则递归左右儿子。
inline void update (int u, int L, int R, int l, int r, int x) { if (Inrange (L, R, l, r)) { make_tag (u, R - L + 1, x); } if (OutofRange (L, R, l, r)) return ; pushdown (u, L, R); int M = (L + R) >> 1; update (u * 2, L, M, l, r, x); update (u * 2 + 1, M + 1, R, l, r, x); pushup (u); }
这就是基础线段树的模板代码。