線段樹和樹狀陣列

2022-07-26 08:39:15 字數 4456 閱讀 6829

線段樹 (segment tree) 和樹狀陣列是兩種常用的資料結構。他們用來維護乙個區間內的操作,可以在 \(logn\) 的複雜度上進行查詢和修改。

線段樹可以維護對乙個區間的查詢和修改,可以對區間進行分塊查詢,而樹狀陣列是線段樹的閹割版,經常用來區間查詢,但修改只能進行單點修改,經過改造之後可以區間修改,區間樹本身就可以支援區間修改。使用樹狀陣列的原因是因為樹狀陣列比較好寫。

樹狀陣列:

區間樹:

可以看到區間樹是父親節點維護子節點的資訊,到了葉子結點才是具體的某乙個值。

樹狀陣列的構建則很獨特,他的結點維護資訊的數量是由其結點轉化成二進位制之後最左邊的1的個數之後的零的個數決定的。

c8 = c4 + c6 + c7 + a8

c4 = c2 + c3 + a4

c6 = c5 + a6

c7 = a7

c2 = c1 + a2

c3 = a3

c5 = a5

c1 = a1

\(8_ = 1000_\)

\(6_ = _2\)

所以我們可以看到,\(c8\) 結點維護了 \(2^3\) 個結點資訊,\(c_6\) 結點維護了 \(2^1\) 個結點資訊。

構建樹狀陣列不需用進行構建,可以把樹的構建當作是樹的修改。

修改操作(單點修改)

樹狀陣列的修改即修改維護這個節點資訊的所有節點,更新與這個節點有關的所有節點即完成了節點的修改。

那麼問題變成了如何尋找到與這個節點有關的上乙個節點,只要找到上乙個結點,那麼我們就可以進行遞迴修改,從而完成對所有相關節點的修改。

而結點上尋可以看成對原來的數以二進位制的形式加上除去除了最低位的 1 以外的二進位制數後得到的結果。

若 \(a_1a_2a_3...a_i..a_j\) 為原來數的二進位制表達形式,\(a_i\) 為最低位的1,那麼其上尋節點應該為\(a_1a_2a_3...a_i..a_j + a_i..a_j\)。

比如從圖中我們可以看到,\(c4\)的上乙個結點是\(c8\),根據這個上尋規則 \(100_2\) 加上 \(100_2\) 得到\(1000_2\)。

\(c6\) 的上乙個結點是\(c8\),根據上尋規則 \(110_2\) 加上 \(10_2\),得到\(1000_2\)。

\(c7\) 的上乙個結點是\(c8\),根據規則 \(111_2\) 加上 \(1_2\) 得到\(1000_2\)。

那麼問題就變成了如何保留某乙個數的最低位的1,把其餘所有的1去除。

這裡,我們可以引入乙個函式 lowbit(x)

int lowbit(x)
這個函式的作用可以只保留 x 的最低位的1,將其高位的1去除,具體原因是計算機中的資料儲存按照補碼規則進行儲存,可以推出。

比如\(lowbit(4) = lowbit((100)_2) = 100_2 = 4_\)

\(lowbit(5) = lowbit((101)_2) = 1_2\)

\(lowbit(6) = lowbit((110)_2) = 10_2 = 2_\)

這樣,我們就完成了整個的修改操作

int add(int x, int k)

}

這裡的 n 表示整個陣列的長度。

查詢操作 區間查詢

樹狀陣列的父親節點維護的資訊是一段字首和的資訊,如果需要進行區間查詢,那麼利用字首和也可以求出區間查詢的結果。

查詢操作和修改操作相反,我們需要不斷查詢子節點,直到子節點為0。

如何求解子節點可以看成如何查詢父親節點的逆操作,父親節點為加上lowbit值,那麼子節點即為減去lowbit的值。

int sum(int x)

return ans;

}

區間修改+單點查詢

區間修改的操作,可以把樹狀陣列維護的前i項和看成第i個數,那麼對 \([x,y]\) 的區間修改,可以看成對第x個位置和第 y+1 的位置進行修改。

這樣之後進行單點查詢,即詢問某個位置的值。如果查詢範圍在 \([0, x]\) 之間 或 \([y, +\infty]\),即為原來的數。如果查詢範圍在 \([x, y]\) 之間,因為對第x個位置進行了更改,所以字首和之後即可滿足條件。

構建區間樹需要我們進行建樹操作,我們可以觀察一下區間樹的構成。

可以看到區間樹的父親節點維護的區間是左右兒子區間的並集,左右兒子節點的劃分是父親節點的中間元素,所以,我們可以採用這種方式進行遞迴建樹。區間樹的資訊是由兩個子節點的資訊決定的,所以,我們需要在兩個子節點建好之後,維護父親節點的資訊。

先定義兩個函式,獲取父親節點的兩個兒子節點

int leftchild(int p)

int rightchild(int p)

void build(int p, int left, int right) 

int mid = (left + right) / 2;

build(leftchild(p), left, mid);

build(rightchild(p), mid+1, right);

push_up(p);

}

上面的push_up()操作是用來維護父親節點資訊,這個資訊可以是由兩個子區間決定的區間和,區間最值等資訊。但這個資訊必須滿足結合律。這裡我們使用維護區間和。

void push_up(int p)
區間修改操作

對於線段樹而言,單點修改和區間修改沒有什麼具體的差別,無非是區間長度不一樣而已,對於線段樹,我們可以引入懶標記的操作,沒有懶標記之前,我們需要進行區間維護需要先遞迴到葉子節點,然後向上依次維護父親節點。而懶標記的意義在於他不是更新到每乙個具體的葉子節點,而是先記錄在部分區間的公共父親節點上。然後需要更新的時候再更新。需要更新的時機主要是在什麼時候會用到子節點資訊,如果需要用到子節點資訊,那麼我們需要進行將lazy tag進行下放,保證了子節點資訊的一致。

所以,如果當前區間已經被更新區間完全覆蓋,那麼我們不用對這個區間繼續深入到各個子節點,可以直接在父親節點完成對區間維護,如果當前區間被更新區間部分覆蓋,那麼我們就對父親節點的兩個子節點進行部分維護即可,在維護兩個子節點的時候,因為父親節點的lazy tag記錄著上次的更新資訊,所以,我們需要將父親節點的lazy tag下降到兩個子節點,更新兩個子節點的資訊後,才能對兩個子節點進行這次的更新操作。不然,可能會出現資料不一致問題。

void push_down(int p, int left, int right)
void update(int updatel, int updater, int left, int right, int p, int k) 

//此時被更新的區間部分覆蓋當前區間

push_down(p, l, r); // 因為要對子節點進行更新,所以把當前的父親節點的 lazy tag 向下進行傳遞

int mid = (left + right) / 2;

if (updatel <= mid)

if (updater > mid)

//重新維護父親節點

push_up(p);

}

區間查詢

區間查詢就是對指定的區間進行查詢,如果指定的區間完全覆蓋了當前父親節點的區間,就可以直接返回父親節點的資訊,避免進一步的查詢。而如果查詢區間部分覆蓋了當前父親節點,那麼我們需要查詢的就是子節點資訊,需要把父親節點的lazy tag進行下放,更新子節點的資訊。然後進行子節點查詢

int query(int queryleft, int queryright, int p, int left, int right) 

int mid = (left + mid) / 2;

push_down(p, left, right);

if (queryleft <= mid)

//需要查詢左兒子節點

res += query(queryleft, queryright, leftchild(p), left, mid);

if (queryright > mid)

//需要查詢右兒子節點

res += query(queryleft, queryright, rightchild(p), mid+1, right);

return res;

}

以上就是樹狀陣列和線段樹的兩個操作,他們都可以在 \(nlogn\) 的時間下完成區間的查詢。

樹狀陣列和線段樹

主要解決兩個問題 其他問題可以轉化 更新某一點的值 求區間值 時間按複雜度 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...

樹狀陣列和線段樹(未完)

線段樹是一種二叉搜尋樹,與區間樹相似,它將乙個區間劃分成一些單元區間,每個單元區間對應線段樹中的乙個葉結點。效能 於tsinghua online judge,侵刪 題目描述 魔術師將一疊撲克順次在桌上排成一行,全部正面朝上。之後的每一次揮一揮衣袖,都會翻轉一連串的撲克,改變它們的正反朝向。從古代傳...