線段樹上二分搜與經典題

如果你認為他只能做到單點修改,區間詢問,那麼就太少了!

在前一個章節,我們學會了單點修改、區間詢問的線段樹

單點修改 區間詢問

線段樹上二分搜

而現在呢?如果我們有一道問題如下

在一個陣列中尋找第一個出現的大於 xx 的位置,同時也會有修改元素的操作

這樣的問題,線段樹也辦得到!如果要尋找第一個數字,該怎麼做呢?

我們可以試著用一棵區間詢問最大值的線段樹來幫我們做到這一點

一個維護區間最大值的線段樹

當我們建立完一棵區間最大值線段樹之後,我們可以在樹上去尋找我們要的位置,如下圖

線段樹上二分搜(圖為搜尋4的情況)

這樣的動作一般稱為「線段樹上二分搜」,如同圖上,我們先從最上面那個點開始找

大致操作如下

int find(int x, int idx, int l, int r){
    if(l==r) return l; //找到了就回傳
    int m = (l+r)/2;
    
    //往兩邊找,如果左邊的最大值>=x,往左繼續找
    //否則往右找
    if(tr[idx*2] >= x) return find(x, idx*2, l, m); 
    else return find(x, idx*2+1, m+1, r);
}

學會怎麼做之後,當然就是要來做練習題囉

點我前往題目

B: 詢問為 0/10/1 陣列中第 kk11

C: 詢問第一個大於 xx 的元素的出現位置

D: 同上,但是詢問增加了位置需要在 ll 右邊的條件

線段樹維護動態規劃(Dynamic Programming)

有些動態規劃的題目,我們可以將轉移式放到線段樹上,來幫助我們尋找答案

而這裡,我們以「區間最大連續和」為例

問題如下:

給一個 nn 項的陣列,以及 qq 個操作或詢問

每次操作修改一個位置的值

每次詢問區間[l,r][l,r] 之間的區間最大連續和

這個問題,你可能會很直覺的想到動態規劃中所學到的 O(n)O(n) 解法

但是,今天我們有qq次詢問,每次詢問都跑 O(n)O(n) ,那總時間會高達 O(qn)O(qn)

不過,這樣的問題我們可以使用線段樹來輔助達成多次詢問

我們在前一章節所學的線段樹,大多都是維護總和/最大值等等

而如果我們將其改成維護區間最大連續和呢?

int query(int ql, int qr, int idx, int l, int r){
    if(ql <= l && r <= qr){
        return tr[idx];
    }
    int m = (l+r)/2;
    if(ql > m){
        return query(ql, qr, idx*2+1, m+1, r);
    }
    if(qr <= m){
        return query(ql, qr, idx*2, l, m);
    }
    //修改合併函數!
    return combine(query(ql, qr, idx*2, l, m), query(ql, qr, idx*2+1, m+1, r));
}

首先,我們該如何維護一個節點的答案呢?

我們可以寫成以下四個式子,作為每個節點所需維護的值的轉移式

(用 AA 表示答案、SS 表示總和、 L,RL,R 表示前/後綴的最大總和, XX為區間)

X.A=max(Xl.A,Xr.A,Xl.R+Xr.L)X.A = \max(X_l.A,X_r.A,X_l.R+X_r.L)
X.L=max(Xl.S+Xr.L,Xl.L)X.L = \max(X_l.S+X_r.L,X_l.L)
X.R=max(Xl.R+Xr.S,Xr.S)X.R = \max(X_l.R+X_r.S,X_r.S)
X.S=Xl.S+Xr.SX.S = X_l.S+X_r.S

這四個式子為這一題維護答案的四個轉移式

第一個式子表示的是一個區間的答案,會是左右區間的答案,或左區間的後綴最大總和與右區間的前綴最大總和相加

第二與第三個式子則是維護前綴最大總和與後綴最大總和

第四個式子是將左右區間的總和相加,則為此區間的總和

這四種操作能完美的解決這個問題!

以下為實作程式碼

struct node{
    ll ans, left, right, sum;
    node(){}
    node(ll a, ll b, ll c, ll d){
        ans = a, left = b, right = c, sum = d;
    }
};

node combine(node a, node b){
    node c;
    c.ans = max({a.ans, b.ans, a.right+b.left});
    c.left = max(a.sum+b.left, a.left);
    c.right = max(a.right+b.sum, b.right);
    c.sum = a.sum + b.sum;
    return c;
}

點我前往題目

A: 即範例題,在每次修改後輸出整個陣列的答案

Last updated

Was this helpful?