「線段樹」酷酷的資料結構
現在的高中生人手一棵線段樹 BY NPSC裁判
一棵最簡單的線段樹支援以下幾種操作(對陣列)
示意圖
觀察一下這張圖,一個節點需要記錄什麼資訊?
Why 左閉右開?
有人會覺得左閉右閉比較好,但根本邪教
所以開一個struct用來記錄每個節點:
struct seg {
int l, r, mid, val;
seg* ch[2] = {}; //two children
};
How to construct a segment tree?
code:
struct seg{
int l, r, mid, val;
seg *ch[2] = {};
seg(int _l, int _r):l(_l),r(_r){
mid = (l+r)/2;
if(l == r-1){
return;
}
ch[0] = new seg(l, mid); //left child
ch[1] = new seg(mid, r); //right child
}
};
蓋好線段樹了,如何單點改值?
單點改值
操作 : 將
pull
當一個節點的的下方有更改過,就要將資訊拉(更新)上來,就是pull
code:
void pull(){
val = ch[0]->val + ch[1]->val;
}
void add(int idx, int k){
if(l == r-1){
val += k;
return;
}
if(idx < mid){
ch[0]->add(idx, k);
}
else{
ch[1]->add(idx, k);
}
pull();
}
特別注意指標呼叫成員用->
區間求值
操作:給一區間
code:
int query(int a, int b){
if(a <= l && r <= b){
return val;
}
int ans = 0;
if(a < mid){
ans += ch[0]->query(a, b);
}
if(b > mid){
ans += ch[1]->query(a, b);
}
return ans;
}
大功告成!!
Static Range Minimum Queries
Dynamic Range Sum Queries
Dynamic Range Minimum Queries
Range Xor Queries
Tip
善用pull與參照,直接在建構線段樹時設好初始值,不用n次單點改值
code:
struct seg{
int l, r, mid, val;
seg *ch[2] = {};
seg(int _l, int _r, vector<int> &arr):l(_l),r(_r){
mid = (l+r)/2;
if(l == r-1){
val = arr[l];
return;
}
ch[0] = new seg(l, mid, arr);
ch[1] = new seg(mid, r, arr);
pull();
}
void pull(){
val = ch[0]->val + ch[1]->val;
}
};
懶人標記,俗稱懶標,用來記錄當前區間已經做過的操作(子節點尚未更新),等到需要往下修改或查詢的時候再將資訊推下去。
區間加值
操作:將區間
跟query(區間查詢)作法很像,當
這時候query(區間和)往下的時候就要記得
Push ??
push 怎麼做?
code:
struct seg{
int l, r, mid, val, lz = 0;
seg *ch[2] = {};
seg(int _l, int _r, vector<int> &arr):l(_l),r(_r){
mid = (l+r)/2;
if(l == r-1){
val = arr[l];
return;
}
ch[0] = new seg(l, mid, arr);
ch[1] = new seg(mid, r, arr);
pull();
}
void pull(){
val = ch[0]->val + ch[1]->val;
}
void push(){
if(!lz) return;
ch[0]->lz += lz;
ch[1]->lz += lz;
ch[0]->val += lz*(mid - l);
ch[1]->val += lz*(r - mid);
lz = 0;
}
void add(int a, int b, int k){
if(a <= l && r <= b){
val += k*(r-l);
lz += k;
return;
}
if(a < mid){
ch[0]->add(a, b, k);
}
if(b > mid){
ch[1]->add(a, b, k);
}
pull();
}
int query(int a, int b){
if(a <= l && r <= b){
return val;
}
push();
int ans = 0;
if(a < mid){
ans += ch[0]->query(a, b);
}
if(b > mid){
ans += ch[1]->query(a, b);
}
return ans;
}
};
以上是懶標的基礎運用,懶標的概念就是紀錄做了什麼事,等到要查詢再
Range Update Queries
Subarray Sum Queries
寫完下面這題代表你會用懶標了
Range Updates and Sums
持久化線段樹,神奇的資料結構,可以在修改後還能查詢先前的版本。
示意圖
藍色為第一版本,其他為後續。
如何開一顆新節點? 需有人對照(舊版本)
code:
seg(seg *root){
l = root->l;
r = root->r;
mid = (l+r)/2;
if(l == r-1) return;
ch[0] = root->ch[0];
ch[1] = root->ch[1];
pull();
}
void pull(){
val = ch[0]->val + ch[1]->val;
}
void replace(int idx, int k){
if(l == r-1){
val = k;
return;
}
if(idx < mid){
ch[0] = new seg(ch[0]);
ch[0]->replace(idx, k);
}
else{
ch[1] = new seg(ch[1]);
ch[1]->replace(idx, k);
}
pull();
}
constructor,query 一樣,故省略(此為無懶標版本)
補充:如果不想打1~9行的constructor可以這樣寫:
ch[0] = new seg(*ch[0]);
new 代表會新開一個空間,而新的seg的成員變數都會跟原本一樣
給一陣列
想法:
如果每次詢問都滿足
接下來只要開一棵持久化線段樹,第
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define all(x) x.begin(),x.end()
struct seg{
int l, r, mid, val;
seg *lc, *rc;
seg(){};
seg(int _l, int _r):l(_l),r(_r),mid((l+r)>>1){
if(l == r-1){
val = 0;
return;
}
lc = new seg(l, mid);
rc = new seg(mid, r);
pull();
}
void pull(){
val = lc->val + rc->val;
}
void add(int idx, int k){
if(l == r-1){
val += k;
return;
}
if(idx < mid){
lc = new seg(*lc);
lc->add(idx, k);
}
else{
rc = new seg(*rc);
rc->add(idx, k);
}
pull();
}
};
int query(seg *a, seg *b, int k){ //find (k+1)-th
if(a->l == a->r-1) return a->l;
if(b->lc->val-a->lc->val > k) return query(a->lc, b->lc, k);
else return query(a->rc, b->rc, k-(b->lc->val-a->lc->val));
}
signed main(){
int n, q;
cin >> n >> q;
vector<int> arr(n), lisan(n);
for(int i = 0; i < n; i++) cin >> arr[i];
lisan = arr;
sort(all(lisan));
lisan.resize(unique(all(lisan))-lisan.begin());
vector<seg*> sg(n+1);
sg[0] = new seg(0, lisan.size());
for(int i = 0; i < n; i++){
sg[i+1] = new seg(*sg[i]);
sg[i+1]->add(lower_bound(all(lisan), arr[i])-lisan.begin(), 1);
}
for(int i = 0,a,b,k; i < q; i++){
cin >> a >> b >> k;
cout << lisan[query(sg[a], sg[b], k)] << endl;
}
}
李超線段樹,一棵神奇的線段樹,維護一個集合(存線),給一個
目標:
假設一節點
原節點維護之線不變,將
原節點維護之線不變,將
原節點維護之線改為
原節點維護之線改為
總結
code:
struct line{
int a, b;
line(int _a, int _b):a(_a),b(_b){}
int f(int x){
return a*x + b;
}
};
struct seg{
int l, r, mid;
line val = line(0, 1e18);
seg *ch[2] = {};
seg(int _l, int _r):l(_l),r(_r){
mid = (l+r)/2;
if(l == r-1) return;
ch[0] = new seg(l, mid);
ch[1] = new seg(mid, r);
}
void insert(line k){
if(k.f(mid) < val.f(mid)) swap(k, val);
if(l == r-1) return;
if(k.a > val.a){
ch[0]->insert(k);
}
else{
ch[1]->insert(k);
}
}
int query(int x){
if(l == r-1){
return val.f(x);
}
if(x < mid){
return min(val.f(x), ch[0]->query(x));
}
else{
return min(val.f(x), ch[1]->query(x));
}
}
};
看完題目後,可以發現當我們考慮調整第
先將
如果想要讓
總獲利為
但枚舉
接下來只要由給定的
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define all(x) x.begin(),x.end()
struct line{
int a, b;
line(int _a, int _b):a(_a),b(_b){}
int f(int x) {
return a*x+b;
}
};
struct seg{
int l, r, mid;
line val = line(0, 0);
seg *lc = NULL, *rc = NULL;
seg(int _l, int _r):l(_l),r(_r),mid((l+r)>>1){}
void newNode(){
if(lc != nullptr || l == r-1)return;
lc = new seg(l, mid);
rc = new seg(mid, r);
}
void insert(line k){
if(k.f(mid) > val.f(mid)) swap(k, val);
if(l == r-1)return;
newNode();
if(k.a > val.a) rc->insert(k);
else lc->insert(k);
}
int query(int idx){
if(l == r-1 || lc == nullptr) return val.f(idx);
int ans = val.f(idx);
if(idx < mid) ans = max(ans, lc->query(idx));
else ans = max(ans, rc->query(idx));
return ans;
}
};
signed main(){
int n, m;
cin >> n >> m;
vector<int> b(n), c(m);
seg sg(0, 1e9);
for(int i = 0; i < n; i++) cin >> b[i];
for(int i = 0; i < m; i++) cin >> c[i];
sort(all(b));
reverse(all(b));
for(int i = 0; i < n; i++) sg.insert(line(i+1,(i+1)*b[i]));
for(int i = 0; i < m; i++) cout << sg.query(c[i]) << " ";
cout << endl;
}
用李超線段樹把DP過程優化吧
考慮
砸李超線段樹就過了
Segment Add Get Min
插入直線變成插入線段怎麼辦? 其實原本插入的就是在該區間範圍的直線,所以只需要將要插的直線範圍拆成
void insert(int a, int b,line k){
if(a <= l && r <= b) return insert_line(k);
if(a < mid) lc->insert(a, b, k);
if(b > mid) rc->insert(a, b, k);
}
void insert_line(line k){
if(k.f(mid) < ln.f(mid)) swap(k, ln);
if(l == r-1){
val = min(ln.f(l),ln.f(r-1));
return;
}
if(k.a > ln.a) lc->insert_line(k);
else rc->insert_line(k);
}
當然也是可以把兩個函式合併。
李超線段樹適用於具有優超性的函數 只要函數
在 2021 年 1 月,有人在 Codeforces 上發表了擴充版的李超線段樹。
可以處理以下兩種問題
問題一
有一個大小為
問題二
有一個大小為
一般李超線段樹是從可能成為答案的直線取
而有了區間加線後,插入的線是在加線前加入還是在加線後插入就差很多了。
以原版的李超來舉例:
考慮這種情況,答案其實很明顯:
加線段的時候在經過
這個情況下,加入的線段就不能直接推到葉子,要用懶標存在節點上,才可以保證複雜度依然是
問題二其實也差不多
但區間加直線會造成區間最大值的位置改變,若最大值發生在底下的直線,會造成懶人標記的不可預測性,因此作者才在第二種維護中將該操作降級成了單純的區間加值。
code 1
struct line{
int a = 0, b = 0;
line(){}
line(int _a, int _b):a(_a),b(_b){}
int f(int x){
return b+a*x;
}
bool operator ==(const line &next)const{
return (a == next.a && b == next.b);
}
void operator +=(const line &next){
a += next.a;
b += next.b;
}
};
struct seg{
int l, r, mid;
line ln = line(0, 0), lazy = line(0, 0);
seg *lc = NULL, *rc = NULL;
seg(int _l, int _r):l(_l),r(_r),mid((l+r)>>1){}
void newNode(){
if(l == r-1)return;
lc = new seg(l, mid);
rc = new seg(mid, r);
}
void push_lazy(){
if(lazy == line(0, 0))return;
lc->lazy += lazy;
rc->lazy += lazy;
lc->ln += lazy;
rc->ln += lazy;
lazy = line(0, 0);
}
void push_line(){
lc->insert_line(ln);
rc->insert_line(ln);
ln = line(0, 0);
}
void insert_line(line k){
if(k.f(mid) < ln.f(mid)) swap(k, ln);
if(l == r-1)return;
if(lc == nullptr) newNode();
push_lazy();
if(k.a > ln.a) lc->insert_line(k);
else rc->insert_line(k);
}
void insert(int a, int b,line k){
if(a <= l && r <= b) return insert_line(k);
if(lc == nullptr) newNode();
push_lazy();
if(a < mid) lc->insert(a, b, k);
if(b > mid) rc->insert(a, b, k);
}
void add(int a, int b, line k){
if(a <= l && r <= b){
lazy += k;
ln += k;
return;
}
if(lc == nullptr) newNode();
push_lazy();
push_line();
if(a < mid) lc->add(a, b, k);
if(b > mid) rc->add(a, b, k);
}
int query(int idx){
if(l == r-1 || lc == nullptr) return ln.f(idx);
push_lazy();
int ans = ln.f(idx);
if(idx < mid) ans = max(ans, lc->query(idx));
else ans = max(ans, rc->query(idx));
return ans;
}
};
code 2
struct seg{
int l, r, mid;
int val = 0,lazy = 0;
seg *lc = NULL, *rc = NULL;
line ln = line(0, 0);
seg(int _l, int _r):l(_l),r(_r),mid((l+r)>>1){}
void newNode(){
if(l == r-1)return;
lc = new seg(l, mid);
rc = new seg(mid, r);
}
void push_lazy(){
if(lazy == 0)return;
lc->ln.b += lazy;
rc->ln.b += lazy;
lc->lazy += lazy;
rc->lazy += lazy;
lc->val += lazy;
rc->val += lazy;
lazy = 0;
}
void push_line(){
lc->insert_line(ln);
rc->insert_line(ln);
ln = line(0, INF);
}
void pull(){
val = max(max(lc->val,rc->val),max(ln.f(l),ln.f(r-1)));
}
void insert_line(line k){
if(k.f(mid) < ln.f(mid)) swap(k, ln);
if(l == r-1){
val = max(ln.f(l),ln.f(r-1));
return;
}
if(lc == nullptr) newNode();
push_lazy();
if(k.a > ln.a) lc->insert_line(k);
else rc->insert_line(k);
pull();
}
void insert(int a, int b,line k){
if(a <= l && r <= b) return insert_line(k);
if(lc == nullptr) newNode();
push_lazy();
if(a < mid) lc->insert(a, b, k);
if(b > mid) rc->insert(a, b, k);
pull();
}
void add(int a, int b, int k){
if(a <= l && r <= b){
lazy += k;
ln.b += k;
val += k;
return;
}
if(lc == nullptr) newNode();
push_lazy();
push_line();
if(a < mid) lc->add(a, b, k);
if(b > mid) rc->add(a, b, k);
pull();
}
int query(int a, int b){
if(a <= l && r <= b) return val;
if(lc == nullptr) return max(ln.f(a), ln.f(b-1));
push_lazy();
int ans = max(ln.f(a), ln.f(b-1));
if(a < mid) ans = max(ans, lc->query(a, b));
if(b > mid) ans = max(ans, rc->query(a, b));
return ans;
}
};
因為RMQ要預處理區間
I hate Shortest Path Problem
參考資料:
[Tutorial] Li Chao Tree Extended
ioic2023 講義