【數據結構】線段樹的擴展與應用

線段樹是一種非常基礎的數據結構,但有的時候僅僅是普通的線段樹無法滿足需求,那麼我們就要對其進行一些擴展。

Chapter1:標記永久化

實現

普通的線段樹通過懶標記(Lazy Tag)以O(nlogn)O(nlogn)的複雜度實現對序列的區間修改和查詢。但有些時候想要向下push_downpush\_down標記和向上push_uppush\_up維護並不是那麼方便,這個時候就需要用到標記永久化了。

標記永久化的思想和懶標記相反:既然我不能方便地下傳標記和合並答案,那麼幹脆就直接更新, 只有當這個區間整個被修改的時候纔打標記。(其實與之前的區間開平方的思想有些類似,只要一整個區間都變爲1了,我就打個標記表示不需要處理)

以區間加、區間求和爲例,如果我當前訪問的線段樹節點所代表的區間包含了我要修改的區間,那麼很明顯修改完後的貢獻是可以直接算出來的,也就是修改的區間長乘上增加的值,那麼我們就可以直接更新當前點的答案。

那麼如果我們樸素地更新,那麼一次修改肯定會變爲nlognnlogn的,因爲我們會一直更新到葉子節點。那麼這個時候我們還是需要打上一個標記,只不過這個標記只打在整個區間都被修改的節點上,而且不需要下傳。

Z62K9P.png

舉個栗子,對於一個元素個數爲8的序列,假設我們要給圖中染色的節點加上kk,那麼我們修改的過程應該是這樣的:

對於根節點AA,它包含了一整個修改區間(長度爲44),那麼我們將它的答案加上4k4k,但由於它不是被完整修改的,所以我們不能打標記,接着向下遞歸。

對於BB節點,他包含了11個待修改的元素,那麼它的值就應該加上kkEE節點同理。

然後到了底層葉子結點44,首先它的答案也應該加上44,然後由於它被完整覆蓋了,所以需要打上一個值爲kk的標記。

然後來到CC節點,它包含33個待修節點,答案加上3k3k,然後來到FF

FF答案加上2k2k,但此時我們發現它被完整覆蓋了,於是我們打上2k2k的標記,然後不再向下遞歸

對於GG,答案加上kk,然後在77節點答案加kk,打上標記。

於是我們可以給出修改的代碼:

//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)

題意:

維護一個矩陣中的信息:支持修改子矩陣,查詢子矩陣和(或最大/最小值)。

實現

現在一維序列上的操作被扔到了二維平面上,那麼一個最直接的想法就是通過一些方法強行轉換成一維操作(比如在樹上可以利用dfsdfs序)。

我們可以把兩維分開考慮,如果我們把每一列看成一個點,那麼我們就可以把整個矩陣拍扁,看成一個序列,就可以進行常規的線段樹操作了。

那麼每一列內的信息怎麼維護呢?顯然對每一列開一個內層線段樹就完了。

所以我們使用樹套樹,外層線段樹維護行,內層線段樹維護列。這時候我們會發現外層線段樹區間修改的時候標記沒法下傳,那就要用到上文介紹的標記永久化了。

代碼

P3437 TET-Tetris 3D

只要對標記永久化比較熟練,代碼總體就非常好理解。

#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:線段樹合併

在某些情況下,我們的權值線段樹需要合併(一般區間樹是不進行合併的)。那麼最簡單的方法就是啓發式合併,複雜度O(nlog2n)O(nlog^2n),但由於線段樹的一些優美的性質,我們可以把線段樹的合併在O(nlogn)O(nlogn)複雜度內完成。

實現

先上圖感受一下:

[外鏈圖片轉存失敗(img-f0Zzl7FQ-1563235585047)(https://s2.ax1x.com/2019/07/16/Z73hhq.gif)]

其實線段樹合併非常簡單,只要在普通的線段樹上二分的時候進行一些判斷就可以了。

具體操作(可以結合上圖感性理解):

  1. 如果當前節點和另一棵樹上對應位置的節點都有左兒子,那麼遞歸到左子樹合併。
  2. 如果當前節點和對應位置節點只有一個有左兒子,那麼直接把唯一的左兒子作爲合併後這個位置節點的左兒子。(直接拉過來接上去)
  3. 如果都沒有左兒子,就不進行合併。
  4. 右兒子同理。

是不是非常簡單啊!!

代碼

線段樹合併有兩種實現方式,一種是動態開點,優點是可以不影響原線段樹的形態,但是空間複雜度較高;還有一種是直接把另一顆線段樹合併到當前線段樹上,這樣會破壞原線段樹的結構,但是空間複雜度較低(適合詢問離線)。

下面的實現節選自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各種變種~~(挖坑警告!)~~。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章