「學習筆記」線段樹標記永久化

2023-05-18 06:01:10

第一次見到這個詞是在 zkw 線段樹的課件裡見到的。

標記永久化可以避免下傳懶惰標記,只需在進行詢問時把標記的影響加到答案當中,從而降低程式常數。


洛谷的模板題也證明,確實是小常數。
這三次提交都是遞迴寫法,如果搭配 zkw 線段樹,應該會跑得更快。

具體操作

我們在講懶標向下遞迴的過程中,如果當前區間正好等於查詢區間,那就直接改懶標和數值,倘若當前區間包含查詢區間但不與查詢區間相等,那我們只修改值,這些操作與線段樹修改操作很像。

inline void modify(int u, int l, int r, int lr, int rr, ll c) {
	t[u].val += (rr - lr + 1) * c;
	if (lr == l && r == rr) {
		t[u].laz += c;
		return ;
	}
	if (rr <= mid)	modify(ls, l, mid, lr, rr, c);
	else if (lr > mid)	modify(rs, mid + 1, r, lr, rr, c);
	else {
		modify(ls, l, mid, lr, mid, c);
		modify(rs, mid + 1, r, mid + 1, rr, c);
	}
}

需要注意的是,如果查詢的區間橫跨左右兩個孩子區間,那我們需要將查詢區間也從 mid 處分開。


設定好懶標,查詢時該如何處理懶標呢?
按照一般的寫法,在向下遞迴時,我們還要用遞迴把懶標也一起向下傳遞,而標記永久化則是捨棄了向下傳遞懶標這個操作,我們在查詢時設定一個值,用它來記錄沿路的懶標,最後一起統計即可。
為什麼要記錄沿路的懶標呢?
如果包含該區間的大區間被打上了懶標,則說明這一整個大區間都受到這個懶標的影響,所以把它記錄下來。

inline ll query(int u, int l, int r, int lr, int rr, ll add) {
	if (lr == l && r == rr) {
		return t[u].val + add * t[u].len;
	}
	ll sum = 0;
	if (rr <= mid) {
		sum = query(ls, l, mid, lr, rr, add + t[u].laz);
	}
	else if (lr > mid) {
		sum = query(rs, mid + 1, r, lr, rr, add + t[u].laz);
	}
	else {
		sum = query(ls, l, mid, lr, mid, add + t[u].laz) 
		+ query(rs, mid + 1, r, mid + 1, rr, add + t[u].laz);
	}
	return sum;
}

最後處理答案時,就是將懶標的和乘上這個區間的長度,add 記錄的是懶標和,可以將這個 add 看作是對於這個區間的每個元素一共要增加的值。

總結

好處:

  1. 碼量小,不用寫 pushdownpushup
  2. 在可持久化線段樹上應用該技巧能做到區間修改的效果。

壞處:

  1. 適用範圍有限,只有當求的東西滿足區間貢獻獨立。比如區間加法。
    區間最大值就無法標記永久化。
  2. 多標記好像也不適用。

總歸來說,對於一般的線段樹,遞迴寫法就足夠了,標記永久化用的較少,對於線段樹套線段樹這樣的應該會用的比較多。

例題

【模板】線段樹 1

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define ls (u << 1)
#define rs (u << 1 | 1)
#define mid ((l + r) >> 1)

const int N = 1e5 + 5;

int n, m;

struct seg_tree {
	int len;
	ll val, laz;
} t[N << 2];

inline void build(int u, int l, int r) {
	t[u].len = r - l + 1, t[u].laz = 0;
	if (l == r) {
		cin >> t[u].val;
		return ;
	}
	build(ls, l, mid);
	build(rs, mid + 1, r);
	t[u].val = t[ls].val + t[rs].val;
}

inline void modify(int u, int l, int r, int lr, int rr, ll c) {
	t[u].val += (rr - lr + 1) * c;
	if (lr == l && r == rr) {
		t[u].laz += c;
		return ;
	}
	if (rr <= mid)	modify(ls, l, mid, lr, rr, c);
	else if (lr > mid)	modify(rs, mid + 1, r, lr, rr, c);
	else {
		modify(ls, l, mid, lr, mid, c);
		modify(rs, mid + 1, r, mid + 1, rr, c);
	}
}

inline ll query(int u, int l, int r, int lr, int rr, ll add) {
	if (lr == l && r == rr) {
		return t[u].val + add * t[u].len;
	}
	ll sum = 0;
	if (rr <= mid) {
		sum = query(ls, l, mid, lr, rr, add + t[u].laz);
	}
	else if (lr > mid) {
		sum = query(rs, mid + 1, r, lr, rr, add + t[u].laz);
	}
	else {
		sum = query(ls, l, mid, lr, mid, add + t[u].laz) 
		+ query(rs, mid + 1, r, mid + 1, rr, add + t[u].laz);
	}
	return sum;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	build(1, 1, n);
	for (int i = 1, op, x, y; i <= m; ++ i) {
		cin >> op >> x >> y;
		if (op == 1) {
			ll k;
			cin >> k;
			modify(1, 1, n, x, y, k);
		}
		else {
			cout << query(1, 1, n, x, y, 0) << '\n';
		}
	}
	return 0;
}