Segment Trees
線段樹DLC
INDEX
遞迴式
懶標
動態開點
簡報看不懂我也沒辦法
Segment Tree (Recursive)
1. 複習線段樹在幹嘛
2. 遞迴式寫法
Segment Tree
支援各種區間查詢的資結
每個節點都代表一個 "線段",這些節點會組成一顆二元 "樹"
父節點的值都是由兩個小孩推出來的
遞迴式線段樹
遞迴寫法比較好理解(?
一顆線段樹需要:
1. build()
建構
2. modify()
單點改值
3. query()
區間查詢
遞迴式線段樹
1. build()
void build(int id, int l, int r, vector<int> &v) {
if (l == r) { // 葉節點直接賦值
a[id] = v[l];
return;
}
int m = (l + r) / 2;
build(id * 2, l, m, v);
// 蓋左子樹
build(id * 2 + 1, m + 1, r, v);
// 蓋右子樹
a[id] = min(a[id * 2], a[id * 2 + 1]);
// 用左子節點跟右子節點回推當前節點
}
遞迴式線段樹
2. modify()
void single_modify(int id, int l, int r, int pos, int val) {
if (l == r) { // 葉節點直接改值
a[id] = val;
return;
}
int m = (l + r) / 2;
if (pos <= m) { // 分治概念,要改的在左邊就改左子樹
single_modify(id * 2, l, m, pos, val);
} else { // 改右子樹
single_modify(id * 2 + 1, m + 1, r, pos, val);
}
// 用左右子節點回推當前節點
a[id] = min(a[id * 2], a[id * 2 + 1]);
}
遞迴式線段樹
3. query()
int range_query(int id, int l, int r, int L, int R) {
if (L <= l && r <= R) { // 查詢範圍完全包含當前處理範圍
return a[id]; // 直接回傳答案
}
int m = (l + r) / 2, res = INFF;
if (L <= m) {
// 查詢範圍包含左子樹,考慮左子樹
res = min(res, range_query(id * 2, l, m, L, R));
}
if (R > m) {
// 查詢範圍包含右子樹,考慮右子樹
res = min(res, range_query(id * 2 + 1, m + 1, r, L, R));
}
return res;
}
遞迴式線段樹
4. struct 包起來
struct SegTree {
int a[MAXN * 4 + 10]; // 可以證明 worst case 下需要 4n 才可以存
void build(int id, int l, int r, vector<int> &v) {
if (l == r) {
a[id] = v[l];
return;
}
int m = (l + r) / 2;
build(id * 2, l, m, v);
build(id * 2 + 1, m + 1, r, v);
a[id] = min(a[id * 2], a[id * 2 + 1]);
}
void single_modify(int id, int l, int r, int pos, int val) {
if (l == r) {
a[id] = val;
return;
}
int m = (l + r) / 2;
if (pos <= m) {
single_modify(id * 2, l, m, pos, val);
} else {
single_modify(id * 2 + 1, m + 1, r, pos, val);
}
a[id] = min(a[id * 2], a[id * 2 + 1]);
}
int range_query(int id, int l, int r, int L, int R) {
if (L <= l && r <= R) {
return a[id];
}
int m = (l + r) / 2, res = INFF;
if (L <= m) {
res = min(res, range_query(id * 2, l, m, L, R));
}
if (R > m) {
res = min(res, range_query(id * 2 + 1, m + 1, r, L, R));
}
return res;
}
};
題目
Lazy Propagation
區間改值
懶標線段樹
Range Modify
區間改值
思考一下,如果我們要對一個區間修改
修改一個點要
$$ O(log n) $$
Worst case?
$$ O(n log n) $$
原地 💥
Range Modify
區間改值
怎麼辦 ?
只好多紀錄一些東西
懶人標記 ✅
Range Modify
區間改值
觀察一下修改 [1, 5] 區間要動到哪些節點
Range Modify
區間改值
觀察一下修改 [1, 5] 區間要動到哪些節點
Range Modify
區間改值
觀察一下修改 [1, 5] 區間要動到哪些節點
這顆子樹
整個都要改欸
Range Modify
區間改值
在節點 2 記錄一個 tag,表達底下這顆子樹都要修改
等我需要子節點資訊時再把 tag 往下推就好了
這顆子樹
整個都要改欸
$$ O(log n) $$
這樣時間複雜度會降回
懶標線段樹
懶標下推 push()
void push(int id, int l, int r){
int m=(l+r)/2;
st[lc(id)]+=lz[id]*(m-l+1);
st[rc(id)]+=lz[id]*(r-(m+1)+1);
lz[lc(id)]+=lz[id];
lz[rc(id)]+=lz[id];
lz[id]=0;
}
懶標線段樹
用兩個子節點更新當節點
pull()
void pull(int id){
st[id]=st[lc(id)]+st[rc(id)];
}
懶標線段樹
區間改值
modify()
void modify(int l, int r, int ql, int qr, int w, int id){
if(r<ql||l>qr) return;
if(ql<=l&&r<=qr) {
st[id]+=w*(r-l+1);
lz[id]+=w;
return;
}
push(id, l, r);
int m=(l+r)/2;
modify(l, m, ql, qr, w, lc(id));
modify(m+1, r, ql, qr, w, rc(id));
pull(id);
}
懶標線段樹
區間查詢
query()
int query(int l, int r, int ql, int qr, int id){
if(r<ql||l>qr) return 0;
if(ql<=l&&r<=qr){
return st[id];
}
push(id, l, r);
int m=(l+r)/2;
return query(l, m, ql, qr, lc(id))+query(m+1, r, ql, qr, rc(id));
}
懶標線段樹
Code (這是查區間和的)
學長對不起我下次一定會先跑過 code
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define pii pair<int,int>
#define pb push_back
#define all(x) x.begin(),x.end()
#define ff first
#define ss second
#define lc(x) x*2
#define rc(x) x*2+1
struct seg{
int n;
vector<int> st;
vector<int> lz;
seg(int N){
n=N;
st.resize(4*n, 0);
lz.resize(4*n, 0);
}
void pull(int id){
st[id]=st[lc(id)]+st[rc(id)];
}
void push(int id, int l, int r){
int m=(l+r)/2;
st[lc(id)]+=lz[id]*(m-l+1);
st[rc(id)]+=lz[id]*(r-(m+1)+1);
lz[lc(id)]+=lz[id];
lz[rc(id)]+=lz[id];
lz[id]=0;
}
void modify(int l, int r, int ql, int qr, int w, int id){
if(r<ql||l>qr) return;
if(ql<=l&&r<=qr) {
st[id]+=w*(r-l+1);
lz[id]+=w;
return;
}
push(id, l, r);
int m=(l+r)/2;
modify(l, m, ql, qr, w, lc(id));
modify(m+1, r, ql, qr, w, rc(id));
pull(id);
}
int query(int l, int r, int ql, int qr, int id){
if(r<ql||l>qr) return 0;
if(ql<=l&&r<=qr){
return st[id];
}
push(id, l, r);
int m=(l+r)/2;
return query(l, m, ql, qr, lc(id))+query(m+1, r, ql, qr, rc(id));
}
};
signed main(){
ios_base::sync_with_stdio(false);cin.tie(0);
int t;cin>>t;
while(t--){
int n, q;cin>>n>>q;
auto segtree=seg(n);
while(q--){
int op;cin>>op;
if(op==0) {
int p, q, v;cin>>p>>q>>v;
segtree.modify(1, n, p, q, v, 1);
}else{
int p, q;cin>>p>>q;
cout<<segtree.query(1, n, p, q, 1)<<'\n';
}
}
}
}
題目
好大一顆線段樹
Code
動態開點線段樹
思考時間
設想一下
一如往常的在寫題目
你發現了一題單點改值區間查詢的裸題
定睛一看
$$ n \leq 10^9 $$
啊?
思考時間
$$ n \leq 10^9 $$
原地 💥
思考時間
所以我們可以
原地 💥
思考時間
所以我們可以
需要用到該節點的時候再開
aka 動態開點
好大一顆線段樹
不過我們不能用迭代式/遞迴式寫
要用指標型線段樹做
(因為迭代式跟遞迴式要先開好記憶體)
#include <iostream>
using namespace std;
typedef long long ll;
// 節點結構:僅存儲該區間的總和與子節點指針
struct Node {
ll sum;
Node *left, *right;
Node() : sum(0), left(nullptr), right(nullptr) {}
};
class DynamicSegTree {
private:
Node* root;
ll L, R; // 整個線段樹的範圍 [L, R]
public:
DynamicSegTree(ll L, ll R) : L(L), R(R) {
root = new Node();
}
// 單點更新:將位置 pos 更新為 val
void update(ll pos, ll val) {
update(root, L, R, pos, val);
}
// 區間查詢:查詢區間 [l, r] 的累積和
ll query(ll l, ll r) {
return query(root, L, R, l, r);
}
private:
// 遞迴更新:將位置 pos 的值設為 val
void update(Node* node, ll nl, ll nr, ll pos, ll val) {
if (nl == nr) { // 到達葉節點
node->sum = val;
return;
}
ll mid = (nl + nr) / 2;
if (pos <= mid) {
if (!node->left) node->left = new Node();
update(node->left, nl, mid, pos, val);
} else {
if (!node->right) node->right = new Node();
update(node->right, mid + 1, nr, pos, val);
}
node->sum = (node->left ? node->left->sum : 0) + (node->right ? node->right->sum : 0);
}
// 遞迴查詢:查詢區間 [ql, qr] 的累積和
ll query(Node* node, ll nl, ll nr, ll ql, ll qr) {
if (!node || ql > nr || qr < nl) return 0; // 超出範圍
if (ql <= nl && nr <= qr) return node->sum; // 完全覆蓋
ll mid = (nl + nr) / 2;
return query(node->left, nl, mid, ql, qr) + query(node->right, mid + 1, nr, ql, qr);
}
};
int main() {
DynamicSegTree seg(1, 1000000000);
seg.update(5, 10);
seg.update(10, 20);
seg.update(7, 15);
cout << "Sum[1,10]: " << seg.query(1, 10) << endl; // 10 + 15 + 20 = 45
cout << "Sum[6,8]: " << seg.query(6, 8) << endl; // 15 (at 7)
cout << "Sum[10,10]: " << seg.query(10, 10) << endl; // 20
return 0;
}
動態開點線段樹 by cjtsai 的 GPT Plus
不然網路上找的扣都醜的一批
題目 & Live Coding
掰掰
More Segment Tree
By wen Ian
More Segment Tree
- 229