線段樹(超詳解)

缪语博發表於2024-10-11

線段樹(超詳解)

Author :銅陵一中 繆語博

在網上看了幾個講線段樹的,都感覺不咋地,自己琢磨了幾天,大致弄明白了。於是趁興寫了一篇關於線段樹的文章,希望拯救那些看\(oi-wiki\)看不懂的\(oier\)

前言

在閱讀本文之前,你需要明確:

  • 本人碼風可能與你不同,請多諒解。
  • 有可能我的程式碼用到什麼\(define\)可能和晚上的“簡單”違背,請不要誤解。寫多了你就會知道,這樣寫真的很方便。

命名規則:

  • \(p\):當前的線段樹的節點。
  • \(l,r\)當前線段樹的左右區間範圍。
  • \(ql,qr\)目標線段樹的左右區間範圍。

\(Chapter 1\) 幹嘛要用線段樹?

Put simply,就是區間操作。題目中出現區間,大機率就是線段樹了。有人問:我直接一個陣列不就行了嗎?

\(No,No,No,slow!\)

假設有\(10^6\)個操作,每一次操作你都要修改,求和,求最大值,更新————

\(TLE!\)

線段樹就是來解決這個問題的。

但是為什麼呢?

一個類似字首和的思想。思考這樣一個問題:假如你做人口普查,銅陵市政府統計了銅陵市的人口。現在安徽省政府來統計,還需要統計銅陵市的人口嗎?

顯然不需要,直接把銅陵市政府的資料拿來用不就行了嗎?

同理,你已經算出了某個區間的資料,你直接拿來用就可以了,幹嘛還要再算一遍?你是嫌\(1s\)的時間限制短了?

那麼問題來了,怎麼操作呢?

\(Chapter 2\) 什麼是線段樹?怎麼建線段樹?

在學習之前,你得有一些樹的基本知識,比如說:

  • 什麼是樹(廢話)。
  • 在一棵完全二叉樹中(根節點編號為\(1\)),節點\(p\)的左兒子的編號為\(2p\),右兒子的編號為\(2p+1\)

先來了解一下線段樹為什麼快。

試問:怎麼查詢最快?

二分。

對!線段樹就可以理解為“二分”,二分割槽間,這樣查詢就會變得很快,直降\(O(logn)\)

這樣,我們就可以開始建樹了。

建樹過程(OI-Wiki上寫得已經夠詳細了,移步一下吧)。

連結OI-Wiki

好的,預設你已經知道了線段樹長什麼樣子了。

對於一個非葉子節點\(p\),其均有一個左子樹和右子樹,剛剛才講過,左兒子的編號為\(2p\),右兒子的編號為\(2p+1\)

為了方便起見,我們使用\(define\)來簡便定義左子樹和右子樹。

#define ls (p << 1)
#define rs (p << 1 | 1)
  • 其中, (p << 1)(p << 1 | 1)的意思分別是\(2\times p\)\(2\times p + 1\),這樣定義更加簡便快速。

於是我們開始建樹。

首先,對於一個區間\([l,r]\),如果訪問時,\(l = r\),那麼其就是葉子結點,否則就不是葉子結點(廢話)。我習慣於將\(l,r\)放在引數裡傳遞,而不是用結構體來定義,我認為這樣可能會簡便一些。

有人問:那節點的區間長度如何定義叻?

用一個siz陣列不就行了嗎?

以建立一棵求區間和的線段樹為例。

  • 這裡還有一個定義,就是\(mid\)的定義,也使用宏定義:#define mid ((l + r) >> 1)

#define N 100001
#define ll long long
#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

int n, m;
int a[N];
ll tree[N << 2];
int siz[N << 2];
int lazy[N << 2];

void build(int p, int l, int r) {
    lazy[p] = 0;
    if(l == r)  {
        return ;
    }
    build(ls, l, mid);
    build(rs, mid + 1, r);
}
  • 這裡的\(lazy\)陣列你暫時可以不用管,這是以後要講到的。

\(Chapter 3\) 線段樹的初始化

簡單了,加幾行就行了。

首先是葉子結點的資料,直接放區間(節點)所對應的值就好了。

然後是非葉子結點的維護,用一個upd函式來更新tree的值,用一個upds函式來更新siz的大小。


void upd(int p) {
    tree[p] = tree[ls] + tree[rs];
}

void upds(int p) {
    siz[p] = siz[ls] + siz[rs];
}

void build(int p, int l, int r) {

    lazy[p] = 0;

    if(l == r)  {
        siz[p] = 1;
        tree[p] = a[l];
        return ;
    }

    build(ls, l, mid);
    build(rs, mid + 1, r);

    upd(p);
    upds(p);
}

\(Chapter 4\) 線段樹的查詢

還是以建立一棵求區間和的線段樹為例。

泰見但辣!

如果當前的區間\([l,r]\)完全包含於查詢區間\([ql, qr]\),直接加和即可。

如果沒有被完全包含,拆成它的左子樹和右子樹,不斷縮小範圍就行了。

這裡理解一個問題:我怎麼知道應該拆左子樹還是拆右子樹?

比如說,當有這樣一種情況:

\([l,r]=[4,7]\)\([ql,qr]=[3,5]\)

發現沒有被完全包含,其中,\(qr \geq l\),所以可以看它的左子樹和右子樹。如果左子樹滿足條件,就既搜左子樹,又搜右子樹,反覆遞迴,直至區間被完全覆蓋。如果只有右子樹滿足條件,就只搜右子樹,直至區間被完全覆蓋。

ll qry(int p, int l, int r, int ql, int qr) {
    if(ql <= l && r <= qr) {
        return tree[p];
    }

    ll sum = 0;

    if(ql <= mid) {
        sum += qry(ls, l, mid, ql, qr);
    }
    if(qr > mid) {
        sum += qry(rs, mid + 1, r, ql, qr);
    }

    return sum;
}

\(Chapter 5\) 懶標記

很重要的一部分,一定要反覆看,比較難理解。

什麼是懶標記?

就是懶(廢話)。

為什麼?

想一想,如果我每一次增加區間的值,每一次更新都全部下放到子樹,那時間複雜度就是無法估量的。所以,只有碰到查詢時,或者要更改這個區間的一部分的時候才會全部下放到子樹,並且是下放到兒子結點,這樣做會更快(想一想,為什麼)。

定義:\(lazy[p]\)表示當結點\(p\)\(tree\)值已經更新時,其兒子結點還沒有下放的數值。可能很少有文章強調當結點\(p\)\(tree\)值已經更新時,但是這個地方理解很重要!這樣可以使你的思路更加清晰。

於是,我們得到了一個下放結點\(p\)的懶標記的程式碼:

void pushd(int p) {
    tree[ls] += lazy[p] * siz[ls];
    tree[rs] += lazy[p] * siz[rs];

    lazy[ls] += lazy[p];
    lazy[rs] += lazy[p];

    lazy[p] = 0;
}

在更新中的具體程式碼下節講,在查詢中的放在結尾的程式碼裡,自行理解。

\(Chapter 6\) 更新區間

還是以上面那個例子為例(有語病嗎?),更新區間是將區間內所有的值加\(k\)

現在就很好理解了。

  1. 如果當前結點被完全覆蓋,直接將\(tree\)值加上\(siz[p] \times k\)即可。
  2. 如果沒有,繼續拆。
  3. 注意下放懶標記!
void mdf(int p, int l, int r, int ql, int qr, int k) {
    if(ql <= l && r <= qr) {
        tree[p] += 1ll * siz[p] * k;
        lazy[p] += k;
        return;
    }
    pushd(p);
    if(ql <= mid) {
        mdf(ls, l, mid, ql, qr, k);
    }
    if(qr > mid) {
        mdf(rs, mid + 1, r, ql, qr, k);
    }
    upd(p);
}

\(Chapter 7\) 終章\(Code\)

#include <bits/stdc++.h>

#define N 100001
#define ll long long
#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

using namespace std;

int n, m;
int a[N];
ll tree[N << 2];
int siz[N << 2];
int lazy[N << 2];

void upd(int p) {
    tree[p] = tree[ls] + tree[rs];
}

void upds(int p) {
    siz[p] = siz[ls] + siz[rs];
}

void pushd(int p) {
    tree[ls] += lazy[p] * siz[ls];
    tree[rs] += lazy[p] * siz[rs];

    lazy[ls] += lazy[p];
    lazy[rs] += lazy[p];

    lazy[p] = 0;
}

void build(int p, int l, int r) {

    lazy[p] = 0;

    if(l == r)  {
        siz[p] = 1;
        tree[p] = a[l];
        return ;
    }

    build(ls, l, mid);
    build(rs, mid + 1, r);

    upd(p);
    upds(p);
}

void mdf(int p, int l, int r, int ql, int qr, int k) {
    if(ql <= l && r <= qr) {
        tree[p] += 1ll * siz[p] * k;
        lazy[p] += k;
        return;
    }
    pushd(p);
    if(ql <= mid) {
        mdf(ls, l, mid, ql, qr, k);
    }
    if(qr > mid) {
        mdf(rs, mid + 1, r, ql, qr, k);
    }
    upd(p);
}

ll qry(int p, int l, int r, int ql, int qr) {
    if(ql <= l && r <= qr) {
        return tree[p];
    }
    pushd(p);

    ll sum = 0;

    if(ql <= mid) {
        sum += qry(ls, l, mid, ql, qr);
    }
    if(qr > mid) {
        sum += qry(rs, mid + 1, r, ql, qr);
    }

    return sum;
}

int main() {

    cin >> n >> m;

    for(int i = 1; i <= n; ++i) {
        cin >> a[i];
    }

    build(1, 1, n);

    while(m--) {
        int op, x, y, k;

        cin >> op >> x >> y;

        if(op == 1) {
            cin >> k;
            mdf(1, 1, n, x, y, k);
        }
        else {
            cout << qry(1, 1, n, x, y) << endl;
        }
    }

    return 0;
}

也希望看完這篇文章的你能點一個大大的贊,給一個大大的支援!

My Twitter

My Website

My Zhihu

完結撒花!