線段樹(超詳解)
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\)。
現在就很好理解了。
- 如果當前結點被完全覆蓋,直接將\(tree\)值加上\(siz[p] \times k\)即可。
- 如果沒有,繼續拆。
- 注意下放懶標記!
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
完結撒花!