非遞迴線段樹區間修改區間求和的兩種實現(以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;
}