淺談樹狀數組與線段樹


樹狀數組和線段樹都是用於維護數列信息的數據結構,支持單點/區間修改,單點/區間詢問信息。以增加權值與詢問區間權值和為例,其余的信息需要維護也都類似。時間復雜度均為\(O(logn)\)

樹狀數組

對於樹狀數組,編號為\(x\)的結點上統計着[\(x-lowbit(x)+1,x\)]這一段區間的信息,\(x\)的父親就是\(x+lowbit(x)\)

如果不知道\(lowbit\)是啥的話可以去看看這個:https://www.cnblogs.com/AKMer/p/9698694.html

畫出來就長這樣:

假設我們要維護的區間是\(A\)數組,那么\(C\)數組就是樹狀數組里存的東西。每個結點掌管的區間都是[\(x-lowbit(x)+1,x\)]。

單點修改區間查詢

假設我們要令\(A_x\)增加\(v\),那么\(x\)以及\(x\)的所有祖先全部都需要增加\(v\),因為這些結點的統計區間全部都覆蓋了\(x\)這個位置,而其他結點沒有。

假設我們要詢問區間[\(l,r\)]的權值和,我們可以轉化為前綴和相減,也就是\(sum[r]-sum[l-1]\)

假設我們要求\(sum[x]\),那么我們只需要每次加上當前結點\(x\)的權值,然后令\(x\)等於\(x-lowbit(x)\),直到\(x\)為1時停下來。因為\(x\)統計的是區間[\(x-lowbit(x)+1,x\)]的信息,所以前綴和就由若干個這樣的區間組成,每次令\(x-=lowbit(x)\)就相當於去訪問前面一個區間了。由於\(lowbit\)\(x\)的二進制最低位的\(1\)有關,所以復雜度是\(O(logn)\)的。

代碼如下:

#define low(i) ((i)&(-i))

void add(int pos,int v) {
    for(int i=pos;i<=n;i+=low(i))
        c[i]+=v;//單點修改
}

int query(int pos) {
    int res=0;
    for(int i=pos;i;i-=low(i))
        res+=c[i];
    return res;//詢問區間[1,pos]的權值和
}

區間修改單點詢問

這個要利用差分的思想就行了。每次在數組的\(l\)處加\(v\)\(r+1\)處減\(v\),然后一個數的權值就是\([1,x]\)的差分和。

代碼如下:

#define low(i) ((i)&(-i))

int l=read(),r=read(),v=read();
add(l,v);add(r+1,-v);
int pos=read();
printf("%d\n",query(pos));//add與query函數見單點修改區間詢問

區間修改區間詢問

假設\(a\)是差分數組,那么前綴權值和就是\(a\)的前綴和的前綴和,也就是:

\[\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{i}a[j] \]

化開就是:

\(\sum\limits_{i=1}^{x}(x-i+1)a[i]=(x+1)\sum\limits_{i=1}^{x}a[i]-\sum\limits_{i=1}^{x}i*a[i]\)

同單點修改,我們只需要開兩個樹狀數組,一個維護\(a[i]\),一個維護\(i*a[i]\)就行了。

代碼如下:

#include <cstdio>
using namespace std;
typedef long long ll;
#define low(i) ((i)&(-i))

const int maxn=1e5+5;

int n,m;
ll a[maxn],sum[maxn];

ll read() {
	ll x=0,f=1;char ch=getchar();
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=x*10+ch-'0';
	return x*f;
}

struct TreeArray {
	ll c[maxn];

	void add(int pos,ll v) {
		for(int i=pos;i<=n;i+=low(i))
			c[i]+=v;
	}
	
	ll query(int pos) {
		ll res=0;
		for(int i=pos;i;i-=low(i))
			res+=c[i];
		return res;
	}
}T1,T2;//T1維護a[i]的前綴和,T2維護i*a[i]的前綴和

int main() {
	n=read(),m=read();
	for(int i=1;i<=n;i++)
		a[i]=read(),sum[i]=sum[i-1]+a[i];
	for(int i=1;i<=m;i++) {
		int opt=read(),l=read(),r=read();
		if(opt==1) {//區間加
			ll k=read();
			T1.add(l,k);T1.add(r+1,-k);
			T2.add(l,l*k);T2.add(r+1,-(r+1)*k);
		}
		else {//區間查詢
			ll ans=sum[r]-sum[l-1];
			ans+=(r+1)*T1.query(r)-T2.query(r);
			ans-=l*T1.query(l-1)-T2.query(l-1);
			printf("%lld\n",ans);
		}
	}
	return 0;
}

線段樹

線段樹是基於分治思想的數據結構,功能比樹狀數組更強大,長這樣:

對於一個節點\(p\),如果他統計的區間是[\(l,r\)],\(mid=(l+r)/2\),那么他左兒子統計的區間就是\([l,mid]\),右兒子是\([mid+1,r]\)。對於某些節點統計的區間是\([x,x]\),那么就直接是單點的信息,每個點的信息可以由子節點合並更新。因為線段樹是一顆二叉樹,所以我們可以用\(p*2\)來記錄\(p\)的左兒子,\(p*2+1\)記錄右兒子。這樣子的話,因為最后一層可能前面全部空出來,單出一個區間[\(n,n\)]在這一層的最后面,所以空間要開到\(4*n\)才不會段錯誤。

單點修改

直接從根開始,以覆蓋\(x\)這個位置的區間為路徑,將一條鏈上的節點全部更新。復雜度是\(O(logn)\)的。

代碼如下:

void updata(int p) {
    tree[p]=tree[p<<1]+tree[p<<1|1];
}

void change(int p,int l,int r,int pos,int v) {//更新p號節點,p號節點統計了[l,r]的信息,我要把pos位置的值增加v
    if(l==r) {
        tree[p]+=v;
        return;
	}//此時到一條鏈的最底部了就return
    int mid=(l+r)>>1;
    if(pos<=mid)change(p<<1,l,mid,pos,v);
    else change(p<<1|1,mid+1,r,pos,v);//選擇覆蓋pos的路徑遞歸
    updata(p);//更新p節點的信息
}

//更改的時候調用change(1,1,n,x,v)就行了。

區間查詢

假如當前區間被我需要訪問的區間全部覆蓋了,那么直接返回當前區間的權值和就行了。如果不是,再分情況討論,分別去遞歸詢問左兒子右兒子,再合並起來。顯然,我會訪問的節點全部是包含\(l\)\(r\)的,不包含的話會在一開始就返回統計的權值,不會進行遞歸,所以復雜度也是\(O(logn)\)的。

代碼如下:

int query(int p,int l,int r,int L,int R) {
    if(L<=l&&r<=R)return tree[p];//如果當前區間是詢問區間子區間就直接返回統計信息
    int mid=(l+r)>>1,res=0;
    if(L<=mid)res+=query(p<<1,l,mid,L,R);//如果L<=mid就返回[L,mid]的和
    if(R>mid)res+=query(p<<1|1,mid+1,r,L,R);//如果R>mid就返回[mid+1,R]的和
    return res;
}

延遲標記與區間修改

對於區間修改,如果我們一個一個值的去改的話,還不如\(n^2\)暴力統計信息的算法。所以就有了延遲標記這種東西。如果一個節點上面有延遲標記,就表示這個節點已經被修改過了,但是這個節點的子節點還沒有被修改過,如果要進行遞歸必須要把延遲標記的影響一起帶下去,然后把當前結點的延遲標記清空。對於一個區間[\(l,r\)],如果是我要修改的區間的子區間,那么我就直接把當前節點\(p\)的統計信息更新掉,然后打上延遲標記,就不進行遞歸一個一個改了。根據區間查詢的復雜度,區間修改也只會在包含\(l\)\(r\)的路徑上進行遞歸,復雜度是\(O(logn)\)的。

代碼如下:

void updata(int p) {
    tree[p]=tree[p<<1]+tree[p<<1|1];
}

void add_tag(int p,int l,int r,int v) {
    tree[p]+=(r-l+1)*v;tag[p]+=v;//標記只對兒子有影響,自己在打標記的同時一起把統計信息更改了。
}

void push_down(int p,int l,int r) {
    int mid=(l+r)>>1;
    add_tag(p<<1,l,mid,tag[p]);
    add_tag(p<<1|1,mid+1,r,tag[p]);
    tag[p]=0;//把當前標記分別傳給兩個兒子然后清空
}

void change(int p,int l,int r,int L,int R,int v) {//[l,r]為當前區間,[L,R]為要修改的區間
    if(L<=l&&r<=R) {
        add_tag(p,l,r,v);//打標記
        return;
	}
    int mid=(l+r)>>1;push_down(p,l,r);//下傳標記
    if(L<=mid)change(p<<1,l,mid,L,R,v);
    if(R>mid)change(p<<1|1,mid+1,r,L,R,v);//遞歸更改
    updata(p);//更新當前結點的信息
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM