樹狀數組實現矩形區域的修改以及求和
By 巖之痕
講講樹狀數組如何實現對一個矩陣的矩形區域的加法和求和。
以下,(x1,y1)爲矩形的左下角座標,(x2,y2)爲矩形的右上角座標(x1<=x2 , y1<=y2)。
矩形加法:Add(int x1,int y1,int x2,int y2,int K)表示給指定矩形區域的元素都加上K。
矩形求和:Sum(int x1,int y1,int x2,int y2) 即求
思路就是通過差分,化區間修改爲點修改,然後用樹狀數組來解決。
由於要用到樹狀數組,以下所有數組下標都從1開始,並令A[0]=0,方便討論。
先說說一維的情況:
對於一維數組A,要求區間和,首先化區間和爲前綴和:
於是只需要討論如何求前綴和()就行了。
對於區間修改,直接用樹狀數組來維護A的前綴和是O(n)的時間複雜度。
樹狀數組只支持點修改,於是,想一想,怎麼化區間修改爲點修改?
考慮數組a[i]=A[i]-A[i-1](注意邊界條件A[0]=0,所以i=1的時候這個式子也是符合的),
當A數組的區間[L,R]都加上K的時候,a數組只有兩個值改變了,
a[L] 多了K ,a[R+1] 少了K,這就做到了化區間修改爲點修改。
我們再來看看如何從a[i]來得到A[i]的前綴和。
首先注意到:
前綴和的計算如下:
擴展到二維的情況:
開始講二維的情況:
類似一維情況中化區間和爲前綴和之差的方法,有如下公式:
於是只需要解決這個問題:
思路當然還是差分,首先縱向差分:
令,則
於是:
此時,b[i][j]+= t 代表的含義就是b[i][j],b[i][j+1],...,b[i][n]都增加了t.
也就是實現了縱向的化區間修改爲點修改。
要修改一個矩形區域,也就是需要連續修改i在一個區間的b[i][j],於是將b[i][j]再橫向差分,
把(2)式代入(1)式:
由上面公式看出,只需要維護下面四個元素的矩陣和,就可以求出
所以,用二維樹狀數組維護上面四個矩陣和就行了。
此時a[i][j]+=t,代表b[i][j],b[i+1][j],...,b[n][j]都增加了t,
也就代表
A[i][j],A[i][j+1],...A[i][n]都增加了t
A[i+1][j],A[i+1][j+1],...A[i+1][n]都增加了t
...
A[n][j],A[n][j+1],...A[n][n]都增加了t
於是矩形區域的加法也就不難表示了,調用4次Add(點修改)就行了。
矩形區域的求和也不難表示了,調用4次Sum就行了。
void AddSquare(int x1,int y1,int x2,int y2,int K){
Add(x1,y1,K);Add(x2+1,y2+1,K);
Add(x1,y2+1,-K);Add(x2+1,y1,-K);
}
long long SumSquare(int x1,int y1,int x2,int y2){
return Sum(x2,y2)-Sum(x1-1,y2)-Sum(x2,y1-1)+Sum(x1-1,y1-1);
}
時間複雜度:
暴力法做矩形修改的話,時間複雜度是
二維樹狀數組的時間複雜度:
數組元素個數爲1000*1000的時候,Sum操作平均用到24.38個數組元素,
Add操作平均用到25.60個數組元素。
由於本題維護了4個數組,所以每個Sum平均涉及97.52個元素。
所以每個Add平均涉及102.40個元素。
然後每次操作用到了4次Add或者4次Sum.
所以二維樹狀數組每次操作平均更改400個數組元素的值。
同等情況下,暴力法需要更改的元素個數最少爲1,最多爲10^6.在1000*1000,隨機數據的情況下暴力法平均每次操作更改62500元素,極限數據下更改1000000元素
隨機數據下用時是二維樹狀數組的156倍,極限數據下是2500倍。
1000*1000時,10000個對整個數組的操作時(極限數據),減去輸入輸出的時間之後:
二維樹狀數組的用時:0.013s,暴力法用時:39.517s 約爲3000倍。
1000*1000,隨機進行100000次操作時,減去輸入輸出時間後:
二維樹狀數組用時:0.314s 暴力法用時:47.27s. 約爲150.54倍。
實驗跟計算基本符合。
最後附上代碼:
#define LL long long
int n,m;
LL A[1027][1027][4];
LL C[4];
void Add(int x,int y,LL K){//點修改
C[0]=K;C[1]=K*y;C[2]=K*x;C[3]=K*x*y;
int xx=x;
while(xx <= n){
int yy=y;
while(yy <= n){
A[xx][yy][0]+=C[0];
A[xx][yy][1]+=C[1];
A[xx][yy][2]+=C[2];
A[xx][yy][3]+=C[3];
yy+=yy&-yy;
}
xx+=xx&-xx;
}
}
LL Sum(int x,int y){//1..x,1..y的區域求和
C[0]=C[1]=C[2]=C[3]=0;
int xx=x;
while(xx > 0){
int yy=y;
while(yy > 0){
C[0]+=A[xx][yy][0];
C[1]+=A[xx][yy][1];
C[2]+=A[xx][yy][2];
C[3]+=A[xx][yy][3];
yy-=yy&-yy;
}
xx-=xx&-xx;
}
return (x+1)*(y+1)*C[0]-(x+1)*C[1]-(y+1)*C[2]+C[3];
}
void AddSquare(int x1,int y1,int x2,int y2,int K){//矩形區域的修改
Add(x1,y1,K);Add(x2+1,y2+1,K);
Add(x1,y2+1,-K);Add(x2+1,y1,-K);
}
LL SumSquare(int x1,int y1,int x2,int y2){//矩形區域的求和
return Sum(x2,y2)-Sum(x1-1,y2)-Sum(x2,y1-1)+Sum(x1-1,y1-1);
}