樹狀陣列詳解

2022-01-10 10:07:18 字數 4282 閱讀 7928

樹狀陣列和下面的線段樹可是親兄弟了,但他倆畢竟還有一些區別:

樹狀陣列能有的操作,線段樹一定有;

線段樹有的操作,樹狀陣列不一定有。

這麼看來選擇 線段樹 不就「得天下了」

事實上,樹狀陣列的**要比線段樹短得多,思維也更清晰,在解決一些單點修改的問題時,樹狀陣列是不二之選。

如果要具體了解樹狀陣列的工作原理,請看下面這張圖:

最下面的八個方塊 (標有數字的方塊) 就代表存入 \(a\) 中的八個數,現在都是十進位制。

他們上面的參差不齊的剩下的方塊就代表 \(a\) 的上級—— \(c\) 陣列。

很顯然看出:

\(c[2]\) 管理的是 \(a[1]\) & \(a[2]\) ;

\(c[4]\) 管理的是 \(a[1]\) & \(a[2]\) & \(a[3]\) & \(a[4]\) ;

\(c[6]\) 管理的是 \(a[5]\) & \(a[6]\) ; \(c[8]\) 則管理全部 \(8\) 個數。

所以,如果你要算區間和的話,比如說要算 \(a[51]\) ~ \(a[91]\) 的區間和,暴力算當然可以,那上百萬的數,那就 re 嘍。

那麼這種類似於跳一跳的連續跳到中心點而分值不斷變大的原理是一樣的(倍增)。

你從 \(91\) 開始往前跳,發現 \(c[n]\) ( \(n\) 我也不確定是多少,算起來太麻煩,就意思一下)只管 \(a[91]\) 這個點,那麼你就會找 \(a[90]\) ,發現 \(c[n - 1]\) 管的是 \(a[90]\) & \(a[89]\) ;那麼你就會直接跳到 \(a[88]\) , \(c[n - 2]\) 就會管 \(a[81]\) ~ \(a[88]\) 這些數,下次查詢從 \(a[80]\) 往前找,以此類推。

那麼問題來了,你是怎麼知道 \(c\) 管的 \(a\) 的個數分別是多少呢?你那個 \(1\) 個, \(2\) 個, \(8\) 個……是怎麼來的呢?

這時,我們引入乙個函式——lowbit

int lowbit(int x)
lowbit的意思注釋說明了,咱們就用這個說法來證明一下 \(a[88]\) :

\(88_=1011000_\)

發現第乙個 \(1\) 以及他後面的 \(0\) 組成的二進位制是 \(1000\)

\(1000_ = 8_\)

\(1000\) 對應的十進位制是 \(8\) ,所以 \(c\) 一共管理 \(8\) 個 \(a\) 。

這就是lowbit的用處,僅此而已(但也相當有用)。

你可能又問了:x & -x 是什麼意思啊?

在一般情況下,對於int型的正數,最高位是 0,接下來是其二進位制表示;而對於負數 (-x),表示方法是把 x 按位取反之後再加上 1 (補碼知識)。

例如 :

\(x =88_=01011000_\) ;

\(-x = -88_ = (10100111_ + 1_) =10101000_\) ;

\(x\ \& \ (-x) = 1000_ = 8_\) 。

那麼對於單點修改就更輕鬆了:

void add(int x, int k) 

}

每次只要在他的上級那裡更新就行,自己就可以不用管了。

int getsum(int x) 

return ans;

}

若維護序列 \(a\) 的差分陣列 \(b\) ,此時我們對 \(a\) 的乙個字首 \(r\) 求和,即 \(\sum_^ a_i\) ,由差分陣列定義得 \(a_i=\sum_^i b_j\)

進行推導

\[\sum_^ a_i\\=\sum_^r\sum_^i b_j\\=\sum_^r b_i\times(r-i+1)

\\=\sum_^r b_i\times (r+1)-\sum_^r b_i\times i

\]區間和可以用兩個字首和相減得到,因此只需要用兩個樹狀陣列分別維護 \(\sum b_i\) 和 \(\sum i \times b_i\) ,就能實現區間求和。

**如下

int t1[maxn], t2[maxn], n;

inline int lowbit(int x)

void add(int k, int v)

}int getsum(int* t, int k)

return ret;

}void add1(int l, int r, int v)

long long getsum1(int l, int r)

/* ------------另一種寫法 ------------*/

//樹狀陣列 2:區間修改,單點查詢 模板ac**

#include using namespace std;

#define lowbit(x) (x & -x)

typedef long long ll;

const int maxn = 1e6 + 10;

ll n, q, tr[maxn], a, pre;

void add(int i, int v)

ll getsum(int i)

int main() else

cout << getsum(u) << endl;}}

注釋 ①:因為維護的是差分陣列。

區間 [l,r] 加 v 就相當於在差分陣列的 l 位置加 v ,在 r + 1 位置 -v

維護的是差分陣列的字首資訊\(( \sum_^i 和 \sum_ ^i )\)

\(o(n)\) 建樹:

每乙個節點的值是由所有與自己直接相連的兒子的值求和得到的。因此可以倒著考慮貢獻,即每次確定完兒子的值後,用自己的值更新自己的直接父親。

// o(n)建樹

void init()

}

\(o(\log n)\) 查詢第 \(k\) 小/大元素。在此處只討論第 \(k\) 小,第 \(k\) 大問題可以通過簡單計算轉化為第 \(k\) 小問題。

參考 "可持久化線段樹" 章節中,關於求區間第 \(k\) 小的思想。將所有數字看成乙個可重集合,即定義陣列 \(a\) 表示值為 \(i\) 的元素在整個序列重出現了 \(a_i\) 次。找第 \(k\) 大就是找到最小的 \(x\) 恰好滿足 \(\sum_^a_i \geq k\)

因此可以想到演算法:如果已經找到 \(x\) 滿足 \(\sum_^a_i \le k\) ,考慮能不能讓 \(x\) 繼續增加,使其仍然滿足這個條件。找到最大的 \(x\) 後, \(x+1\) 就是所要的值。

在樹狀陣列中,節點是根據 2 的冪劃分的,每次可以擴大 2 的冪的長度。令 \(sum\) 表示當前的 \(x\) 所代表的字首和,有如下演算法找到最大的 \(x\) :

求出 \(depth=\left \lfloor log_2n \right \rfloor\)

計算 \(t=\sum_^}a_i\)

如果 \(sum+t \le k\) ,則此時擴充套件成功,將 \(2^\) 累加到 \(x\) 上;否則擴充套件失敗,對 \(x\) 不進行操作

將 \(depth\) 減 1,回到步驟 2,直至 \(depth\) 為 0

//權值樹狀陣列查詢第k小

int kth(int k)

return ret + 1;

}

時間戳優化:

對付多組資料很常見的技巧。如果每次輸入新資料時,都暴力清空樹狀陣列,就可能會造成超時。因此使用 \(tag\) 標記,儲存當前節點上次使用時間(即最近一次是被第幾組資料使用)。每次操作時判斷這個位置 \(tag\) 中的時間和當前時間是否相同,就可以判斷這個位置應該是 0 還是陣列內的值。

//權值樹狀陣列查詢第k小

int kth(int k)

return ret + 1;

}

文章開源在 github - blog-articles,點選 watch 即可訂閱本部落格。 若文章有錯誤,請在 issues 中提出,我會及時回覆,謝謝。

如果您覺得文章不錯,或者在生活和工作中幫助到了您,不妨給個 star,謝謝。

(文章完)

樹狀陣列 詳解

對於普通陣列,其修改的時間複雜度位o 1 而求陣列中某一段的數值和的時間複雜度為o n 因此對於n的值過大的情況,普通陣列的時間複雜度我們是接受不了的。在此,我們引入了樹狀陣列的資料結構,它能在o logn 內對陣列的值進行修改和查詢某一段數值的和。假設a陣列為儲存原來的值得陣列,c為樹狀陣列。我們...

樹狀陣列詳解

樹狀陣列求區間和的一些常見模型 樹狀陣列在區間求和問題上有大用,其三種複雜度都比線段樹要低很多 有關區間求和的問題主要有以下三個模型 以下設a 1.n 為乙個長為n的序列,初始值為全0 1 改點求段 型,即對於序列a有以下操作 修改操作 將a x 的值加上c 求和操作 求此時a l.r 的和。這是最...

樹狀陣列詳解

比如說,我這裡有一組數1,2,3,2,k。我想知道第i到第j的和 mathop sum limits j v i 是多少?樸素演算法 for int k 0 k n k if k i k j ans v k 類似這種的寫法,雖然在某些點值改變時也依然可以計算 我們稱這種問題為動態問題 但複雜度最高到...