樹狀陣列基礎

菜雞mk 發表於 2021-07-26

樹狀陣列簡介

如果有哪一種資料結構可以支援區間/單點和的更新和查詢,一個顯而易見的答案就是萬能的線段樹。但是線段樹雖然能支援很多的區間問題,但是程式碼量有些長。如果我們只是單純地為了維護區間和其實並不用去專門構建一棵線段樹。樹狀陣列作為一種更加簡單的,可以維護區間和的資料結構應運而生。

樹狀陣列基本思想

對於陣列\(A\)來說,如果我們要求​Sum(Ai , Aj)的結果,除了使用直接遍歷或者是線段樹,我們還可以為其定義一個陣列\(C\)像下面這樣:

樹狀陣列基礎

然後我們可以簡單地羅列一個c陣列到a陣列到對映關係

A陣列 C陣列
A1 A1
A2 A1+A2
A3 A3
A4 A1+A2+A3+A4
A5 A5
A6 A5+A6
A7 A7
A8 A1+A2+A3+A4+A5+A6+A7+A8
A9 A9

我們可以把c陣列看成一種特殊的字首和。一般的字首和的\(s~i~\)都是代表著\(A~i-A~0\) 的值,但是我們的c陣列每一位代表著的值是像上面這個表寫著的那樣。

那麼剩下的任務就是找規律了。最先可以看到的是對於\(A~i~\)​來說,如果i是奇數的話,那麼有\(C~i~ = A~i~\)​。如果i是偶數的話,我們可以發現這樣一條規律:設x為 i 的二進位制形式中最低位1所代表的值,有\(C~i~ = Sum(A~x~ , A~1~)\)​​​ 。求出x值的函式我們一般稱之為lowbit()函式,寫法如下:

def lowbit(a):
	return a&(-a)

這樣我們就可以完成C陣列的維護。假設我們要在保持c陣列維護著字首和的性質的前提下更改A[1]的值(A[1]=A[1]+x),可以按照這樣的順序對C陣列進行修改:

C[1]=C[1]+x lowbit(1)==1

C[2]=C[2]+x 1+lowbit(1)==2

C[4]=C[4]+x 2+lowbit(2)==4

C[8]=C[8]+x 4+lowbit(4)==8

寫成程式碼就是這樣:

def add(x,pos):
	while pos<=n:
		C[pos]+=x
		pos=pos+lowbit(pos)

區間查詢的思想和單點更新其實差不多(就是單點更新的反向操作),看看程式碼就能理解了:

def query(pos):
	ans=0
	while(pos>0):
		ans+=tree[pos]
		pos=pos-lowbit(pos)
	return ans

樹狀陣列在逆序對上的應用

逆序對問題除了可以通過歸併排序進行合併以外還可以用樹狀陣列進行計算

逆序對問題就是求對於陣列\(A\)來說,存在一組i,j使得i<j且\(A~i~>A~j~\) 我們稱i,j為一組逆序對。逆序對問題通常是用歸併排序的方式來分治計算,同時也可以用樹狀陣列來計算

可以簡單地把逆序對問題看成計算對於每一個數字,它前面有多少個比它大的數字,最後的答案就是對於每個數字的結果的和。首先,我們對輸入資料進行離散化,因為輸入資料大小可能會很大。然後,我們用離散化之後的資料下標構建一個全0樹狀陣列。對於一個數,所有比它大的數字都可以通過計算下標陣列上在他前面的數字的個數。此時就可以通過樹狀陣列來加速計算的過程。

其實用線段樹也可以進行計算,不過從程式碼量上來說還是樹狀陣列更優一些。

樹狀陣列可以被看作一種用來維護結合律支援的區間操作的一種程式碼量和細節都比較少的資料結構。如果要維護某些不支援結合律(比如說區間眾數)之類的值的時候還是好好地打線段樹吧