線段樹原理以及一些模板題
阿新 • • 發佈:2021-08-17
線段樹
百度百科定義:線段樹是一種二叉搜尋樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點
個
所以我們的線段樹去掉最後一層後節點的個數是嚴格小於
而最後一層最多不會超過上一層的倆倍 即最壞情況下有 <
所以我們長度一般取
線段樹
定義
百度百科定義:線段樹是一種二叉搜尋樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點
藍書定義:線段樹是一種基於分治思想的二叉樹結構,用於在區間上進行資訊統計
原理
- 線段樹的每個節點都代表一個區間
- 線段樹具有唯一的根節點,代表的區間是整個統計範圍,如[1, N]
- 線段樹的每個葉節點都代表一個長度為1的元區間[x, x]
- 對於每個內部節點[l, r],它的左子節點是[l, mid],右子節點是[mid + 1, r],其中 mid = l + r >> 1(下取整)
圖示
由上面倆圖展示了一顆區間長度為10的線段樹
不難發現我們可以由一個struct陣列來儲存線段樹的每個節點
struct Node
{
int l, r; //左右區間端點
int date; //線段樹要維護的區間性質(一般為最大值,最小值,求和等)
}Tr[4 * N]; //N 為長度為1的區間節點的個數
一般我們Tr[]陣列的長度要求不小於4 * N
原因(瞭解即可):由上面倆幅圖我們不難發現,線段樹的最後一層是不滿的,有多餘位置。而除去最後一層
後的線段樹一定是一顆完全二叉樹, 樹的深度為\(O(log)N\)
一共有N個長度為1的區間節點, N > 倒數第二層的節點個數。
因為最後一層的節點個數為N的一顆滿二叉樹的所有節點個數 = N + N/2 + N/ 4 +.... + 2 + 1 = 2N - 1
所以我們的線段樹去掉最後一層後節點的個數是嚴格小於
2N - 1
的(因為我們的線段樹最後一層有空餘位置)而最後一層最多不會超過上一層的倆倍 即最壞情況下有 <
2*N
個節點所以我們長度一般取
4 * N
即可
基本操作
- pushup 由子節點更新父節點資訊
- pushdown 由子節點更新父節點資訊 lazytag(懶標記)
- build 由區間建立線段樹
- modify 修改某一個點(easy)或者區間(hard)
- query 查詢某一端區間資訊
1.build操作
void build(int u, int l, int r) { tr[u].l = l, tr[u].r = r; //更新當前區間的左右端點 if(l == r) return ; //當前已是葉節點,返回 int mid = l + r >> 1; //取區間中點 build(u << 1, l, mid); //遞迴建立左子樹 build(u << 1 | 1, mid + 1, r); //遞迴建立右子樹 pushup(u); //一般在這裡pushup(即更新區間所要維護的資訊/屬性) }
2.query
int query(int u, int l, int r)
{
if(tr[u].l >= l && tr[u].r <= r) return tr[u].v; //當前查詢區間[l,r]完全覆蓋了u節點所代表的區間,直接返回
int mid = tr[u].l + tr[u].r >> 1;
int v = 0;
if(l <= mid) v = query(u << 1, l, r); //否則,若[l,r]和左子節點有重疊,遞迴訪問左子節點
if(r > mid) v = max(query(u << 1 | 1, l, r), v); //否則,若[l,r]和右子節點有重疊,遞迴訪問左子節點
return v;
}
3. modify 操作和query類似,具體問題具體分析如何修改區間資訊/屬性
4. pushup 直接看模板題更好理解
5. pushdown 直接看模板提更好理解
題目連結 AcWing1275. 最大數
第一個模板題,用線段樹維護區間最大值, 只需用到pushup操作,暫時沒用到pushdown
題目思路
1.我們可以提前把m個數給填好
2.因此我們的第一個操作 在第n個數後面加一個數x == 修改第n + 1個數
3.第二個操作即 查詢[n - l + 1, n]內的最大值
時間複雜度
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 2e5 + 10;
int m, p;
struct Node
{
int l, r;
int v; //[l,r]區間內最大的數
}tr[N * 4];
void pushup(int u) //由子節點資訊更新父節點資訊
{
tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v); //父節點的最大值 = max(左子節點的最大值,右子節點的最大值)
}
void build(int u, int l, int r) //build基本操作
{
tr[u] = {l, r};
if(l == r) return ;
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
}
int query(int u, int l, int r) //query基本操作
{
if(tr[u].l >= l && tr[u].r <= r) return tr[u].v; //當前區間[l, r]已經覆蓋u節點區間,直接返回
int mid = tr[u].l + tr[u].r >> 1;
int v = 0;
if(l <= mid) v = query(u << 1, l, r); //[l, r]和左子節點有重疊,遞迴查詢左子節點
if(r > mid) v = max(query(u << 1 | 1, l, r), v); //[l, r]和右子節點有重疊,遞迴查詢右子節點
return v;
}
void modify(int u, int x, int v)
{
if(tr[u].l == x && tr[u].r == x) tr[u].v = v; //當前節點已是葉節點,直接修改
else
{
int mid = tr[u].l + tr[u].r >> 1;
if(x <= mid) modify(u << 1, x, v);
else modify(u << 1 | 1, x, v);
pushup(u); //不要忘記回溯更新父節點資訊, 因為子節點已經被修改,所以父節點資訊可能會改變
}
}
int main()
{
int n = 0, last = 0;
scanf("%d%d", &m, &p);
build(1, 1, m); //建立一個長度為m的線段樹
int x;
char op[2];
while(m -- )
{
scanf("%s%d", op, &x);
if(op[0] == 'Q')
{
last = query(1, n - x + 1, n);
printf("%d\n", last);
}
else
{
modify(1, n + 1, (last + x) % p);
n ++;
}
}
return 0;
}
2021.8.17 持續更新中 還沒學完