目錄
前言
一、線段樹知識回顧
線段樹區間加減
區間修改維護:
區間修改的操作:
區間修改update:
線段樹的區間查詢
區間查詢:
區間查詢的操作:
遞歸查詢過程:
區間查詢query:
代碼:
二、線段樹的區間修改---乘上一個數’x'
build
區間修改
update
mulpr_update
push_up
代碼:
push_down
左:
右:
代碼:
區間查詢query
代碼:
三、例題
例題:P3373 【模板】線段樹 2 - 洛谷
?編輯
完整代碼:
總結
前言
本篇文章主要是根據P3373 【模板】線段樹 2 - 洛谷對線段樹進行進一步的講解,因為比較笨拙加上畫圖耗時大,沒有圖文搭配講解,只有純粹的文字講解,但是也是非常清楚的,很容易就能理解
一、線段樹知識回顧
我們在進一步學習前,先來看一下前置知識
開一個build函數建樹,具體操作如下:
1.定義數組:首先,需要定義一個大小為 4n 的數組,其中 n 是線段樹的葉子節點數量。這個數組將用于存儲線段樹的節點信息。
2.構建線段樹:一般將線段樹按照完全二叉樹的形式存儲在數組中。假設根節點在數組中的索引是 1,那么對于節點 i,其左子節點為 2i,右子節點為 2i + 1。
3.存儲節點信息:每個節點需要保存代表的區間范圍和相應的信息,比如區間的最大值、最小值、和等等。在數組中,可以按照某種順序依次存儲這些信息,以便后續的查詢和更新操作。
4.建立線段樹:通過遞歸或迭代的方式構建線段樹。一般會從葉子節點開始向上構建,通過合并子節點的信息得到父節點的信息,直至構建完整的線段樹。
5.查詢和更新:通過線段樹的結構和數組存儲,可以實現高效的區間查詢和更新操作。比如,對于查詢一個區間的最大值,可以通過遞歸向下查詢到包含目標區間的節點,并根據存儲的信息計算出結果。
6.記得注意邊界情況:在實現線段樹時,需要考慮樹的邊界情況,比如樹的根節點索引是 1,葉子節點索引從 n+1 開始等,以確保正確地訪問和操作節點。
// 建樹函數
void build(LL l, LL r, LL fa) {if (l == r) { // 如果區間只有一個元素t[fa] = a[l] % m; // 直接賦值return;}LL mid = (l + r) >> 1; // 計算中間節點build(l, mid, fa << 1); // 遞歸構建左子樹build(mid + 1, r, fa << 1 | 1); // 遞歸構建右子樹push_up(fa); // 更新父節點
}
線段樹區間加減
區間修改維護:
1.需要修改線段樹中某個特定區間的值時,可以通過遞歸的方式向下更新區間。
2.如果要修改的區間與當前節點表示的區間沒有交集,則無需修改該節點。
3.如果要修改的區間完全包含當前節點的區間,則直接更新當前節點的信息,并將修改操作下傳給子節點。
4.如果要修改的區間與當前節點的區間部分相交,則需要先將當前節點的信息更新,然后將修改操作同時下傳給左右子節點。
區間修改的操作:
1.區間修改的操作通常包括加法、減法、賦值等。
2.當需要對區間內的每個元素進行相同的修改時,可以利用線段樹的特性進行高效操作。
3.在修改區間時,需要根據當前節點的區間范圍、待修改區間和修改方式來確定如何操作當前節點和其子節點。
遞歸更新過程:
從線段樹的根節點開始遞歸向下更新,直到找到包含待修改區間的葉子節點。
在遞歸過程中根據節點的區間范圍和待修改區間的關系,決定如何更新節點的信息并向下傳遞修改操作。
? ? ? ?此外,對于區間操作,我們考慮引入一個名叫“ lazy tag ”(懶標記)的東西——之所以稱其“lazy”,是因為原本區間修改需要通過先改變葉子節點的值,然后不斷地向上遞歸修改祖先節點直至到達根節點,時間復雜度最高可以到達 O(nlogn) 的級別。但當我們引入了懶標記之后,區間更新的期望復雜度就降到了 O(logn) 的級別且甚至會更低。
因此,我們再弄一個tag數組,大小也是4*N
區間修改update:
void psuh_up(LL fa) {t[fa] = t[fa << 1] + t[fa << 1 | 1];//向上不斷維護父節點
}
void push_down(LL l,LL r,LL fa) {LL mid = (l + r) >> 1;t[fa << 1] += tag[fa] * (mid - l + 1);tag[fa << 1] += tag[fa];t[fa << 1|1] += tag[fa] * (r-mid);tag[fa << 1|1] += tag[fa];tag[fa] = 0;// //每次將懶惰標識下放到兩個兒子節點,自身置為0,以此不斷向下傳遞
}
void update(LL ql, LL qr, LL l, LL r, LL k, LL fa) {if (ql <= l && qr >= r) //如果區間被包含,直接返回該節點的懶惰標識{t[fa] +=k * (r - l + 1);tag[fa] += k;return;}LL mid = (l + r) >> 1;push_down(l, r, fa);//下放懶惰標識if (ql <= mid)update(ql, qr, l, mid,k, fa << 1);//朝左邊下放if (qr > mid)update(ql, qr, mid + 1, r,k, fa << 1 | 1);//右邊psuh_up(fa);//再將修改后的值向上返回,維護父節點
}
我們這一期會對update和push_down進行更改
線段樹的區間查詢
-
區間查詢:
- 當需要查詢線段樹中某個特定區間的信息時,可以通過遞歸的方式向下查詢區間。
- 如果要查詢的區間與當前節點表示的區間沒有交集,則無需查詢該節點,直接返回默認值(如0或無窮大)。
- 如果要查詢的區間完全包含當前節點的區間,則直接返回該節點存儲的信息。
- 如果要查詢的區間與當前節點的區間部分相交,則需要同時查詢左右子節點,并根據查詢結果合并得到最終結果。
-
區間查詢的操作:
- 區間查詢的操作通常包括求和、求最大值、求最小值等。
- 在查詢區間時,需要根據當前節點的區間范圍、待查詢區間和查詢方式來確定如何操作當前節點和其子節點。
-
遞歸查詢過程:
- 從線段樹的根節點開始遞歸向下查詢,直到找到包含待查詢區間的葉子節點。
- 在遞歸過程中根據節點的區間范圍和待查詢區間的關系,決定如何查詢節點的信息并向下傳遞查詢操作。
- 最終將所有查詢結果合并得到最終的區間查詢結果。
? ? ? 通過以上方法,可以實現對線段樹中特定區間的查詢操作。線段樹區間查詢是線段樹的一個重要功能,能夠快速有效地獲取區間內的信息,提高了區間查詢的效率。
區間查詢query:
LL query(LL ql, LL qr, LL l, LL r, LL fa) {LL ret = 0;if (ql <= l && qr >= r) 如果區間被包含,直接返回該節點的懶惰標識{return t[fa];}LL mid = (l + r) >> 1;push_down(l, r, fa);//沒有被包含,下放任務if (ql <= mid)ret += query(ql, qr, l, mid, fa << 1);if (qr > mid)ret += query(ql, qr, mid + 1, r, fa << 1|1);//在查詢范圍的左區間和右區間的值相加并返回return ret;
}
代碼:
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
LL n, m, t[N * 4], tag[N * 4], a[N];
void psuh_up(LL fa) {t[fa] = t[fa << 1] + t[fa << 1 | 1];//向上不斷維護父節點
}
void push_down(LL l,LL r,LL fa) {LL mid = (l + r) >> 1;t[fa << 1] += tag[fa] * (mid - l + 1);tag[fa << 1] += tag[fa];t[fa << 1|1] += tag[fa] * (r-mid);tag[fa << 1|1] += tag[fa];tag[fa] = 0;// //每次將懶惰標識下放到兩個兒子節點,自身置為0,以此不斷向下傳遞
}
LL query(LL ql, LL qr, LL l, LL r, LL fa) {LL ret = 0;if (ql <= l && qr >= r) 如果區間被包含,直接返回該節點的懶惰標識{return t[fa];}LL mid = (l + r) >> 1;push_down(l, r, fa);//沒有被包含,下放任務if (ql <= mid)ret += query(ql, qr, l, mid, fa << 1);if (qr > mid)ret += query(ql, qr, mid + 1, r, fa << 1|1);//在查詢范圍的左區間和右區間的值相加并返回return ret;
}
void update(LL ql, LL qr, LL l, LL r, LL k, LL fa) {if (ql <= l && qr >= r) //如果區間被包含,更新懶惰標識并返回{t[fa] +=k * (r - l + 1);tag[fa] += k;return;}LL mid = (l + r) >> 1;push_down(l, r, fa);//下放懶惰標識if (ql <= mid)update(ql, qr, l, mid,k, fa << 1);//朝左邊下放if (qr > mid)update(ql, qr, mid + 1, r,k, fa << 1 | 1);//右邊psuh_up(fa);//再將修改后的值向上返回,維護父節點
}
void build(LL l, LL r, LL fa) {if (l == r) // //如果左右區間相同,那么必然是葉子節,只有葉子節點是被真實賦值的{t[fa] = a[l];return;}LL mid = (l + r) >> 1;build(l, mid, fa << 1);build(mid + 1, r, fa << 1 | 1);//使用二分來優化psuh_up(fa);//此處由于我們是要通過子節點來維護父節點,所以push_up的位置應當是在回溯時將子節點的值取和交給父節點
}
int main() {cin >> n >> m;for (int i = 1; i <= n; i++)cin >> a[i];build(1, n, 1);while (m--) {int op; cin >> op;if (op == 1) {LL x, y, k; cin >> x >> y >> k;update(x, y, 1, n, k, 1);}else if(op==2){LL x, y;cin >> x >> y;cout << query(x, y, 1, n, 1) << endl;}}return 0;
}
二、線段樹的區間修改---乘上一個數’x'
老樣子,先看題目
根據題目意思先建樹
build:
代碼:
// 建樹函數
void build(LL l, LL r, LL fa) {if (l == r) { // 如果區間只有一個元素t[fa] = a[l] % m; // 直接賦值return;}LL mid = (l + r) >> 1; // 計算中間節點build(l, mid, fa << 1); // 遞歸構建左子樹build(mid + 1, r, fa << 1 | 1); // 遞歸構建右子樹push_up(fa); // 更新父節點
}
因為題目增加了一個區間乘一個數,所以我們需要維護兩個tag數組,tag1,tag2,大小都是n*4;
令tag1為區間乘一個數的數組,由于區間乘一個數,所以數組tag1全部初始化為1;
代碼:
int main() {cin >> n >>q>> m;for (int i = 0; i <= N * 4; i++)tag1[i] = 1;for (int i = 1; i <= n; i++)cin >> a[i];build(1, n, 1);while (q--) {int op; cin >> op;if (op == 2) {LL x, y, k; cin >> x >> y >> k;update(x, y, 1, n, 1, k%m);}else if (op == 3) {LL x, y; cin >> x >> y;cout << query(x, y, 1, n, 1)%m << endl;}else if (op == 1) {LL x, y, k; cin >> x >> y >> k;mulpr_update(x, y, 1, n, 1, k);}}return 0;
}
這個模去m是題目要求的,后面不會去詳細講,只會說tag數組如何維護以及query的一點改動;
區間修改
需要兩個不同的update來維護區間加和區間乘
區間加就不多贅述了
update
// 更新函數
void update(LL ql, LL qr, LL l, LL r, LL fa, LL k) {if (ql <= l && qr >= r) { // 如果更新區間完全覆蓋當前區間t[fa] = (t[fa] + (r - l + 1) * k) % m; // 更新當前節點的值tag2[fa] = (tag2[fa] + k) % m; // 更新加法標記return;}LL mid = (l + r) >> 1; // 計算中間節點push_down(l, r, fa); // 向下更新標記if (ql <= mid) // 如果更新區間在左子樹update(ql, qr, l, mid, fa << 1, k); // 更新左子樹if (qr > mid) // 如果更新區間在右子樹update(ql, qr, mid + 1, r, fa << 1 | 1, k); // 更新右子樹push_up(fa); // 更新父節點
}
來看一下區間乘
如果要更新的區間覆蓋了當前的區間,直接更新當前t數組的值,更新tag1,tag2的懶惰標識,然后return,就不需要下放懶惰標識了;
如果區間沒有覆蓋,puah_down下放懶惰標識,更新左邊,然后更新右邊,然后push_up向上更新父節點;
mulpr_update
// 乘法更新函數
void mulpr_update(LL ql, LL qr, LL l, LL r, LL fa, LL k) {if (ql <= l && qr >= r) { // 如果更新區間完全覆蓋當前區間t[fa] = t[fa] * k % m; // 更新當前節點的值tag1[fa] = tag1[fa] * k % m; // 更新乘法標記tag2[fa] = tag2[fa] * k % m; // 更新加法標記return;}LL mid = (l + r) >> 1; // 計算中間節點push_down(l, r, fa); // 向下更新標記if (ql <= mid) // 如果更新區間在左子樹mulpr_update(ql, qr, l, mid, fa << 1, k); // 更新左子樹if (qr > mid) // 如果更新區間在右子樹mulpr_update(ql, qr, mid + 1, r, fa << 1 | 1, k); // 更新右子樹push_up(fa); // 更新父節點
}
push_up
代碼:
// 向上更新函數,更新父節點的值
void push_up(LL fa) {// 父節點的值等于左右子節點的和,取模 mt[fa] = (t[fa << 1] + t[fa << 1 | 1]) % m;
}
push_down
比較有難度的一個push_down,如果思路清晰的話就會變得很簡單
左:
先將需要下放懶惰標識的區間取mid,更新樹t左子節點,用tag1和tag2 fa位置的懶惰值更新
然后更新tag1左子節點,因為是乘,所以將tag1fa父節點的懶惰標識乘上tag1[fa<<1]左子節點的懶惰標識更新該左子節點的懶惰標識
然后更新tag2左子節點,雖然是區間加的懶惰數組,但是tag1和tag2作用在同一個線段樹,所以在更新tag2時需要看看tag1[fa]有沒有懶惰標識,有的話得加上tag1[fa] * tag2[fa << 1],即如果tag1有懶惰標識,將其乘上tag2左子節點[fa<<1]原本有的懶惰標識;
右:
更新樹t右子節點,用tag1和tag2 fa位置的懶惰值更新
然后更新tag1右子節點,因為是乘,所以將tag1fa父節點的懶惰標識乘上tag1[fa<<1|1]右子節點的懶惰標識更新該左子節點的懶惰標識
然后更新tag2右子節點,雖然是區間加的懶惰數組,但是tag1和tag2作用在同一個線段樹,所以在更新tag2時需要看看tag1[fa]有沒有懶惰標識,有的話得加上tag1[fa] * tag2[fa << 1|1],即如果tag1有懶惰標識,將其乘上tag2左子節點[fa<<1|1]原本有的懶惰標識;
代碼:
// 向下更新函數,處理懶惰標記
void push_down(LL l, LL r, LL fa) {LL mid = (l + r) >> 1; // 計算中間節點// 更新左子節點t[fa << 1] = (tag1[fa] * t[fa << 1] % m + tag2[fa] * (mid - l + 1) % m) % m;tag1[fa << 1] = (tag1[fa << 1] * tag1[fa]) % m; // 更新乘法標記tag2[fa << 1] = (tag2[fa] + tag1[fa] * tag2[fa << 1] % m) % m; // 更新加法標記// 更新右子節點t[fa << 1 | 1] = (tag1[fa] * t[fa << 1 | 1] % m + (tag2[fa] * (r - mid)) % m) % m;tag1[fa << 1 | 1] = (tag1[fa << 1 | 1] * tag1[fa]) % m; // 更新乘法標記tag2[fa << 1 | 1] = (tag2[fa] + tag1[fa] * tag2[fa << 1 | 1] % m) % m; // 更新加法標記// 清空當前節點的標記tag1[fa] = 1;tag2[fa] = 0;
}
區間查詢query
代碼:
// 查詢函數
LL query(LL ql, LL qr, LL l, LL r, LL fa) {LL ret = 0; // 初始化返回值if (ql <= l && qr >= r) { // 如果查詢區間完全覆蓋當前區間return t[fa] % m; // 返回當前節點的值}LL mid = (l + r) >> 1; // 計算中間節點push_down(l, r, fa); // 向下更新標記if (ql <= mid) // 如果查詢區間在左子樹ret = (ret + query(ql, qr, l, mid, fa << 1)) % m; // 查詢左子樹if (qr > mid) // 如果查詢區間在右子樹ret = (ret + query(ql, qr, mid + 1, r, fa << 1 | 1)) % m; // 查詢右子樹return ret % m; // 返回結果
}
三、例題
例題:P3373 【模板】線段樹 2 - 洛谷
完整代碼:
#include <iostream>
#include <cstring>
using namespace std;// 定義常量 N,表示數組的最大大小
const int N = 1e5 + 10;// 定義長整型別名 LL
typedef long long LL;// 全局變量
LL n, m, q; // n: 數組大小, m: 模數, q: 查詢次數
LL t[N * 4], a[N]; // t: 線段樹數組, a: 原始數組
LL tag1[N * 4], tag2[N * 4]; // tag1: 乘法標記, tag2: 加法標記// 向上更新函數,更新父節點的值
void push_up(LL fa) {// 父節點的值等于左右子節點的和,取模 mt[fa] = (t[fa << 1] + t[fa << 1 | 1]) % m;
}// 向下更新函數,處理懶惰標記
void push_down(LL l, LL r, LL fa) {LL mid = (l + r) >> 1; // 計算中間節點// 更新左子節點t[fa << 1] = (tag1[fa] * t[fa << 1] % m + tag2[fa] * (mid - l + 1) % m) % m;tag1[fa << 1] = (tag1[fa << 1] * tag1[fa]) % m; // 更新乘法標記tag2[fa << 1] = (tag2[fa] + tag1[fa] * tag2[fa << 1] % m) % m; // 更新加法標記// 更新右子節點t[fa << 1 | 1] = (tag1[fa] * t[fa << 1 | 1] % m + (tag2[fa] * (r - mid)) % m) % m;tag1[fa << 1 | 1] = (tag1[fa << 1 | 1] * tag1[fa]) % m; // 更新乘法標記tag2[fa << 1 | 1] = (tag2[fa] + tag1[fa] * tag2[fa << 1 | 1] % m) % m; // 更新加法標記// 清空當前節點的標記tag1[fa] = 1; tag2[fa] = 0;
}// 建樹函數
void build(LL l, LL r, LL fa) {if (l == r) { // 如果區間只有一個元素t[fa] = a[l] % m; // 直接賦值return;}LL mid = (l + r) >> 1; // 計算中間節點build(l, mid, fa << 1); // 遞歸構建左子樹build(mid + 1, r, fa << 1 | 1); // 遞歸構建右子樹push_up(fa); // 更新父節點
}// 查詢函數
LL query(LL ql, LL qr, LL l, LL r, LL fa) {LL ret = 0; // 初始化返回值if (ql <= l && qr >= r) { // 如果查詢區間完全覆蓋當前區間return t[fa] % m; // 返回當前節點的值}LL mid = (l + r) >> 1; // 計算中間節點push_down(l, r, fa); // 向下更新標記if (ql <= mid) // 如果查詢區間在左子樹ret = (ret + query(ql, qr, l, mid, fa << 1)) % m; // 查詢左子樹if (qr > mid) // 如果查詢區間在右子樹ret = (ret + query(ql, qr, mid + 1, r, fa << 1 | 1)) % m; // 查詢右子樹return ret % m; // 返回結果
}// 更新函數
void update(LL ql, LL qr, LL l, LL r, LL fa, LL k) {if (ql <= l && qr >= r) { // 如果更新區間完全覆蓋當前區間t[fa] = (t[fa] + (r - l + 1) * k) % m; // 更新當前節點的值tag2[fa] = (tag2[fa] + k) % m; // 更新加法標記return;}LL mid = (l + r) >> 1; // 計算中間節點push_down(l, r, fa); // 向下更新標記if (ql <= mid) // 如果更新區間在左子樹update(ql, qr, l, mid, fa << 1, k); // 更新左子樹if (qr > mid) // 如果更新區間在右子樹update(ql, qr, mid + 1, r, fa << 1 | 1, k); // 更新右子樹push_up(fa); // 更新父節點
}// 乘法更新函數
void mulpr_update(LL ql, LL qr, LL l, LL r, LL fa, LL k) {if (ql <= l && qr >= r) { // 如果更新區間完全覆蓋當前區間t[fa] = t[fa] * k % m; // 更新當前節點的值tag1[fa] = tag1[fa] * k % m; // 更新乘法標記tag2[fa] = tag2[fa] * k % m; // 更新加法標記return;}LL mid = (l + r) >> 1; // 計算中間節點push_down(l, r, fa); // 向下更新標記if (ql <= mid) // 如果更新區間在左子樹mulpr_update(ql, qr, l, mid, fa << 1, k); // 更新左子樹if (qr > mid) // 如果更新區間在右子樹mulpr_update(ql, qr, mid + 1, r, fa << 1 | 1, k); // 更新右子樹push_up(fa); // 更新父節點
}// 主函數
int main() {cin >> n >> q >> m; // 輸入數組大小 n, 查詢次數 q, 模數 mfor (int i = 0; i <= N * 4; i++) tag1[i] = 1; // 初始化乘法標記為 1for (int i = 1; i <= n; i++) cin >> a[i]; // 輸入數組元素build(1, n, 1); // 構建線段樹while (q--) { // 處理每個查詢int op; cin >> op; // 輸入操作類型if (op == 2) { // 加法更新操作LL x, y, k; cin >> x >> y >> k; // 輸入區間 [x, y] 和加數 kupdate(x, y, 1, n, 1, k % m); // 執行加法更新}else if (op == 3) { // 查詢操作LL x, y; cin >> x >> y; // 輸入查詢區間 [x, y]cout << query(x, y, 1, n, 1) % m << endl; // 輸出查詢結果}else if (op == 1) { // 乘法更新操作LL x, y, k; cin >> x >> y >> k; // 輸入區間 [x, y] 和乘數 kmulpr_update(x, y, 1, n, 1, k); // 執行乘法更新}}return 0; // 程序結束
}
總結
本期關于線段樹的講解就到這里,有什么疑問或者有什么錯誤的地方歡迎大家一起交流學習,下期帶來線段樹的離散化,二分搜索等進階內容