线段树是用于维护区间信息的一种数据结构,可以在 O ( log ⁡ n ) O(\log n) O(logn)的复杂度内实现区间修改,区间查询等操作,还可以维护各种满足结合律的信息。

原理:

线段树的建树:设有数组 a i , i ∈ [ 1 , n ] a_i,i \in[1,n] ai,i[1,n],把一整个数组区间划分为两个子区间,设区间 [ l , r ] [l,r] [l,r],那么取区间中点 m i d = ⌊ l + r 2 ⌋ mid=\left \lfloor \frac{l+r}{2} \right \rfloor mid=2l+r,那么区间就会被划分为 [ l , m i d ] , [ m i d + 1 , r ] [l,mid],[mid+1,r] [l,mid],[mid+1,r],一直递归下去,直到 l = r l=r l=r,这个过程非常像二叉树,所以我们就用一棵树来记录每个区间。递归到达边界时区间长度为 0 0 0,那么就可以代表一个数组 a i a_i ai,回溯的时候进行合并,这样就能维护数组的信息。与二叉树一样,设节点编号为 u u u,那么左儿子的编号就为 2 ∗ u 2*u 2u,右儿子节点的编号就为 2 ∗ u + 1 2*u+1 2u+1,在程序中 2 ∗ u 2*u 2u可以用u << 1来代替, 2 ∗ u + 1 2*u+1 2u+1可以用u << 1 | 1来代替,这样就方便递归建树了。

线段树结构体:

struct Node {
   
    int l, r;
    //需要维护的信息和懒标记
} tr[N << 2];

代码:

void build(int u, int l, int r) {
   
    if (l == r) tr[u] = {
   l, r};
    else {
   
        tr[u] = {
   l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

区间查询:

L , R L,R L,R为查询区间, t r [ u ] . l , t r [ u ] . r tr[u].l,tr[u].r tr[u].l,tr[u].r为节点左右端点, m i d mid mid为节点中点

① : L , R > m i d ①:L,R > mid :L,R>mid 查询右子树

② : L ≤ m i d , R > m i d ②:L \le mid,R> mid :Lmid,R>mid 查询左右子树

③ : t r [ u ] . l ≥ L , t r [ u ] . r ≤ R ③:tr[u].l \ge L ,tr[u].r \le R :tr[u].lL,tr[u].rR 直接返回整个区间

④ : L ≤ m i d , R > m i d ④:L \le mid,R >mid :Lmid,R>mid 查询左右子树

⑤ : L , R ≤ m i d ⑤:L,R \le mid :L,Rmid 查询左子树


例如求区间 [ l , r ] [l,r] [l,r]的和,我们可以通过线段树的性质,合并两个子区间的和来求最终答案,首先子区间必须记录这个信息才可以合并,于是我们就引入了 pushup \text{pushup} pushup这个操作,来维护向上传递的信息。

区间和的 pushup : \text{pushup}: pushup:

void pushup(int u) {
   
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

很好理解,就是总区间的和等于两个子区间的和,那么最初的区间和就是在建树过程中递归边界遍历到区间长度为 0 0 0的区间时记录的,此时 t r [ u ] . s u m = a [ l ] tr[u].sum=a[l] tr[u].sum=a[l],所以建树的时候需要在最后 pushup \text{pushup} pushup

代码:

int ask(int u, int l, int r) {
   
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
    int mid = tr[u].l + tr[u].r >> 1, res = 0;
    if (l <= mid) res += ask(u << 1, l, r);
    if (r > mid) res += ask(u << 1 | 1, l, r);
    return res;
}

单点修改:

假设要将 a p o s a_{pos} apos修改为 c c c,那么只需要二分地找到位置 p o s pos pos,当 l = r l=r l=r时,把 a l a_l al改为 c c c,由于修改了单个值,需要重新 pushup \text{pushup} pushup一下,向上传递更新的信息。

代码:

void modify(int u, int pos, int c) {
   
    if (tr[u].l == pos && tr[u].r == pos) {
   
        tr[u] = {
   l, r, c};
    } else {
   
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) {
   
            modify(u << 1, pos, c);
        } else {
   
            modify(u << 1 | 1, pos, c);
        }
        pushup(u);
    }
}

区间修改:

假设要把区间 [ l , r ] [l,r] [l,r]的所有数修改为 c c c,如果按照常规方法每个数都做单点修改,时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),是比较慢的,所以我们可以引入懒标记,用 pushdown \text{pushdown} pushdown操作来维护,具体操作就是先对一个区间做一个标记,这个标记表示对这个区间所有数都 + c +c +c,需要用到的时候再用 pushdown \text{pushdown} pushdown下传到子节点,懒标记清空。 pushdown \text{pushdown} pushdown必须在查询或修改之前就下传,保证答案的正确性。

区间修改、区间和的 pushdown : \text{pushdown}: pushdown:

void pushdown(int u) {
   
    tr[u << 1].add += tr[u].add, tr[u << 1 | 1].add += tr[u].add;
    tr[u << 1].sum += (tr[u << 1].r - tr[u << 1].l + 1) * tr[u].add, tr[u << 1 | 1].sum += (tr[u << 1 | 1].r - tr[u << 1 | 1].l + 1) * tr[u].add;
    tr[u].add = 0;
}