1. 程式人生 > 其它 >線段樹1

線段樹1

線段樹(Segment Tree),是一種支援單點/區間修改、滿足結合律的詢問(如求和、最值等)的資料結構。

下圖可以大體表示線段樹的工作原理。

解釋:
容易看出最頂上的是原陣列,每個葉子節點依次是一個單個的陣列元素。而對於每個非葉子節點,將其分解為兩半——假使這個節點儲存的是 \([l,r]\) 區間,那麼左子節點儲存的則是 \([l,mid]\),右子節點是 \([mid+1,r]\),其中 \(mid\) 為他們的正中央,即 \(\lfloor\frac{l+r}{2}\rfloor\)。關於編號:可以方便地得出編號為 \(k\) 的節點的左右子節點編號為 \(2k,2k+1\);使用位運算更加快捷,即 k<<1,k<<1|1

。對於線段樹,每個節點除了有一個編號和一個子陣列,還需要儲存 \([l,r]\) 的答案,供詢問和修改所用(要不建這麼大一棵樹幹嘛),用 \(t_k\) 表示節點 \(k\) 的答案。

下面以區間加(修改)與(詢問)區間求和為例來闡述線段樹的工作過程。

5 4
3 6 2 1 9
1 2 5 2
2 1 4
1 1 3 5
2 2 4

假設上面是輸入:第一行是 \(n,m\) 代表數列長度和詢問個數。第二行 \(n\) 個數代表數列。最後 \(m\) 行每行一個詢問——

  • 1 l r x 代表對區間 \([l,r]\) 內所有數加個 \(x\)
  • 2 l r 代表詢問 \([l,r]\) 中所有數之和。

一、建樹

const int N=5e6+5;
int a[N],t[N],lazy[N];
void pushup(int k){
	t[k]=t[k<<1]+t[k<<1|1];
}
void build(int l,int r,int k){
	if(l==r) t[k]=a[l];
	else {
		int mid=(l+r)/2;
		build(l,mid,k<<1);
		build(mid+1,r,k<<1|1);
		pushup(k);
	}
}

解釋:

  1. a:原陣列 t:線段樹
  2. build: 建樹函式。思路是,首先把左右兒子都建了,然後把它們的 t 給加起來於是得到 k 的 t,就建好了。如果這棵子樹就是葉子結點(l==r),那麼顯然它的 t 就等於 a[l]。
  3. pushup: 整合函式,就是把左右兒子的值整合一下傳給當前的 k。

二、修改

void pushdown(int l,int r,int k){
	if(lazy[k]){
		lazy[k<<1]+=lazy[k];
		lazy[k<<1|1]+=lazy[k];
		int mid=(l+r)/2;
		t[k<<1]+=lazy[k]*(mid-l+1);
		t[k<<1|1]+=lazy[k]*(r-mid);
		lazy[k]=0;
	}
}
void updata(int L,int R,int v,int l,int r,int k){
	if(L<=l && r<=R){
		lazy[k]+=v;
		t[k]+=v*(r-l+1);
	}
	else {
		pushdown(l,r,k);
		int mid=(l+r)/2;
		if(L<=mid) updata(L,R,v,l,mid,k<<1);
		if(R>mid) updata(L,R,v,mid+1,r,k<<1|1);
		pushup(k);
	}
}
  1. lazy: 如果現在對編號 k 的區間加上了 x 但是現在還沒有問到 k 或 k 的子孫們的答案,那就省一時的力,暫且給 k 打上一個叫做 lazy 的標籤,代表“此節點還有多少需要加但偷懶還沒有加的值”,所以只需要 lazy[k]+=x 就好了。
  2. pushdown: 如果 k 還有偷懶沒加的值,就把他給處理掉:首先下發給它的兒子們(因為 [l,r]+=x 就相當於 [l,mid]+=x,[mid+1,r]+=x 了),然後他的兒子們如果需要用到實際值(不能再偷懶了)就把他們的lazy處理掉,不需要用就儲存在他們的 lazy 裡,總之 lazy[k] 是已經沒有負擔了。
  3. updata: 修改函式。如果這個區間完全包含於要修改的區間 [L,R] 內,說明他可以偷懶,只用把 lazy 加上 v 就好了。那麼如果不是完整的包含在要修改的區間內,比如說修改的是 [3,5] 但當前的是 [1,4],那麼就需要先修改他的兒子們再傳給他自己。當然,修改他的兒子之前,需要先把 lazy[k] 處理掉,就好比可以暫且不幹活的時候就偷懶不幹活,不能不幹活的時候就乾脆一氣全部幹了,以求一勞永逸。

三、詢問

int query(int L,int R,int l,int r,int k){
	pushdown(l,r,k);
	if(L<=l && r<=R) return t[k];
	else {
		int mid=(l+r)/2;
		int res=0;
		if(L<=mid) res+=query(L,R,l,mid,k<<1);
		if(R>mid) res+=query(L,R,mid+1,r,k<<1|1);
		return res;
	}
}

結構與 updata() 類似,這裡不贅述。

四、完整程式碼

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e6+5;
int a[N],t[N],lazy[N];
void pushup(int k){
	t[k]=t[k<<1]+t[k<<1|1];
}
void build(int l,int r,int k){
	if(l==r) t[k]=a[l];
	else {
		int mid=(l+r)/2;
		build(l,mid,k<<1);
		build(mid+1,r,k<<1|1);
		pushup(k);
	}
}
void pushdown(int l,int r,int k){
	if(lazy[k]){
		lazy[k<<1]+=lazy[k];
		lazy[k<<1|1]+=lazy[k];
		int mid=(l+r)/2;
		t[k<<1]+=lazy[k]*(mid-l+1);
		t[k<<1|1]+=lazy[k]*(r-mid);
		lazy[k]=0;
	}
}
void updata(int L,int R,int v,int l,int r,int k){
	if(L<=l && r<=R){
		lazy[k]+=v;
		t[k]+=v*(r-l+1);
	}
	else {
		pushdown(l,r,k);
		int mid=(l+r)/2;
		if(L<=mid) updata(L,R,v,l,mid,k<<1);
		if(R>mid) updata(L,R,v,mid+1,r,k<<1|1);
		pushup(k);
	}
}
int query(int L,int R,int l,int r,int k){
	pushdown(l,r,k);
	if(L<=l && r<=R) return t[k];
	else {
		int mid=(l+r)/2;
		int res=0;
		if(L<=mid) res+=query(L,R,l,mid,k<<1);
		if(R>mid) res+=query(L,R,mid+1,r,k<<1|1);
		return res;
	}
}
signed main()
{
	int n,m,opt,l,r,c;
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	build(1,n,1);
	while(m--){
		cin>>opt;
		if(opt==1) cin>>l>>r>>c,updata(l,r,c,1,n,1);
		else cin>>l>>r,cout<<query(l,r,1,n,1)<<endl;
	}
	return 0;
}

這是一道線段樹模板(link),還有一些練習題可以參照以下連結:

感謝觀看!