瓶頸
如何理解樹狀陣列?
這個結構的思想和線段樹有些類似:用一個大節點表示一些小節點的資訊,進行查詢的時候只需要查詢一些大節點而不是更多的小節點。
最下面的八個方塊就代表存入 a 中的八個數,現在都是十進位制。
他們上面的參差不齊的剩下的方塊就代表 a 的上級—— c 陣列。
很顯然看出: c2 管理的是 a1 & a2 ; c4 管理的是 a1 & a2 & a3 & a4 ; c6 管理的是 a5 & a6 ;c8 則管理全部 8 個數。
所以,如果你要算區間和的話,比如說要算 a51 ~ a91 的區間和,暴力算當然可以,那上百萬的數,那就 TLE 嘍。
——————摘自oi-wiki.org
初看這些文字,你可能會想:
“啊這這這???你講這些我們怎麼聽得懂啊,這樹狀陣列是啥,咋用,我們還是懵的啊”
當然,為了解決問題而書寫演算法的話,我們不需要去理解這個結構的原理到底是啥,我們只需要知道這個東西
用在哪?
怎麼用?
就足夠了
用在哪?
我們都知道:
一般的普通陣列單點操作的時間複雜度的O(1)、區間操作的時間複雜度是O(n)。
而我們樹狀陣列的和普通陣列的區別就在於:
單點操作和區間操作的時間複雜度都是O(log n),而且
單點修改和區間操作(加、求和)都需要用函式實現。
那麼這麼說我們大概能理解一點了,那就是:
一旦遇到大規模使用區間求和的問題,我們就可以考慮使用樹狀陣列。
怎麼用?
總得來說就是三個函式:lowbit、新增函式,求和函式
lowbit
int lowbit(int x) {
/*
算出x二進位制的從右往左出現第一個1以及這個1之後的那些0組成數的二進位制對應的十進位制的數
簡單說就是用位運算改變了查詢操作,以契合上述的時間複雜度
*/
return x & -x;
}
單點修改
void add(int x, int k) { //在i位置加上k
while (x <= n) { // 不能越界
c[x] = c[x] + k;
x = x + lowbit(x);
}
}
區間求和
int sum(int x) { // 返回a[1]……a[x]的和
int ans = 0;
while (x >= 1) {
ans = ans + c[x];
x = x - lowbit(x);
}
return ans;
}
就這??就這??
啊啊,看似就這,那我們來找一道模板題做一做,深化一下理解吧。
例題:Acwing 788 逆序對的數量
題面
給定一個長度為n的整數數列,請你計算數列中的逆序對的數量。
逆序對的定義如下:對於數列的第 i 個和第 j 個元素,如果滿足 i < j 且 a[i] > a[j],則其為一個逆序對;否則不是。
輸入
第一行包含整數n,表示數列的長度。
第二行包含 n 個整數,表示整個數列。
輸出
輸出一個整數,表示逆序對的個數。
PS: 1≤n≤1000001≤n≤100000
輸入樣例:
6
2 3 4 5 6 1
輸出樣例:
5
解題過程
“逆序對”的計算需要用到大量的區間運算,在這個時候我們的樹狀陣列就發揮了很大的用處了,
對於這道題的核心思想,即是:
用陣列的值作為下標,每次出現逆序對則給該下標對應值加一,最後求和
程式碼如下
#include <iostream> #include <cstdio> #include <algorithm> #include <cstring> using namespace std; typedef long long LL; const int N = 100010; int n; int a[N]; int tr[N]; int lowbit(int x) { return x & -x; } void add(int x, int k) { for (int i = x; i < N - 1; i += lowbit(i)) tr[i] += k; } LL sum(int x) { LL res = 0; for (int i = x; i; i -= lowbit(i)) res += tr[i]; return res; } int main() { scanf("%d", &n); for (int i = 0; i < n; i ++) scanf("%d", &a[i]); LL ans = 0; for (int i = n - 1; i >= 0; i --) {//倒序讀入 int t = a[i];//讀入的值作為下標 ans += sum(t - 1);//若比a[i]小的值在其之前被讀入,即出現了逆序對 add(t, 1);//記錄逆序對 } printf("%lld\n", ans); return 0; }
emmm,樣例過了,提交!
誒怎麼wa了,還是段錯誤?
看了看測試資料,原來是我們將資料當作下標,而資料的大小超過了陣列大小的限制,而且也造成了空間的冗餘,這個時候,我們想到一個方法:
離散化
離散化,即是將物件之間的關係模糊化,在不改變資料相對大小的條件下,對資料進行相應的縮小。
什麼意思呢?
比如說:
在{ 1、 2、 99999、 3 }之間判斷逆序對和在{ 1、 2、 4、 3}之間判斷逆序對在基本流程上無差別,而如果不進行離散化,則花費了99999個空間,為了節省空間,也為了消除陣列越界的風險,我們使用離散化優化一下程式碼。
#include <iostream> #include <cstdio> #include <algorithm> #include <cstring> using namespace std; typedef long long LL; const int N = 100010; int n; int a[N], backup[N];//backup即是離散化之後的序列 LL tr[N]; int lowbit(int x) { return x & -x; } void add(int x, int k) { for (int i = x; i <= n; i += lowbit(i)) tr[i] += k; } LL sum(int x) { LL res = 0; for (int i = x; i; i -= lowbit(i)) res += tr[i]; return res; } int find(int k) {//查詢(利用了二分的思想) int l = 0, r = n - 1; while (l < r) { int mid = l + r + 1 >> 1; if (backup[mid] <= k) l = mid; else r = mid - 1; } return r + 1; } //排好序儲存進來的序列,其每個元素的對應下標就是其離散化之後的“大小” int main() { scanf("%d", &n); for (int i = 0; i < n; i ++) scanf("%d", &a[i]); memcpy(backup, a, sizeof a); sort(backup, backup + n);//進行排序 LL ans = 0; for (int i = n - 1; i >= 0; i --) { int t = find(a[i]); ans += sum(t - 1); add(t, 1); } printf("%lld\n", ans); return 0; }
最後,我們得到了AC!!!
希望我的拋磚引玉能引起更多的思考! ? (蒟蒻鞠躬)。