稀疏表 樹狀陣列和線段樹

2022-08-28 19:42:13 字數 3690 閱讀 3824

區間問題,例如區間求和、區間最值,有很多資料結構可以選擇。稀疏表、樹狀陣列和線段樹就是解決區間問題比較套路的方法,只要理解了這些資料結構的特徵並且掌握了**模版,遇到可建模成區間求和和區間最值的問題,就可以輕鬆解決了。

稀疏表經常用來處理離線多輪rmq(區間最值)問題。之所以強調多輪,因為多輪的情況下稀疏表的預處理時間$(nlgn)$才能被攤還出去,試想一下只為一次的區間最值查詢建立乙個稀疏表,未免給人雷聲大雨點小的感覺。

先從最簡單暴力的思路開始。因為是離線的問題,可以用乙個矩陣來儲存答案,$matrix[m][n]$為區間$[m, n]$的最值,這樣用起來直接查詢就行了。預處理的時間為$o(n^2)$,查詢時間為$o(1)$,空間複雜度為$o(n^2)$。預處理比較慢且浪費空間,是不是有其他的手段降低一下這兩塊的開銷?

區間最值問題是不是可以通過求解子區間的最值然後整合出結果?顯然可以,這樣我們就沒必要直接給出區間的最值,給出子區間的就可以了,可以壓縮狀態。以區間極小值為例,我們定義$d(a)$表示區間$a$上的最小值,假設有n個區間$a_1,a_2,.....a_n$,滿足對$\forall i:1\leq i\leq n, a_i\cup a=a$,並且$\bigcap_^a_i = a$,那麼$d(a)$可以通過$min(d(a_i),1\leq i\leq n)$得到。可以看出:在最值問題中,子區間的並集要為當前區間,但是對子區間之間的交集並無要求。

最直觀的想法當然是將區間二分,查詢的時候分別求左右區間的最值,就可以得到了這個區間的最值,用$d(l, r)$表示區間$[l, r)$的最值,$min(l, r)=min\, m=(l+r)/2$。發現這一通操作只是稍微改善了一下複雜度係數,數量級並沒有改變。既然對子區間之間的交集沒有要求,那狀態是不是可以繼續壓縮,比如說$d(1,5)=min\;d(1,6)=min\$,這樣$d(1,4)$就可以被復用多次了。怎麼設定才合理呢?既然$o(n^2)$的複雜度不行,那就往$o(nlgn)$考慮一下?乙個節點平均儲存$o(lgn)$個的區間,到這裡差不多大家心裡已經有答案了吧:只需要維護2的冪次長度的區間就可以了。

定義$st(i,j)$表示以$i$開始的,長度為$2^j$的區間的最值,$st(i,j)$可以通過一下公式得到:

$$st(i,j)=min\, j-1)\}$$

用上面的遞推關係進行預處理,查詢的時候用下面的式子:

$$d(l,r)=min\;k=\left \lfloor log(r-l+1) \right \rfloor$$

1

class

sparsetable(object):

2def

__init__

(self, nums: list):

3 size, row, col =len(nums), len(nums), 0

4while (1 << (col + 1)) <=size:

5 col += 1

6 self.st = [[0] * col for _ in

range(row)]

7for i in

range(row):

8 self.st[i][0] =nums[0]

9for j in range(1, col):

10for i in

range(row):

11if i + (1 << j) >size:

12break

13 self.st[i][j] = min(self.st[i][j - 1], self.st[i + (1 << (j - 1))][j - 1])

1415

defquery(self, left: int, right: int):

16 k =0

17while (1 << (k + 1)) <= right - left + 1:

18 k += 1

19return min(self.st[left][k], self.st[right - (1 << k) + 1][k])

當問題滿足和rmq問題一樣的區間特性的時候,就可以考慮用稀疏表求解:

樹狀陣列也是乙個區間長度為2的冪次的一種資料結構,和稀疏表不同的是,位置為$i$的節點只維護一段長度為$lowbit(i)$,範圍為$[i - lowbit(i) + 1, i]$的一段區間。翻譯成樹狀陣列好像並不是很貼切,可能只是把這個東西抽象成樹更好理解一點。

樹狀陣列的**比較定式,理解記憶都比較簡單,這裡以求和為例,單獨用一小節整理一下**,下面的例子均可套用。

1

class

bit:

2def

__init__

(self, arr: list[int]):

3 self.arr =arr

4 self.bitree = [0] * (len(arr) + 1) #

下標為0的位置沒有意義5#

建立樹狀陣列

6for index, value in

enumerate(arr):

7self.update(index, value)89

defupdate(self, index: int, delta: int):

10"""

11index對應的是arr的下標,delta是增量值

12"""

13 self.arr[index] +=delta

14 pos = index + 1

15while pos 16 self.bitree[pos] +=delta

17 pos +=self._lowbit(pos)

1819

def sum(self, start: int, end: int) ->int:

20"""

21計算arr陣列中[start, end)區間的和

22"""

23return self._sum(end) -self._sum(start)

2425

def _sum(self, pos: int) ->int:

26"""

27計算arr陣列中[0, pos)區間的和

28"""

29 res =0

30while pos >0:

31 res +=self.bitree[pos]

32 pos -=self._lowbit(pos)

33return

res34

35def _lowbit(self, pos: int) ->int:

36return pos & (-pos)

2.3.1 單點更新,區間查詢

2.3.2 區間更新,單點查詢

2.3.3 區間更新,區間查詢介紹lazy思想:lazy-tag思想,記錄每乙個線段樹節點的變化值,當這部分線段的一致性被破壞我們就將這個變化值傳遞給子區間,大大增加了線段樹的效率。

pushup(rt):通過當前節點rt把值遞迴向上更新到根節點

pushdown(rt):通過當前節點rt遞迴向下去更新rt子節點的值

3.2.1 單點更新,區間查詢

3.2.1 區間更新,區間查詢

樹狀陣列和線段樹

主要解決兩個問題 其他問題可以轉化 更新某一點的值 求區間值 時間按複雜度 logn 原陣列a 1 a 2 a n 寫成樹狀陣列c c x x lowbit x x 左開右閉 筆記 主要 const int n int tr n int lowbit int x void add int x,int...

線段樹和樹狀陣列

引入1 有n個數 n 50000 個數,m m 50000 次詢問。每次詢問區間l到r的數的和。要求輸出每一次詢問的結果.分析 1.用字首和問題進行求解 再開乙個陣列 暫且記為b n 設n個數所組成的陣列為a n b i 用來記錄從a 1 到a i 的所有數字的和 即 b 1 a 1 b 2 b 1...

線段樹和樹狀陣列

線段樹 segment tree 和樹狀陣列是兩種常用的資料結構。他們用來維護乙個區間內的操作,可以在 logn 的複雜度上進行查詢和修改。線段樹可以維護對乙個區間的查詢和修改,可以對區間進行分塊查詢,而樹狀陣列是線段樹的閹割版,經常用來區間查詢,但修改只能進行單點修改,經過改造之後可以區間修改,區...