區間修改與懶惰標記

遇到修改連續區間時,紀錄下來之後再做不就好了嗎?

在看這章節前,若尚未學過單點修改的線段樹,請點選以下連結

單點修改 區間詢問

區間加值 單點詢問

我們已經學過單點修改的線段樹可以在 O(logn)O(\log n)的時間內完成,那麼如果我們今天只是要在修改後,詢問某一個位置的值,那麼我們其實可以使用一種技巧,也就是「差分」!

設一個陣列 AA 的元素為以下

A[0], A[1], A[2], ..., A[n]A[0], \ A[1], \ A[2], \ ..., \ A[n]

在進行區間修改 [l,r][l,r] 時,如果我們每一個點都去修改他的值,我們要花的時間為 O((rl+1)logn)O(nlogn)O((r-l+1)\log n) \approx O(n \log n) ,實在不是一個非常好的作法

如果我們用另外一個陣列 BB 存陣列 AA 兩兩元素之間的差距呢? 也就是如下

A[1]A[0], A[2]A[1], A[3]A[2],..., A[n]A[n1]A[1] - A[0], \ A[2] - A[1], \ A[3] - A[2], ..., \ A[n] - A[n-1]

那其實只需要讓 B[l]B[l] 去加上區間所要加的值,以及 B[r+1]B[r+1] 去減掉區間加的值

當我們在詢問 A[i]A[i] 時,只要詢問陣列BB 的前 ii 項之和,即可得到答案

時間複雜度:區間加值 O(logn)O(\log n) ,單點求值 O(logn)O(\log n)

區間修改 區間詢問

我們可以看一下線段樹的運作方式,如下圖

在我們要修改區間 [2,5][2,5] 時,我們其實只要像區間詢問時,只修改上面塗成紅色的三個區間就好了

但是!這樣會出現一個問題,當我們修改完 [3,4][3,4] 之後,如果詢問 [3][3] 的值呢?會發現如果我們僅僅是修改 [3,4][3,4] ,那麼在詢問 [3][3] 的時候,就會是還沒修改過的值了

你可能會想,那我們就認命的一個一個元素慢慢修改吧!

不過!這一點,線段樹可以輕鬆解決! 我們可以在每一個節點去紀錄已經修改過的值,當我們在詢問他的子節點時,再下推答案就好了!

這時候我們就會用到我們稱為「懶惰標記(Lazy Propagation)」的東西了

懶惰標記

要使用這種方式進行區間修改,則對於線段樹中的值必須具有「可預測性」

什麼是「可預測性」呢?

例如:對整個區間進行 ++ 的操作,如果每個點都增加 xx ,那麼整個區間 [l,r][l,r] 的總和則會增加 (rl+1)x(r-l+1)x

代表說,我們不必去修改他的子節點,即可得知該區間的值

之後,我們可以在該節點上,打上一個標記,紀錄目前這個區間已經增加了 xx ,那麼當我們要詢問樹上該節點的子節點時,我們就可以順便將 xx 的值下推

詳細作法如下:

const int MAXN = 1e5+5;
int tr[MAXN*4], arr[MAXN], tag[MAXN*4]; //線段樹的節點數量一般會開成 4n

void push(int idx){
    //下推節點的標記(這裡以區間加值 區間最大值為例)
    if(tag[idx]){
        tr[idx<<1] += tag[idx];
        tr[idx<<1|1] += tag[idx];
        tag[idx<<1] += tag[idx];
        tag[idx<<1|1] += tag[idx];
        tag[idx] = 0;
    }
}

int query(int ql, int qr, int idx, int l, int r){
    if(l!=r) push(idx); //當節點並非葉節點時,下推標記
    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));
}

//這裡最特別的地方,區間修改,寫法與區間詢問大致相同
void modify(int ql, int qr, int val, int idx, int l, int r){
    if(l!=r) push(idx); //當節點並非葉節點時,下推標記
    if(ql <= l && r <= qr){
        tr[idx] += val;
        tag[idx] += val;
        return;
    }
    int m = (l+r)/2;
    if(qr > m) modify(ql, qr, val, idx*2+1, m+1, r);
    if(ql <= m) modify(ql, qr, val, idx*2, l, m);
    tr[idx] = combine(tr[idx<<1],tr[idx<<1|1]);
}

練習題

點我前往題目

第一題:區間加值 單點求值

第二題:區間最大值 單點求值

第三題:區間賦值 單點求值

Last updated