線段樹學習筆記(更新中)

viki617發表於2024-03-10

本文章開始寫作於2024年3月10日22:48,這也是我第一次沒有參考板子,獨立寫出一個線段樹的時刻(雖然只是板子題並且debug的時候還是參考了一下)

寫這個主要是為了我自己以後複習起來方便,畢竟這玩意還是極其容易寫掛的

注意:以下內容中標為斜體的是需要按照題目要求具體情況具體分析的,文章中的操作和程式碼是對板子題而言的

下面是板子題的完整程式碼(區間加法,區間求和)

#include<bits/stdc++.h>
#define int long long
#define ls (p << 1)
#define rs ((p << 1) | 1)
#define mid (s + ((t - s) >> 1))
using namespace std;
const int MX = 4000055;
int a[MX],sum[MX],lz[MX];
void build(int s,int t,int p) {
	if(s == t) {
		sum[p] = a[s];
		return;
	}
	build(s,mid,ls);
	build(mid + 1,t,rs);
	sum[p] = sum[ls] + sum[rs];
}
void pushdown(int p,int s,int t) {
	if(lz[p]) {
		sum[ls] += lz[p] * (mid - s + 1);
		sum[rs] += lz[p] * (t - mid);
		lz[ls] += lz[p];
		lz[rs] += lz[p];
		lz[p] = 0;
	}
}
void update(int l,int r,int s,int t,int p,int v) {
	if(s >= l && t <= r) {
		sum[p] += v * (t - s + 1);
		lz[p] += v;
		return; 
	}
	pushdown(p,s,t);
	if(l <= mid) update(l,r,s,mid,ls,v);
	if(r >= mid + 1) update(l,r,mid + 1,t,rs,v);
	sum[p] = sum[ls] + sum[rs]; 
}
int query(int l,int r,int s,int t,int p) {
	if(s >= l && t <= r) return sum[p];
	pushdown(p,s,t);
	int ans = 0;
	if(l <= mid) ans += query(l,r,s,mid,ls);
	if(r >= mid + 1) ans += query(l,r,mid + 1,t,rs);
	return ans;
} 
signed main()
{
	int op,x,y,k,n,q;
	scanf("%lld%lld",&n,&q);
	for(int i = 1;i <= n;i++) scanf("%lld",&a[i]);
	build(1,n,1);
	for(int i = 1;i <= q;i++) {
		scanf("%lld",&op);
		if(op == 1) {
			scanf("%lld%lld%lld",&x,&y,&k);
			update(x,y,1,n,1,k);
		}
		else {
			scanf("%lld%lld",&x,&y);
			printf("%lld\n",query(x,y,1,n,1));
		}
	}
	return 0;
}

我們接下來慢慢分析(勿噴

#define ls (p << 1)
#define rs ((p << 1) | 1)
#define mid (s + ((t - s) >> 1))

一大堆莫名其妙的define。
把左兒子編號、右兒子編號和中間位置縮略了一下。

以下的程式碼中,

a[]代表原數列,sum[]代表區間的和,lz[]代表懶標記,
l、r代表目標區間的左右端點,s、t代表當前操作區間的左右端點,p代表當前操作區間的編號。


void build(int s,int t,int p) {
	if(s == t) {
		sum[p] = a[s];
		return;
	}
	build(s,mid,ls);
	build(mid + 1,t,rs);
	sum[p] = sum[ls] + sum[rs];
}

這段程式碼是用來建樹的,首先,如果當前區間的左右端點相等(也就是隻有一個點), 那就把它的sum值賦值為原數列中對應位置的數(一個數的和不就是他自己嗎)

否則,就不斷對左右子區間遞迴操作,直到區間只有一個數。

最後,不要忘了將每個區間的sum值賦值為它左右兩個子區間sum值的和。


void pushdown(int p,int s,int t) {
	if(lz[p]) {
		sum[ls] += lz[p] * (mid - s + 1);
		sum[rs] += lz[p] * (t - mid);
		lz[ls] += lz[p];
		lz[rs] += lz[p];
		lz[p] = 0;
	}
}

這段程式碼是用來下放懶標記的。

首先解釋一下懶標記的作用:

如果某次要求對一個大區間進行修改,那麼正常來說每次修改的時候都要對它的子區間進行更新以保證正確性,但是這樣的時間複雜度無法接受,是\(O(N)\)的、

那麼如何最佳化呢?
那就是,當每次修改大區間的時候只對大區間的維護值進行更新,而其子區間的維護值等到需要查詢或修改的時候再更新。
這樣做的話可以最佳化掉大量不必要的操作,從而降低時間複雜度,使得線段樹的時間複雜度為\(O(nlogn)\)的。
這就很nice。

sum[ls] += lz[p] * (mid - s + 1);
sum[rs] += lz[p] * (t - mid);

這兩句的作用是用父區間的懶標記更新左右子區間的維護值。

lz[ls] += lz[p];
lz[rs] += lz[p];
lz[p] = 0;

這三句的作用是更新左右子區間的懶標記(因為只更新了“兒子”區間的維護值,“孫子”區間的維護值還沒更新呢,需要將父區間的懶標記下傳給“兒子”區間以正確更新“孫子”區間)並清空父區間的懶標記。


void update(int l,int r,int s,int t,int p,int v) {
	if(s >= l && t <= r) {
		sum[p] += v * (t - s + 1);
		lz[p] += v;
		return; 
	}
	pushdown(p,s,t);
	if(l <= mid) update(l,r,s,mid,ls,v);
	if(r >= mid + 1) update(l,r,mid + 1,t,rs,v);
	sum[p] = sum[ls] + sum[rs]; 
}

這段程式碼的作用是更新區間。
如果目標區間完全覆蓋了當前操作區間,那麼直接更新當前操作區間的維護值。 更新方法具體情況具體分析,比如板子題要求區間加法,給區間內的每個數都加上一個值,那麼區間和的更新量就是區間元素數量乘以要加的值。
如果沒有完全覆蓋,就先下放當前區間的懶標記以保證正確性(否則子區間的更新量可能會是錯誤的),然後遞迴更新左右子區間。

最後,將父區間的維護值更新為左右子區間的和。注意這裡要寫“=”而不是“+=”!


 int query(int l,int r,int s,int t,int p) {
	if(s >= l && t <= r) return sum[p];
	pushdown(p,s,t);
	int ans = 0;
	if(l <= mid) ans += query(l,r,s,mid,ls);
	if(r >= mid + 1) ans += query(l,r,mid + 1,t,rs);
	return ans;
} 

這段程式碼是用來對區間求和的。
同樣,如果目標區間覆蓋了當前操作區間,直接返回當前操作區間的維護值;否則,先下放懶標記,然後遞迴處理左右子區間,最後返回答案。


以上就是線段樹的基本框架(以板子題為例),本文持續更新中。

相關文章