關於樹狀陣列一些有意思的東西

TA201314發表於2016-04-28

嘛~最近剛剛學會樹狀陣列,寫個blog記錄一下心得。

樹狀陣列呢,核心是一個叫lowbit的東西,lowbit(x)=x&-x=x的最後一位1的大小。
一、一個經典問題

一個初始值為0的k位計數器,要求支援n次+1操作。時間複雜度?

經典解法:

法I:考慮第i位的改變次數,可得O(k1i=0n2i)O(i=0n2i)=O(n)

O(\sum_{i=0}^{k-1}{n\over 2^i})\le O(\sum_{i=0}^{\infty}{n\over 2^i})=O(n)

法II:考慮計數器中1的數量,顯然每次只會增加1個(減少若干個),所以時間複雜度O(n)
O(n)

法III:考慮勢能函式f(T)=計數器中1的數量,則顯然單次操作均攤O(1)
O(1)

奇怪向做法:

對於x,考慮大於x的第一個y使得y&lowbit(x)==0(lowbit(x)第一次被進位),顯然y=x+lowbit(x)。所以由x向x+lowbit(x)連一條邊。這樣的話顯然會形成一棵樹,計數器操作的代價就是點數+邊數等於O(n)

O(n)

這棵樹就是樹狀陣列啦!
二、樹狀陣列中的一些基本關係
x的父親是x+lowbit(x)。
x的子樹是(x-lowbit(x),x],(即所有能通過若干次+lowbit到達x的節點集合)。
考慮x一直沿著它父親走,那麼lowbit一定是嚴格單增的,所以樹高是O(log2n)

O(\log_2n)
的。
考慮x的兒子,就是能通過一次+lowbit操作到達x的元素數量,它顯然等於log2lowbit(x)
\log_2 lowbit(x)
,就等於{x2i|2i<lowbit(x)}
\{ x-2^i|2^i<lowbit(x)\}
,所以一個節點的兒子數量也是O(log2n)
O(\log_2n)
的。更直觀的說法是,x的兒子數量其實就等於從x-1 +1(上文中的例題)時被進位的(消失的)1的數量。
三、基本的樹狀陣列怎麼寫?
我們先來考慮一個簡單的問題,就是求區間和。要求支援單點修改,區間詢問。

那麼我們對於每個節點儲存它的子樹的和。
修改每個節點的時候直接沿著父親一路找上去就可以了。

void add(int x,int delta){
    for(;x<=n;x+=x&-x)bit[x]+=delta;
}

查詢的時候我們可以把區間和改為兩個字首和的差。而一個字首和可以拆分成O(log2n)

O(\log_2n)
棵子樹。

int query(int x){
    int ans=0;
    for(;x;x-=x&-x)ans+=bit[x];
    return ans;
}

四、如何初始化樹狀陣列?
比如說我們有一個陣列a,我們要建出它的bit,我們該怎麼做呢?
我以前的做法是把n個數插入進去。

但顯然這是不必要的。

我們可以從1~n遞推,假設推到i時bit[i]已經推出來了,那麼顯然它只需貢獻給bit[i+lowbit(i)]即可。

void build(){
    for(int i=1,x;i<=n;++i){
        bit[i]+=a[i];
        if((x=i+(i&-i))<=n)bit[x]+=bit[i];
    }
}

五、維護最值?
顯然,樹狀陣列維護最值的話,只能支援兩種操作:增大一個位置的數,查詢字首最值。
這看起來非常苛刻,但是其實在很多情況下,都是滿足的。最常見的是bit+掃描線/dp這種的。

但是,如果時間複雜度允許是O(log22n)

O(\log_2^2n)
,樹狀陣列也是可以做到維護最值的。

初始化當然不必說。

void build(){
    for(int i=1,x;i<=n;++i){
        bit[i]=max(bit[i],a[i]);
        if((x=i+(i&-i))<=n)bit[x]=max(bit[x],bit[i]);
    }
}

修改的時候,我們只需要修改x的O(log2n)

O(\log_2n)
個祖先,而每個祖先又有O(log2n)
O(\log_2n)
個兒子。

void update(int x,int A){
    a[x]=A;
    for(int y,i;x<=n;x+=x&-x){
        bit[x]=a[x];
        for(y=x-1,i=1;y&i;y-=i,i<<=1)bit[x]=max(bit[x],bit[y]);
    }
}

查詢[l,r]的時候我們把它分成[l,r]路徑上的點和被完全覆蓋的子樹兩部分,因為[l,r]路徑上只有O(log2n)

O(\log_2n)
個點,所以被完全覆蓋的子樹顯然只有O(log22n)
O(\log_2^2n)
個。

int query(int l,int r){
    int ans=-0x7fffffff;
    --l;
    for(int x;r>l;--r){
        for(;r&&r-(r&-r)>=l;r-=r&-r)ans=max(ans,bit[r]);
        if(r>l)ans=max(ans,a[r]);
    }
    return ans;
}

當然。。這O(log22n)

O(\log_2^2n)
的玩意兒顯然是沒什麼卵用的東西。僅供娛樂~
六、維護字尾?
Q:如何支援單點修改,字尾和查詢?
A:= =這不跟區間查詢一樣麼。
Q:查兩邊字首和?常數太大!不開心。
A:那就把原陣列反過來不就行了麼。
Q:座標什麼的反來反去,很麻煩的。好煩人,不開心!
A:。。。

其實。。我們只需要把修改和詢問改一下下就好了!
先上程式碼:

void build(){
    for(int i=n;i;--i){
        bit[i]+=a[i];
        bit[i-(i&-i)]+=bit[i];
    }
}
void add(int x,int delta){
    for(;x;x-=x&-x)bit[x]+=delta;
}
int query(int x){
    int ans=0;
    for(;x<=n;x+=x&-x)ans+=bit[x];
    return ans;
}

←_←看起來就像是寫殘了的樹狀陣列。。
但為什麼可以這樣搞?!

我們可以將修改看成是在樹上打永久化的標記,查詢就是在收集標記。

但是,我們需要更高逼格的解釋方法。
注意到樹狀陣列中x的父親是x+lowbit(x),而如果x+lowbit(x)>n,那麼其實它的父親是不存在的,就是說其實它是一棵森林。我們在build的時候為了防止陣列越界,還要特判一下,好煩人!
所以我們不妨把x的父親改為x-lowbit(x),這樣就是一棵以0為根的樹啦!這樣的話,x的子樹就是[x,min(n,x+lowbit(x)-1)]。上述程式碼就變得顯而易見了。

相關文章