1. 程式人生 > >非遞迴線段樹區間修改區間求和的兩種實現(以POJ 3468為例)

非遞迴線段樹區間修改區間求和的兩種實現(以POJ 3468為例)

題意:就是一個數列,支援  查詢區間和  以及  區間內的數都加上 C 。 

遞迴線段樹很好寫,就不講了。

遞迴版本        : 記憶體:6500K   時間:2.6 秒

非遞迴版本一: 記憶體:4272K   時間:1.1秒

非遞迴版本二: 記憶體:4272K   時間:1.3秒

------------------------------------------------------------------------------------------------------------------------------------------

-----------------------                非遞迴思路都來自張昆瑋的PPT《統計的力量》                 ----------------------------

------------------------------------------------------------------------------------------------------------------------------------------

看了神一樣的PPT《統計的力量》之後,想試試非遞迴線段樹的區間修改和求和,於是就找了這題來測試。

方法一(差分再求和):

大意就是先將數列差分,每個數減去前一個數。然後,原本的數就變成了新數列的字首和。

原本的字首和就變成了新數列的字首和的字首和。

用S[i]表示a[1]+a[2]+...+a[i] ,用 P[i] 表示 a[1]+2*a[2]+3*a[3]+...+i*a[i]

則字首和的字首和 SS[x]=S[1]+S[2]+S[3]+...+S[x]=n*a[1]+(n-1)*a[2] +...+2*a[x-1] + a[x] = (n+1)S[x]- P[x]

於是只要對差分數列維護S[i]和P[i]兩個性質就好了。

由於陣列變成了相對的值,區間[L,R]加上C,只是把L的值加上C,把R+1的值減去C。也就是把區間修改簡化到了點修改。

於是程式碼就很好寫了。

程式碼中S[i]只代表 a[i] 這一項,要對S[i]求字首和才得到上面公式中的S[i].

程式碼中P[i]只代表i*a[i]這一項,要對P[i]求字首和才得到上面公式中的P[i].

最後求區間和[L,R]就是求兩個SS再相減(SS[R]-SS[L-1])。

方法二(標記永久化):

《統計的力量》中只對這個方法作了簡短的說明,想了好久才想出怎麼實現。

大致思想就是,由於非遞迴的查詢是自下而上的,不可能下傳標記,那麼就乾脆不下傳標記(也就是標記永久化)。

而是改成往上查詢區間的過程中遇到標記就更新答案,以前一直不知道怎麼做到這一點,最近重新看的時候才想到。

這題需要add標記和sum標記(節點的sum標記並沒有考慮本節點的add)。

區間查詢:

s和t 的區間查詢過程本來就是在它們變成同一顆樹的左右子樹之前,若s是左節點,就將s^1節點的值加上,若t是右節點,則將t^1的節點的值加上。

現在有了標記,注意到,在每次for迴圈中 樹s上的標記是對s的葉節點有效的,而目前s這邊已經計算的所有節點都是s的子樹,

所以只需要記錄s這邊已經被計算的節點數量Ln就可以做到按標記更新左邊的答案。t 的那邊是一樣的。

for迴圈結束後並沒有到此為止,還需要處理此時s和t 的標記,之後還要處理s和t的所有公共祖先上的標記。

一個小問題:這裡解決了所求區間段以上的add標記,那麼這些區間以下的標記怎麼辦?

比如只對某元素做了add標記(非遞迴的區間加標記也是自下而上的,所以頂層並不知道下面有標記),

但是區間查詢的時候是對整體查詢的話,非遞迴的查詢會直接查詢上面的區間,而忽略下面的標記。

答案是以下的標記資訊存於sum中,於是區間修改也需要修改 被修改的段 所影響的所有祖先的sum(其實要修改的並不多),

通過sum來知道該節點以下有多少被add了。

也就是說,add是直接加到需要加的區間上,然後向上處理所有被影響的sum.

區間修改:自下而上地更新所有改變的add和sum

修改的整體框架跟查詢一樣。

核心思想:每次for迴圈中,s的標記代表了所有s這邊已經處理過的數,s^1的標記是需要被修改(區間修改中)或加上(區間求和中)的資料。

修改或計算完s^1之後不要忘了更新已經被計算的節點數量Ln的值。

for迴圈結束後要分別處理s,t節點,並且再處理s和t的所有公共祖先。

小小總結:

第一種方法比第二種稍微快一點,寫起來也簡單一點,但是侷限性更大一些,沒發現如何修改成求區間最大最小值。

第二種方法更加常規一些,同樣的思路可以支援更多標記的維護,而且陣列的定義上也跟遞迴線段樹一樣(sum和add)。

感覺我寫的不夠簡潔,第二種方法的寫法上應該還可以優化。

程式碼:

下面是第一種方法的核心程式碼(先差分再求字首和的字首和):

#define LL long long
#define maxn 100001
LL S[maxn<<2];
LL SS[maxn<<2];
int N,Q,X;
void PushUp(int x){//更新
	S[x]=S[x<<1]+S[x<<1|1];
	P[x]=P[x<<1]+P[x<<1|1];
}
void init(){//init之前給 N 賦值  
	X=1;while(X <N+2) X <<=1;//計算偏移量
	for(int i=1;i<=N;++i) scanf("%lld",&S[X+i]);//讀取N個數
	S[X]=P[X]=0;for(int i=N+1;i<X;++i) S[X+i]=0; 
	for(int i=X-1;i>0;--i) S[X+i]-=S[X+i-1];//差分 
	for(int i=1;i<X;++i) P[X+i]=S[X+i]*i; //計算P
	for(int i=X-1;i>0;--i) PushUp(i);//建樹
}
void INC(LL L,LL R,LL C){//區間修改簡化成點修改
	int s=X+L,t=X+R+1;
	S[s]+=C;S[t]-=C;
	P[s]+=C*L;P[t]-=C*(R+1);
	while(s^1) s>>=1,PushUp(s);
	while(t^1) t>>=1,PushUp(t);
}
LL QUE(LL R){//字首和
	LL SumP=0,SumS=0;
	for(int t=X+R+1;t^1;t>>=1){
		if(t&1)  SumP+=P[t^1],SumS+=S[t^1];
	}
	return (R+1)*SumS-SumP;
}

第二種方法(標記永久化):

#define LL long long
#define maxn 100001
LL sum[maxn<<2];
LL add[maxn<<2];
int N,Q,X;
void init(){//init之前給 N 賦值  
	X=1;while(X <N+2) X <<=1; 
	memset(add,0,sizeof(add));
	for(int i=1;i<=N;++i) scanf("%lld",&sum[X+i]);
	sum[X]=0;for(int i=N+1;i<X;++i) sum[X+i]=0; 
	for(int i=X-1;i>0;--i) sum[x]=sum[x << 1] + sum[x << 1 | 1];
}
LL QUE(int L,int R){//區間求和
	int s=X+L-1,t=X+R+1;//葉節點
	int Ln=0,Rn=0,x=1;//左右支的已加點數,以及每樹元素個數 
	LL Ans=0;//如果是set標記的話,可以加左右Ans分開求和 
	for(;s^t^1;s >>= 1,t >>= 1,x <<= 1){
		//先讀取標記更新
		if(add[s]) Ans+=add[s]*Ln;
		if(add[t]) Ans+=add[t]*Rn;
		//再常規求和 
		if(~s&1) Ans+=sum[s^1]+x*add[s^1],Ln+=x;
		if(t&1)  Ans+=sum[t^1]+x*add[t^1],Rn+=x;
	}
	//處理同層的情況
	if(add[s]) Ans+=add[s]*Ln;
	if(add[t]) Ans+=add[t]*Rn;
	s>>=1;Ln+=Rn;
	//處理上層的情況 
	for(;s^1;s>>=1) if(add[s]) Ans+=add[s]*Ln;
	return Ans;
}
void INC(int L,int R,int C){//區間+C
	int s=X+L-1,t=X+R+1;//葉節點
	int Ln=0,Rn=0,x=1;//左右支的已加點數,以及每樹元素個數 
	for(;s^t^1;s >>= 1,t >>= 1,x <<= 1){
		//先處理sum 
		sum[s]+=(LL) C*Ln;
		sum[t]+=(LL) C*Rn;
		//再處理add
		if(~s&1) add[s^1]+=C,Ln+=x;
		if(t&1)  add[t^1]+=C,Rn+=x;
	}
	//處理同層 
	sum[s]+=(LL) C*Ln;
	sum[t]+=(LL) C*Rn;
	s>>=1;Ln+=Rn;
	//處理上層 
	for(;s^1;s>>=1) sum[s]+=(LL)C*Ln;
}