线段树学习笔记

线段树学习笔记

作为一大重点,我将仔细讲解线段树。

开始讲解

板子支持区间查询和区间修改,维护区间和。

我们考虑使用这样建一颗树:

 

 然后我们使用 $ 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);

}

这就是基础线段树的模板代码。

posted @ 2023-01-28 09:45  __Tzf  阅读(16)  评论(0编辑  收藏  举报