線段樹入門

線段樹入門

引題

有一個包含\(N\)個數的序列(\(N \leq 1e6\)),給\(Q(\le 1e6)\)個操做,每一個操做是下面兩種中的一種:php

  • 區間加:給定\(l,r,x\),將序列\(N\)下標\(\in [l, r]\)的數加上\(x\)
  • 區間求和:給定\(l,r\),詢問下標\(\in [l,r]\)的數的和

一種很暴力的想法是對每一個操做都一遍循環進行修改、求和,顯然會超時;看到區間求和很容易就能想到前綴和,這樣能夠把區間求和降到常數複雜度,然而區間加仍是\(O(N)\);這時就須要線段樹登場了(不知道爲啥排版變得巨醜,你們將就一下吧)node

介紹

線段樹是一種實用的數據結構,它能夠快速地處理區間操做,維護區間信息。線段樹是一棵二叉樹,它的每個節點存儲的是一個區間的信息(如區間和, 左右端點等),以下圖所示ios

筆者我的比較習慣用結構體來定義每個節點,若是隻開\(2N\)個節點,有一些狀況是不夠的,索性開到\(4N\),並從上到下,從左向右進行編號,根節點編號爲1,其左兒子是2,右兒子是3,以次類推:數據結構

#define ls (k << 1) // 左兒子
#define rs (ls | 1) // 右兒子

struct Node {
    int l, r, sum, lazy; // l爲左端點,r爲端點,sum是區間和, lazy是懶標記下文會講
    Node() {}
    Node(int _l, int _r, int _sum, int _lazy=0) : l(_l), r(_r), sum(_sum), lazy(_lazy) {}
    inline int length() {return r - l + 1; } // 返回區間長度
    inline ll mi() { return (l + r) >> 1; } // 返回中間點
} node[N << 2];

維護區間信息

每次更新了較低一層的區間信息時,須要維護其父節點的信息,好比區間信息爲區間和\(sum\)時,維護時父節點的\(sum\)值等於其左右兒子的\(sum\)值的和post

inline update(int k) {
    node[k].sum = node[ls].sum + node[rs].sum;
}

建樹

建樹從最上一層節點開始向下,一旦遇到葉子節點(區間長度爲1的點),說明到最底層了,則返回,再遞歸地更新其父節點的區間信息ui

void build(int l, int r, int k) { // k是編號
    if(l == r) { // 葉子節點,輸入它的值並返回
        scanf("%d", &a);
        node[k] = Node(l, r, a);
        return ;
    }
    node[k].l = l; node[k].r = r;
    int mid = node[k].mi();
    build(l, mid, ls);
    build(mid + 1, r, rs);
    update(k);
}

區間加

(注意區分等待加的區間\([l,r]\)節點\(k\)上的區間\([node[k].l, node[k].r]\)!!)在區間\([l,r]\)上加\(addnum\):從根節點開始,若是咱們所在的節點的區間\([node[k].l, node[k].r] \subseteq [l,r]\),那麼說明這個節點區間的每一個值都須要被加\(addnum\);不然,說明節點上的區間沒有被徹底包含在\([l,r]\)中,若是\(r>mid(mid是節點的區間中值)\),說明區間\([mid + 1, r]\)這個區間還須要加上\(addnum\),因此進入右兒子節點;若是\(l <= mid\),說明區間\([l, mid]\)這個區間還須要加上\(addnum\),因此進入右兒子節點。須要注意的是,後兩種狀況徹底有可能同時知足。咱們再仔細考慮區間加,爲了維護線段樹使其知足左右兒子的\(sum\)之和等於父節點的\(sum\),將父節點的\(sum\)更新以後應該要把它的全部子節點都更新,再用一下上面的圖,好比說咱們讓\([6, 10]\)加10,那爲了維護線段樹,\([6, 10]\)的子節點們都須要加10,總共須要9次加操做,這形成了一個很嚴重的問題:這樣的區間加甚至比暴力還要慢!一個本來是\(O(N)\)的操做被咱們改進成了\(O(NlogN)\),這時,一個重要的思想出現:懶標記。它的思想是先僅維護最上一層的區間信息,而延遲對其子節點的更新,這樣作的好處在於能夠把區間加累積起來,等有須要時將懶標記下傳一次性更新子節點,從而有效下降複雜度spa

inline void push(int k) { // 懶標記下傳
    node[ls].lazy = node[rs].lazy = node[k].lazy;
    node[ls].sum += node[ls].length() * node[k].lazy;
    node[rs].sum += node[rs].length() * node[k].lazy;
    node[k].lazy = 0;
}

inline void add(int k) {
    if(node[k].l >= l && node[k].r <= r) { // 徹底包含
        node[k].sum += node[k].length() * addnum;
        node[k].lazy += addnum; // 懶標記
        return ;
    }
    if(node[k].lazy) push(k); // 下傳
    if(r > node[k].mi()) add(rs);
    if(l <= node[k].mi()) add(ls); // 不能是else if
    update(k);
}

區間求和

區間求和的步驟基本和區間加同樣,代碼也是十分相似code

inline int query(int k) {
    if(node[k].l >= l && node[k].r <= r)
        return node[k].sum;
    int ans = 0;
    if(node[k].lazy) push(k);
    if(r > node[k].mi()) ans += query(rs);
    if(l <= node[k].mi()) ans += query(ls);
    return ans;
}

板子

玩整版開了long long,主要是由於不少題區間一求和就容易爆intorm

#include <cstdio>
#include <cstring>
#include <iostream>
#define mid ((l + r) >> 1)
#define ls (k << 1)
#define rs (k << 1 | 1)
typedef long long ll;
const int N = 1e6+5;

struct Node {
    ll l, r, sum, lazy;
    Node() {}
    Node(ll _l, ll _r, ll _sum, ll _lazy = 0L) : l(_l), r(_r), sum(_sum), lazy(_lazy) {}
    inline ll length() { return r - l + 1; }
    inline ll mi() { return (l + r) >> 1; }
}node[N << 2];

ll n, m, l, r, addnum;

inline ll read() { // 快讀
    ll x = 0;
    char ch = getchar();
    while(ch < '0' || ch > '9')
        ch = getchar();
    while(ch >= '0' && ch <= '9') {
        x = (x << 3) + (x << 1) + (ch ^ 48);
        ch = getchar();
    }
    return x;
}

inline void update(int k) {
    node[k].sum = node[ls].sum + node[rs].sum;
}

inline void push(int k) {
    node[ls].lazy += node[k].lazy;
    node[rs].lazy += node[k].lazy;
    node[ls].sum += node[k].lazy * node[ls].length();
    node[rs].sum += node[k].lazy * node[rs].length();
    node[k].lazy = 0L;
}

void build(int l, int r, int k) {
    if(l == r) {
        ll a = read();
        node[k] = Node(l, r, a);
        return ;
    }
    node[k].l = l; node[k].r = r;
    build(l, mid, ls);
    build(mid + 1, r, rs);
    update(k);
}

inline void add(int k) {
    if(node[k].l >= l && node[k].r <= r) {
        node[k].sum += node[k].length() * addnum;
        node[k].lazy += addnum;
        return ;
    }
    if(node[k].lazy) push(k);
    if(r > node[k].mi()) add(rs);
    if(l <= node[k].mi()) add(ls);
    update(k);
}

inline ll query(int k) {
    if(node[k].l >= l && node[k].r <= r)
        return node[k].sum;
    ll ans = 0L;
    if(node[k].lazy) push(k);
    if(r > node[k].mi()) ans += query(rs);
    if(l <= node[k].mi()) ans += query(ls);
    return ans;
}

int main() {
    n = read(), m = read();
    build(1L, n, 1L);
    while(m--) {
        ll type;
        type = read(); l = read(), r = read();
        if(type == 2L) // 區間查詢
            printf("%lld\n", query(1L));
        else if(type == 1L) { // 區間加
            addnum = read();
            add(1L);
        }
    }
    return 0;
}

各類類型

最基礎的幾種

  • 區間加 + 區間求和,這是最基本的線段樹,板子題luogu 3372blog

  • 區間乘 + 區間求和,其實像維護加法懶標記同樣,再維護一個乘法的懶標記就能夠了,再稍微改改懶標記下傳,板子題luogu 3373
  • 區間修改 + 區間求最值,若是沒有區間修改,那打個ST就好了(不知道ST的話能夠百度一下,不少博客都講得很清楚),常數還小,有修改就用線段樹就行,維護也很簡單,取個max就好了

區間加 + 區間求平方之和(或者立方之和)

\[ (a_i + x)^2 = a_i^2+2x*a_i+x^2 \\ \sum_{i=l}^{r}((a_i+x)^2) = \sum_{i=l}^{r}a_i^2+2x*\sum_{i=l}^{r}a_i+(l-r+1)*x^2 \]

能夠按照上面的公式維護\(\sum a_i\)\(\sum a^2_i\),立方相似,題目HDU 4578 Transformation

區間開根號(向下取整) + 區間求和

開根號操做會讓區間裏的值變得更加接近,那隻要同時維護區間\(max\)\(min\),若是\(max = min\)\(sum = length * max\);若是\(max \neq min\),就暴力地把這個區間上的數都開方,維護懶標記開方次數,且由於一個數\(n\)最多被開方\(logn\)次就會變成1,因此每一個數暴力其實最多\(O(logn)\),不會超時。區間除的思想也是同樣的,由於除法也會使得區間裏的值變得更加接近

例題

然而線段樹的不少題都結合了各類技巧,以下面這道:

相關文章
相關標籤/搜索