線段樹是一種非常基礎的數據結構,但有的時候僅僅是普通的線段樹無法滿足需求,那麼我們就要對其進行一些擴展。
Chapter1:標記永久化
實現
普通的線段樹通過懶標記(Lazy Tag)以的複雜度實現對序列的區間修改和查詢。但有些時候想要向下標記和向上維護並不是那麼方便,這個時候就需要用到標記永久化了。
標記永久化的思想和懶標記相反:既然我不能方便地下傳標記和合並答案,那麼幹脆就直接更新, 只有當這個區間整個被修改的時候纔打標記。(其實與之前的區間開平方的思想有些類似,只要一整個區間都變爲1了,我就打個標記表示不需要處理)
以區間加、區間求和爲例,如果我當前訪問的線段樹節點所代表的區間包含了我要修改的區間,那麼很明顯修改完後的貢獻是可以直接算出來的,也就是修改的區間長乘上增加的值,那麼我們就可以直接更新當前點的答案。
那麼如果我們樸素地更新,那麼一次修改肯定會變爲的,因爲我們會一直更新到葉子節點。那麼這個時候我們還是需要打上一個標記,只不過這個標記只打在整個區間都被修改的節點上,而且不需要下傳。
舉個栗子,對於一個元素個數爲8的序列,假設我們要給圖中染色的節點加上,那麼我們修改的過程應該是這樣的:
對於根節點,它包含了一整個修改區間(長度爲),那麼我們將它的答案加上,但由於它不是被完整修改的,所以我們不能打標記,接着向下遞歸。
對於節點,他包含了個待修改的元素,那麼它的值就應該加上,節點同理。
然後到了底層葉子結點,首先它的答案也應該加上,然後由於它被完整覆蓋了,所以需要打上一個值爲的標記。
然後來到節點,它包含個待修節點,答案加上,然後來到。
答案加上,但此時我們發現它被完整覆蓋了,於是我們打上的標記,然後不再向下遞歸。
對於,答案加上,然後在節點答案加,打上標記。
於是我們可以給出修改的代碼:
//s爲節點代表的的區間和,tag是節點的標記
void update(int p, int l, int r, int ul, int ur, ll k){
s[p] += (ur-ul+1)*k; //直接統計答案
if(l == ul && r == ur){ //被完全覆蓋,打標記
tag[p] += k;
return;
}
//從上述分析可以看出,與普通線段樹不同,在標記永久化的時候,由於需要判斷節點是否被完全覆蓋
//我們需要同時二分節點代表的區間和詢問的區間,這樣纔可以保證詢問區間包含在當前區間內,纔可以直接統計答案
if(ul > mid) update(rc(p), mid+1, r, ul, ur, k);
else if(ur <= mid) update(lc(p), l, mid, ul, ur, k);
else update(lc(p), l, mid, ul, mid, k), update(rc(p), mid+1, r, mid+1, ur, k);
}
接着考慮如何查詢答案。
其實只要理解了我們在修改時打標記的意義,查詢就變得非常簡單了。由於我們的標記表示的是對整段區間進行的修改,那麼只要這個節點包含查詢區間,那麼它的標記就會對查詢結果產生影響。於是我們只要在查詢的時候累加經過的節點上的標記,當整個節點都是查詢區間的時候,我們就返回這個節點自身的答案加上累加的標記對區間的影響。
那麼查詢的代碼也就十分簡單:
ll query(int p, int l, int r, int ul, int ur, ll sum){ //sum是路徑上累加的標記和
if(l == ul && r == ur) return s[p]+sum*(r-l+1);
//和修改一樣,也要二分查詢區間
if(ul > mid) return query(rc(p), mid+1, r, ul, ur, sum+tag[p]);
else if(ur <= mid) return query(lc(p), l, mid, ul, ur, sum+tag[p]);
else return query(lc(p), l, mid, ul, mid, sum+tag[p])+query(rc(p), mid+1, r, mid+1, ur, sum+tag[p]);
}
完整代碼可以參考我的提交記錄:線段樹1
小結
標記永久化相對標記下傳沒那麼好理解,並且侷限性較強,比如不能像傳統線段樹那樣維護如區間最大子段和這種相對複雜、不能直接統計答案的信息。但是在一些特定的場合,標記下傳會顯得非常不方便,那麼就需要標記永久化。
Chapter2:二維線段樹(樹套樹 Tree Tao Tree)
題意:
維護一個矩陣中的信息:支持修改子矩陣,查詢子矩陣和(或最大/最小值)。
實現
現在一維序列上的操作被扔到了二維平面上,那麼一個最直接的想法就是通過一些方法強行轉換成一維操作(比如在樹上可以利用序)。
我們可以把兩維分開考慮,如果我們把每一列看成一個點,那麼我們就可以把整個矩陣拍扁,看成一個序列,就可以進行常規的線段樹操作了。
那麼每一列內的信息怎麼維護呢?顯然對每一列開一個內層線段樹就完了。
所以我們使用樹套樹,外層線段樹維護行,內層線段樹維護列。這時候我們會發現外層線段樹區間修改的時候標記沒法下傳,那就要用到上文介紹的標記永久化了。
代碼
只要對標記永久化比較熟練,代碼總體就非常好理解。
#include <cstdio>
#include <iostream>
#define MAX 2050
#define lc(x) (x<<1)
#define rc(x) (x<<1|1)
using namespace std;
template<typename T>
inline void read(T &n){
n = 0;
T f = 1;
char c = getchar();
while(!isdigit(c) && c != '-') c = getchar();
if(c == '-') f = -1, c = getchar();
while(isdigit(c)) n = n*10+c-'0', c = getchar();
n *= f;
}
template<typename T>
inline void write(T n){
if(n < 0) putchar('-'), n = -n;
if(n > 9) write(n/10);
putchar(n%10+'0');
}
int n, m, q;
inline int max(int x, int y){
return x>y?x:y;
}
struct segy{
int mx[MAX], tag[MAX];
void update(int p, int l, int r, int ul, int ur, int k){
mx[p] = max(mx[p], k);
if(l == ul && r == ur){
tag[p] = max(tag[p], k);
return;
}
int mid = (l+r)>>1;
if(ur <= mid) update(lc(p), l, mid, ul, ur, k);
else if(ul > mid) update(rc(p), mid+1, r, ul, ur, k);
else update(lc(p), l, mid, ul, mid, k), update(rc(p), mid+1, r, mid+1, ur, k);
}
int query(int p, int l, int r, int ul, int ur){
if(l == ul && r == ur) return mx[p];
int res = tag[p], mid = (l+r)>>1;
if(ur <= mid) res = max(res, query(lc(p), l, mid, ul, ur));
else if(ul > mid) res = max(res, query(rc(p), mid+1, r, ul, ur));
else res = max(res, max(query(lc(p), l, mid, ul, mid), query(rc(p), mid+1, r, mid+1, ur)));
return res;
}
};
struct segx{
segy mx[MAX], tag[MAX];
void update(int p, int l, int r, int ul, int ur, int yl, int yr, int k){
mx[p].update(1, 1, m, yl, yr, k);
if(l == ul && r == ur){
tag[p].update(1, 1, m, yl, yr, k);
return;
}
int mid = (l+r)>>1;
if(ur <= mid) update(lc(p), l, mid, ul, ur, yl, yr, k);
else if(ul > mid) update(rc(p), mid+1, r, ul, ur, yl, yr, k);
else update(lc(p), l, mid, ul, mid, yl, yr, k), update(rc(p), mid+1, r, mid+1, ur, yl, yr, k);
}
int query(int p, int l, int r, int ul, int ur, int yl, int yr){
if(l == ul && r == ur) return mx[p].query(1, 1, m, yl, yr);
int res = tag[p].query(1, 1, m, yl, yr), mid = (l+r)>>1;
if(ur <= mid) res = max(res, query(lc(p), l, mid, ul, ur, yl, yr));
else if(ul > mid) res = max(res, query(rc(p), mid+1, r, ul, ur, yl, yr));
else{
res = max(res, query(lc(p), l, mid, ul, mid, yl, yr));
res = max(res, query(rc(p), mid+1, r, mid+1, ur, yl, yr));
}
return res;
}
}a;
int main()
{
read(n), read(m), read(q);
int x, y, d, s, h;
while(q--){
read(d), read(s), read(h), read(x), read(y);
x++, y++;
int mx = a.query(1, 1, n, x, x+d-1, y, y+s-1);
a.update(1, 1, n, x, x+d-1, y, y+s-1, mx+h);
}
write(a.query(1, 1, n, 1, n, 1, m));
return 0;
}
Chapter3:線段樹合併
在某些情況下,我們的權值線段樹需要合併(一般區間樹是不進行合併的)。那麼最簡單的方法就是啓發式合併,複雜度,但由於線段樹的一些優美的性質,我們可以把線段樹的合併在複雜度內完成。
實現
先上圖感受一下:
[外鏈圖片轉存失敗(img-f0Zzl7FQ-1563235585047)(https://s2.ax1x.com/2019/07/16/Z73hhq.gif)]
其實線段樹合併非常簡單,只要在普通的線段樹上二分的時候進行一些判斷就可以了。
具體操作(可以結合上圖感性理解):
- 如果當前節點和另一棵樹上對應位置的節點都有左兒子,那麼遞歸到左子樹合併。
- 如果當前節點和對應位置節點只有一個有左兒子,那麼直接把唯一的左兒子作爲合併後這個位置節點的左兒子。(直接拉過來接上去)
- 如果都沒有左兒子,就不進行合併。
- 右兒子同理。
是不是非常簡單啊!!
代碼
線段樹合併有兩種實現方式,一種是動態開點,優點是可以不影響原線段樹的形態,但是空間複雜度較高;還有一種是直接把另一顆線段樹合併到當前線段樹上,這樣會破壞原線段樹的結構,但是空間複雜度較低(適合詢問離線)。
下面的實現節選自P4556 雨天的尾巴 的代碼,本題可以離線詢問,所以使用第二種方式。
void merge(int x, int y, int l, int r){ //線段樹合併
if(l == r){
s[x] += s[y];
return;
}
if(lc[x] && lc[y]) merge(lc[x], lc[y], l, mid); //如果都有左孩子,遞歸合併
else if(lc[y]) lc[x] = lc[y]; //否則直接接上去
if(rc[x] && rc[y]) merge(rc[x], rc[y], mid+1, r); //右兒子同理
else if(rc[y]) rc[x] = rc[y];
push_up(x);
}
完結撒花
線段樹雖然很基礎,但是還是有很多巧妙的擴展和應用,還有貓樹、zkw各種變種~~(挖坑警告!)~~。