最近準備學深點樹狀數組,在原來的文章上添寫內容。
定義
樹狀數組或二元索引樹(英語:Binary Indexed Tree/(BIT)),又以其發明者命名爲Fenwick樹,其初衷是解決數據壓縮裏的累積頻率(Cumulative Frequency)的計算問題,現多用於高效計算數列的前綴和, 區間和
。它可以的時間得到任意前綴和,並同時支持在時間內支持動態單點值的修改。空間複雜度。
結構起源
按照Peter M. Fenwick的說法,正如所有的整數都可以表示成2的冪和,我們也可以把一串序列表示成一系列子序列的和。採用這個想法,我們可將一個前綴和劃分成多個子序列的和,而劃分的方法與數的2的冪和具有極其相似的方式。一方面,子序列的個數是其二進制表示中1的個數,另一方面,子序列代表的f[i]的個數也是2的冪。
預備函數——函數
定義一個函數,返回參數轉爲二進制後,最後一個1的位置所代表的數值.
求法:
int lowbit(int x){ return x & (-x);}
列如,返回的是2
關於樹狀數組的理解
藉助 oi wiki 一張圖來描述。
定義樹狀數組
中節點維護原數組的區間和。
即
所以在構造樹狀數組的時候可以前綴和構造。
然後舉個例子,對於,區間可以分解爲三個小區間,每個區間的長度對應着二進制分解後的大小,且每個小區間的長度也等於區間右段點的的值。
該結構滿足如下性質:
1、每個內部節點保存以它爲根的子樹中所有葉節點的和。
2、每個內部節點的子節點個數等於的位數
3、除樹根外,每個內部節點的父節點是
4、樹的深度爲
然後下面是引用知乎上得一句話。
我們知道樹狀數組是可以用來求前綴和的。而線段樹樹求前綴和時是不需要右兒子的,所以把右兒子全部去掉,只保留左區間就是BIT了。
而維護的區間則是[i-lowbit(x)+1,i];其中lowbit爲結點i二進制最低位1的值。而lowbit操作就是這課二叉樹的層,而BIT的實質上也是在Binary上建個tree。所以modify的時候就沿着邊往上爬;求前綴和時就往下爬。
但還有個問題就是,區間修改,然後再單點查詢怎麼辦?這時就在tree上構建差分數組,然後每次change的時候<就只修改[l,+k],[r+1,-k]即可。之後再把要查詢的加起來就是了。————————————————Violetinori
二維樹狀數組
二維樹狀數組常用來維護子矩陣的和。
和一維類似,二維樹狀數組每一維都是樹狀數組。
例:舉個例子來看看C[][]的組成。
設原始二維數組爲:
={{},
{},
{},
{}};
C[1][1]=a11,C[1][2]=a11+a12,C[1][3]=a13,C[1][4]=a11+a12+a13+a14,c[1][5]=a15,C[1][6]=a15+a16,…
這是A[][]第一行的一維樹狀數組
C[2][1]=a11+a21,C[2][2]=a11+a12+a21+a22,C[2][3]=a13+a23,C[2][4]=a11+a12+a13+a14+a21+a22+a23+a24,
C[2][5]=a15+a25,C[2][6]=a15+a16+a25+a26,…
這是A[][]數組第一行與第二行相加後的樹狀數組
C[3][1]=a31,C[3][2]=a31+a32,C[3][3]=a33,C[3][4]=a31+a32+a33+a34,C[3][5]=a35,C[3][6]=a35+a36,…
這是A[][]第三行的一維樹狀數組
C[4][1]=a11+a21+a31+a41,C[4][2]=a11+a12+a21+a22+a31+a32+a41+a42,C[4][3]=a13+a23+a33+a43,…
這是A[][]數組第一行+第二行+第三行+第四行後的樹狀數組
ll c[N+5][N+5];
int n,m;
inline int lowbit(int x) {return x&(-x);}
void add(int i,int j,int val){
for(int x = i;x <= n;x += lowbit(x))
for(int y = j;y <= m;y += lowbit(y))
c[x][y] += val;
}
ll ask(int i,int j){
ll ans = 0;
for(int x = i;x>0;x -= lowbit(x))
for(int y = j;y > 0;y -= lowbit(y))
ans += c[x][y];
return ans;
}
ll sum(int x,int y,int xx,int yy){
x --;y --;
return ask(xx,yy) - ask(xx,y) - ask(x,yy) + ask(x,y);
}
功能
說到底,樹狀數組最主要的作用就是動態維護前綴和。
注意:樹狀數組的下標必須從1開始,,因爲lowbit(0)=0,如果從0開始的話就會陷入死循環!!樹狀數組適用於所有滿足結合律的運算(加法,乘法,異或等)
1、查詢區間前綴和
從上面我們可以知道,在樹狀數組中被分解爲3個小區間,每個小區間爲。而是樹狀數組中的節點。所以要查詢的前綴和,只需要從頂點不斷往下跳累加即可。
int ask(int x){
int ans = 0;
while(x){
ans += c[x];
x -= lowbit(x);
}
return ans;
}
所以我們要求的區間和,區間和滿足區間可減性,即,
2、單點修改,維護前綴和
注意在樹狀數組中包含原數組中的只有及其它的祖先節點。並且任意節點的祖先至多有個。所以我們在點邊修改邊網上跳即可。
void add(int x,int y){//n爲區間上界
for(;x <= n;x += lowbit(x)) c[x] += y;
}
樹狀數組的構造
比較一般的是直接建立一個全爲的數組,然後對每個位置執行,時間複雜度
for(int i = 1;i <= n;++i) add(i,a[i]);
關於的構造上面已經提到了,因爲每個節點以它爲右端點,長度爲的一段區間,所以我們邊維護原數組的前綴和,邊構造。
void init(){
for(int i = 1;i <= n;++ i){
sum[i] = sum[i-1] + a[i];
c[i] = sum[i] - sum[i-lowbit(i)];
}
}
還有個構造
每一個節點的值是由所有與自己直接相連的兒子的值求和得到的。因此可以倒着考慮貢獻,即每次確定完兒子的值後,用自己的值更新自己的直接父親。
void init(){
for(int i = 1;i <= n;++i){
c[i] += a[i];
int j = i + lowbit(i);
if(j <= n) c[j] += c[i];
}
}
樹狀數組黑科技
- 樹狀數組維護區間最值
我們有上述可以發現,樹狀數組又似一種分塊方式,將分爲若干子區間,然後每個節點維護這一段區間區間和,而如果我們讓每個節點維護區間最值(以下假如是最大值),那麼類比於查詢區間和功能,我們改下得:
int ask(int x){
int ans = 0;
while(x){
ans = max(ans,c[x]);
x -= lowbit(x);
}
return ans;
}
但是卻有一個問題,那就是這樣只能查詢的最值,而不能查詢的最值,因爲區間最值不滿足區間可加/減性。但也不妨礙我們使用它,比如在求的過程中,原來做法,因爲每次找前面比他小的最大的都要遍歷一遍,找到這個最大值,注意此處是找的最大值,所以我們完全可以使用樹狀數組來優化這個過程變爲。這樣整體複雜度就降爲了,關於LIS這部分講解,以後應該會放出來吧。
至於網上其他的改法什麼的個人感覺複雜度不大靠譜,還是老老實實學線段樹去吧。
-
求第k小
如果我們把每個值當做下標插入進去,我們很容易求的某個數前面有多少個數小於等於它。在樹狀數組中,節點是根據 2 的冪劃分的
,每次可以擴大 2 的冪的長度。當然這也離不開離散化,我會在最後一道題講解下這個用法。下面這張圖比較直觀的說明了樹狀數組的結構性質。
(圖片來自網絡)
-
區間操作
1、區間修改,單點查詢————樹狀數組維護差分數組
樹狀數組只能查詢前綴和 和 單點修改,我們可以通過設立一個差分數組,
並用樹狀數組維護,那麼區間修改就變爲了單點修改。如在的區間內增加一
個,那麼。這就完成了差分數組的維護,若單點查
詢x處 的值,只需。
2、區間修改,區間查詢————兩個樹狀數組維護(可以用來完成動態區間 加 等差數列的維護)
由上面我們得知,用樹狀數組進行區間修改就是維護一個差分數組,對於位置
處增加的值就是,即,那麼對於原數組,數組的前綴和
總體增加的值就是
即我們可以用兩個樹狀數組維護上述兩個前綴和。。(其中前一個是差分數組,後面是i*差分數組)
具體的說,我們建立兩個樹狀數組c0,c1,起初全部賦值爲0.對於每條指令 C l r d 執行四個操作
1、在樹狀數組c0中把位置 l 上的加上d
2、在樹狀數組c0中,把位置r+1 上的位置減去d
3、在樹狀數組c1中,在位置l 上加上l * d
4、在樹狀數組c1中,在位置r+1上的數減去(r+1)*d
另外,我們建立數組S存儲a的原始前綴和。所以對於每條指令 Q l r爲:
雖然這樣區間修改區間維護這個做法比較麻煩,不如直接用線段樹,不過,有時候動態區間加等差數列會產生比較巧妙的用處。
附上算法競賽進階指南上的代碼
const int SIZE = 100010;
int a[SIZE],n,m;
ll c[2][SIZE],S[SIZE];
int lowbit(int x){return x&(-x);}
ll ask(int k,int x){
ll ans = 0;
for(;x > 0;x -= lowbit(x)) ans += c[k][x];
return ans;
}
void add(int k,int x,int y){
for(;x <= n;x += lowbit(x)) c[k][x] += y;
}
int main(){
cin >> n >> m;
for(int i = 1;i <= n;++i) scanf("%d",&a[i]),S[i] = S[i-1] + a[i];
while(m --){
char op[3];int l,r,d;
scanf("%s%d%d",op,&l,&r);
if(op[0] == 'C'){
scanf("%d",&d);
add(0,l,d);add(0,r+1,-d);
add(1,l,l*d);add(1,r+1,-(r+1)*d);
}
else {
ll ans = S[r] + (r+1)*ask(0,r) - ask(1,r);
ans -= S[l-1] + l *ask(0,l-1) - ask(1,l-1);
printf("%lld\n",ans);
}
}
}
離散化
一些問題用樹狀數組解決需要離散化,比如給兩個數組,這是一個二維偏序,然後讓你根據題意維護前綴和。但是需要注意的是,時間是一種全序全系,就是比晚。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=100000;
int a[maxn];
int b[maxn];
int n;
int main()
{
int m=0;
cin>>n;//元素個數
for(int i=1;i<=n;++i)
{
scanf("%d",&a[i]);
b[i]=a[i];//b[]作爲離散化的數組
}
sort(b+1,b+1+n);//將b數組排序,因爲是從b[1]開始存儲的,所以要b+1
m=unique(b+1,b+1+n)-b-1;//去重操作,返回不同元素的個數
for(int i=1;i<=n;++i)
a[i] = lower_bound(b+1,b+1+m,a[i]) - b;//映射
return 0;
}
例題講解
例題:給你個數,,然後對於每一個讓你求前面有多少值小於它。
思路:
題目說每一個前面有多少元素小於它,位置是一個偏序,且是個全序關係。
這裏我們維護的是元素數目的前綴和。插入時,把每個元素的當做下標,值爲1。
我們正序遍歷,然後對於每個,我們先查詢小於的前綴和,然後在執行插入。
但是的範圍很大,我們不關心的大小,我們在乎的是之間的大小關係。所以我們將離散化,就是將這些值映射爲一組排名。
poj2299
題意:讓你求有多少逆序對。
思路:相比於上個例題,這個是讓求後面有多少個元素小於他,所以我們倒序插入處理,做法類似於上面。
注意開long long。。。
ll c[N];int n;
inline int lowbit(int x){return x & (-x);}
ll ask(int x){
ll ans = 0;
while(x){
ans += c[x];
x -= lowbit(x);
}
return ans;
}
void add(int x,int y){
for(;x <= n;x += lowbit(x)) c[x] += y;
}
int a[N],b[N];
int main(){
while(scanf("%d",&n) == 1&&n){
memset(c,0,sizeof c);
for(int i = 1;i <= n;++i) scanf("%d",&a[i]),b[i] = a[i];
sort(b + 1,b + n + 1);
int m = unique(b+1,b+n+1) - b - 1;
for(int i = 1;i <= n;++i) a[i] = lower_bound(b+1,b+m+1,a[i]) - b;
ll ans = 0;
for(int i = n;i >= 1;i --){
ans += ask(a[i]-1);
add(a[i],1);
}
printf("%lld\n",ans);
//bug;
}
}
在跑了接近800ms,接下來講講memset的時間戳優化數組清0。
什麼是時間戳?
時間戳就是記錄這是第幾次調用樹狀數組的標誌,在修改時如果發現當前的節點的時間戳不在當前時間就提前清零,如果詢問時發現當前的節點的時間戳不在當前時間顯然需要忽視這個節點,這樣的操作類似於一種lazy思想——當修改時才清除當前節點。
這樣的話在多次需要樹狀數組時就可以減少大量的不必要操作,起到十分顯著的優化效果。——————CHN_JZ
int T;//時間戳
int time[N];//儲存節點的訪問時間
ll c[N];int n;
inline int lowbit(int x){return x & (-x);}
ll ask(int x){
ll ans = 0;
while(x){
ans += (time[x]<T?0:c[x]);
x -= lowbit(x);
}
return ans;
}
void add(int x,int y){
for(;x <= n;x += lowbit(x)) {c[x] = time[x] < T?y:c[x] + y;time[x] = T;}
}
完整代碼
int T;//時間戳
int time[N];//儲存節點的訪問時間
ll c[N];int n;
inline int lowbit(int x){return x & (-x);}
ll ask(int x){
ll ans = 0;
while(x){
ans += (time[x]<T?0:c[x]);
x -= lowbit(x);
}
return ans;
}
void add(int x,int y){
for(;x <= n;x += lowbit(x)) {c[x] = time[x] < T?y:c[x] + y;time[x] = T;}
}
int a[N],b[N];
int main(){
while(scanf("%d",&n) == 1&&n){
T ++;
for(int i = 1;i <= n;++i) scanf("%d",&a[i]),b[i] = a[i];
sort(b + 1,b + n + 1);
int m = unique(b+1,b+n+1) - b - 1;
for(int i = 1;i <= n;++i) a[i] = lower_bound(b+1,b+m+1,a[i]) - b;
ll ans = 0;
for(int i = n;i >= 1;i --){
ans += ask(a[i]-1);
add(a[i],1);
}
printf("%lld\n",ans);
}
}
有點尷尬的是不知道爲什麼這樣反而慢了幾十ms?應該是遠古oj太破了吧。。。
Codeforces Round #624 (Div. 3)F
題意:
有個點在軸上,初始座標值分別爲,然後每個點都有一個速度,定義是經過秒後,兩個點的最小距離。讓你求。
思路:
我們考慮對答案的貢獻。顯然對於,不失一般性設,如果,的話兩個點遲早會相遇,也就是,只有,纔有有貢獻,且貢獻爲。
樹狀數組 +離散化
我們按從小到大排序,對每個點,我們只需要查詢比的點且小於等於的點,那麼答案的貢獻是比他小的點的個數,其中爲小於等於的的和值。
struct node{
int a,b;
}res[N];
bool cmp(node A,node B){
return A.a < B.a
}
int b[N];ll c[N][2];
int n;
void add(int x,int num){
for(;x <= n;x += x&(-x)) c[x][0] ++,c[x][1] += num;
}
ll ask(int x,int k){
ll ans = 0;
while(x > 0){
ans+= c[x][k];
x -= x&(-x);
}
return ans;
}
int main(){
n = read();
rep(i,1,n) res[i].a = read();
rep(i,1,n) b[i] = res[i].b = read();
sort(res+1,res+n+1,cmp);
sort(b+1,b+n+1);
int m = unique(b+1,b+n+1) - b - 1;
ll ans = 0;
rep(i,1,n){
int x = lower_bound(b + 1,b +m + 1,res[i].b) - b;
ans += res[i].a*ask(x,0) - ask(x,1);
add(x,res[i].a);
}
cout << ans;
}
poj2352
題意:在一個二維座標系中存在着若干點,一個點的等級是這個點左下角點的數目。問你從到級分別有多少個點。
思路:
這顯然是個二維偏序,我們固定軸,按軸查詢。即先按從小到大排序(相同按軸從小到大排序),然後按照上面題目的思路,先查詢,在插入。
注意:樹狀數組下標從1開始,所以對於可能出現不合法的情況可以偏移座標軸。
struct node{
int x,y;
}res[N];
bool cmp(node A,node B){
if(A.y!=B.y) return A.y < B.y;
else return A.x < B.x;
}
int c[N];int n;
inline int lowbit(int x) {return x & (-x);}
int ask(int x){
int ans = 0;
while(x){
ans += c[x];
x -= lowbit(x);
}
return ans;
}
void add(int x,int y){
for(;x <= N;x += lowbit(x)) c[x] += y;
}
int ans[N];
int main(){
n = read();
rep(i,1,n) res[i].x = read()+1,res[i].y = read();
sort(res + 1,res + n +1,cmp);
rep(i,1,n){
ans[ask(res[i].x)] ++;
add(res[i].x,1);
}
rep(i,0,n-1) printf("%d\n",ans[i]);
}
注意:這道題目重複的數不算排名,所以對於每一種數,我們只插入一次就好。直接看代碼吧。
int c[N];
int m;
void add(int x,int y){
for(;x <= m;x += x & (-x)) c[x] += y;
}
int ask(int x){
int sum = 0;
while(x){
sum += c[x];
x -= x&(-x);
}
return sum;
}
int a[N],b[N];
int kth(int x){//倍增求第k小
int t = 0;
for(int i = 19;i>=0;-- i){
t += 1<<i;
if(t>m||c[t]>=x) t -= 1<<i;
else x -= c[t];
}
return b[t+1];
}
int main(){
int n = read(),k = read();
rep(i,1,n) a[i] = b[i] = read();
sort(b+1,b+n+1);//離散化
m = unique(b+1,b+n+1) - b - 1;
rep(i,1,m) add(i,1);
if(ask(m)<k) puts("NO RESULT");//看總排名有多少
else cout << kth(k);
}
編者注:以上爲博主參考諸多網上資料所總結--by K