概念
樹狀數組,也叫二叉索引樹(Binary Indexed Tree,BIT),它是用數組來模擬樹形結構。樹狀數組的每個節點存儲的是數組中某一段的和(或其他可合并的信息),通過巧妙的索引方式和樹形結構,能夠在對數時間內完成前綴和的查詢以及單個元素的修改操作。
原理
結構特點:樹狀數組的結構基于二進制位的原理。對于一個數組?C,假設其下標從?1?開始,對于任意下標?i,C[i]?負責維護的區間長度是?i?的二進制表示中最低位的?1?所對應的值。例如,i = 6,二進制表示為?110,最低位的?1?對應的值是?2,所以?C[6]?維護的區間長度為?2,即?C[6]?存儲的是?a[5] + a[6]?的和(假設?a?是原始數組)。
前綴和計算:計算前綴和時,利用樹狀數組的結構特點,通過不斷地訪問父節點來累加區間和。例如,要求?a[1]?到?a[7]?的前綴和,7?的二進制是?111,則依次訪問?C[7]、C[6]、C[4]?并累加它們的值,就能得到前綴和。這是因為?C[7]?包含了?a[7],C[6]?包含了?a[5] + a[6],C[4]?包含了?a[1] + a[2] + a[3] + a[4],恰好覆蓋了?a[1]?到?a[7]?的范圍。
單點更新:當對原始數組中的一個元素進行修改時,需要更新樹狀數組中與之相關的節點。例如,修改?a[i]?的值,那么需要更新所有包含?a[i]?的區間對應的樹狀數組節點。通過不斷地將?i?加上其最低位的?1?來找到需要更新的節點。例如,i = 3,二進制是?11,更新完?C[3]?后,下一個要更新的是?C[4],因為?3?加上最低位的?1(即?1)得到?4,以此類推,直到超出數組范圍。
處理的問題類型
前綴和查詢:快速計算數組中某一段的前綴和,時間復雜度為?O(logn),相比樸素的遍歷求和方法(時間復雜度為?O(n))效率更高。
單點更新:在修改數組中單個元素的值后,能夠快速更新相關的前綴和信息,同樣具有?O(logn)?的時間復雜度。
區間修改和查詢:通過一些技巧,可以將樹狀數組擴展到支持區間修改和區間查詢操作。例如,使用差分思想,將區間修改轉化為單點修改,然后再通過樹狀數組來維護差分后的數組,從而實現高效的區間操作。
注意:如果數據滿足差分的條件,也可以實現區間更新以及區間查詢
一些步驟,功能
1,lowbit
lowbit?函數用于獲取一個整數二進制表示中最低位的?1?所對應的值。例如,對于數字?6,它的二進制表示是?110,最低位的?1?對應的值是?2;對于數字?7,二進制表示是?111,最低位的?1?對應的值是?1。在樹狀數組里,lowbit?至關重要,它能幫助我們快速找到節點之間的父子關系。
代碼:
int lowbit(int x) {
????return x & -x;}
2,get_sum
get_sum?函數用于計算樹狀數組中前?i?個元素的前綴和。它利用樹狀數組的結構特點,通過不斷訪問父節點并累加區間和來實現前綴和的快速計算。
int get_sum(int i) {
????????int sum = 0;
????????while (i > 0) {
????????????sum += tree[i];
????????????i -= lowbit(i);
????????}
????????return sum;
????}
3,update
update?函數用于更新樹狀數組中第?i?個元素的值。當修改原始數組中的某個元素時,需要更新樹狀數組中所有包含該元素的區間節點,以保證前綴和信息的正確性。
void update(int i, int val) {
?????while (i <= n) {
?????????tree[i] += val;
?????????i += lowbit(i);
????}
}
4,初始化O(n)的初始化
-使用前綴和的初始化,先計算a的前綴和數組pre,然后根據c[i]=pre[i]-pre[i-lowbit(i)]來計算
for (int i = 1; i <= n; ++i) {
????????????prefixSum[i] = prefixSum[i - 1] + arr[i - 1];
????????}
????????// 初始化樹狀數組
????????for (int i = 1; i <= n; ++i) {
????????????tree[i] = prefixSum[i] - prefixSum[i - lowbit(i)];
????????}
-使用update做到原地的O(n)的修改
void init() {
?for (int i = 1; i <= n; ++i) {
t[i] += a[i];
int j = i + lowbit(i);
?if (j <= n) t[j] += t[i];
}
?}
練習題:
樓蘭圖騰
在完成了分配任務之后,西部 314 來到了樓蘭古城的西部。相傳很久以前這片土地上(比樓蘭古城還早)生活著兩個部落,一個部落崇拜尖刀(V),一個部落崇拜鐵鍬(∧),他們分別用?V?和?∧?的形狀來代表各自部落的圖騰。
西部 314 在樓蘭古城的下面發現了一幅巨大的壁畫,壁畫上被標記出了?N?個點,經測量發現這?N?個點的水平位置和豎直位置是兩兩不同的。西部 314 認為這幅壁畫所包含的信息與這?N?個點的相對位置有關,因此不妨設坐標分別為?(1,y1?),(2,y2?),?,(n,yn?),其中?y1?~yn??是?1?到?n?的一個排列。
如圖,圖中的?y1?=1,y2?=5,y3?=3,y4?=2,y5?=4。
西部 314 打算研究這幅壁畫中包含著多少個圖騰,其中?V?圖騰的定義如下(注意:圖騰的形式只和這三個縱坐標的相對大小排列順序有關)1≤i<j<k≤n?且?yi?>yj?,?yj?<yk?;
而崇拜?∧?的部落的圖騰被定義為?1≤i<j<k≤n?且?yi?<yj?,yj?>yk?;
西部 314 想知道,這?n?個點中兩個部落圖騰的數目。因此,你需要編寫一個程序來求出?V?的個數和?∧?的個數。
輸入格式
第一行一個正整數?n;
第二行是?n?個正整數,分別代表?y1?,y2?,?,yn?。
輸出格式
輸出兩個數,中間用空格隔開,依次為?V?的個數和?∧?的個數
分析,很裸的樹狀數組,統計前后的比當前值大的,小的值的數,根據乘法原理,得答案
代碼:
#include <iostream>
#include <cstring>
using std::cin;
using std::cout;
using std::endl;
const int N = 200010;
int n;
int c[N], a[N];
// 計算 x 的最低位 1 代表的值
int lb(int x) {
????return x & -x;
}
// 查詢前綴和
long long get(int x) {
????long long res = 0;
????for (int i = x; i; i -= lb(i)) res += c[i];
????return res;
}
// 更新樹狀數組
void up(int x, int val) {
????for (int i = x; i <=n; i += lb(i)) {
????????c[i] += val;
????}
}
long long preg[N], prel[N];
int main() {
????cin >> n;
????for (int i = 1; i <= n; i++) cin >> a[i];
????for (int i = 1; i <= n; i++) {
????????preg[i] = get(n) - get(a[i]);
????????prel[i] = get(a[i] - 1);
????????up(a[i], 1);
????}
????long long res1 = 0, res2 = 0;
????memset(c, 0, sizeof c);
????for (int i = n; i; i--) {
????????res1 += preg[i] * (get(n) - get(a[i]));
????????res2 += prel[i] * get(a[i] - 1);
????????up(a[i], 1);
????}
????cout << res1 << ' ' << res2 << endl;
????return 0;
}
一個簡單的整數問題
給定長度為?NN?的數列?AA,然后輸入?MM?行操作指令。
第一類指令形如?C l r d,表示把數列中第?l~rl~r?個數都加?dd。
第二類指令形如?Q x,表示詢問數列中第?xx?個數的值。
對于每個詢問,輸出一個整數表示答案。
輸入格式
第一行包含兩個整數?NN?和?MM。
第二行包含?NN?個整數?A[i]A[i]。
接下來?MM?行表示?MM?條指令,每條指令的格式如題目描述所示。
輸出格式
對于每個詢問,輸出一個整數表示答案。
每個答案占一行。
分析:
內核是一個單點查詢和區間修改的題,我們可以通過差分和樹狀數組的合作來解決
代碼:
#include <iostream>
#include <cstring>
using std::cin;
using std::cout;
using std::endl;
const int N = 200010;
int n,m;
int c[N], a[N];
// 計算 x 的最低位 1 代表的值
int lb(int x) {
????return x & -x;
}
// 查詢前綴和
long long get(int x) {
????long long res = 0;
????for (int i = x; i; i -= lb(i)) res += c[i];
????return res;
}
// 更新樹狀數組
void up(int x, int val) {
????for (int i = x; i <=n; i += lb(i)) {
????????c[i] += val;
????}
}
int main() {
????cin >> n>>m;
????for (int i = 1; i <= n; i++) cin >> a[i];
????for (int i = 1; i <= n; i++) {
????????up(i, a[i]-a[i-1]);
????}
????for(int i=0;i<m;i++){
????????char w;
????????cin>>w;
????????if(w=='Q'){
????????????int tmp;
????????????scanf("%d",&tmp);
????????????cout<<get(tmp)<<endl;
????????}else {
????????????int l,r,d;
????????????scanf("%d%d%d",&l,&r,&d);
????????????up(l,d);
????????????up(r+1,-d);
????????}
????????
????}
????return 0;
}
一個簡單的整數問題2:
給定一個長度為?NN?的數列?AA,以及?MM?條指令,每條指令可能是以下兩種之一:
C l r d,表示把?A[l],A[l+1],…,A[r]A[l],A[l+1],…,A[r]?都加上?dd。
Q l r,表示詢問數列中第?l~rl~r?個數的和。
對于每個詢問,輸出一個整數表示答案。
輸入格式
第一行兩個整數?N,MN,M。
第二行?NN?個整數?A[i]A[i]。
接下來?MM?行表示?MM?條指令,每條指令的格式如題目描述所示。
輸出格式
對于每個詢問,輸出一個整數表示答案。
每個答案占一行。
分析:
內核是區間查詢和區間修改,上面的區間修改利用了差分來解決,接下來的區間查詢可以根據貢獻度來幫忙處理
先明確下get_sum的作用,我們想要他求出1~i的數字的和,那么對于區間l~r,就可以通過調用get(r)-get(l-1)來得到答案
接下來的問題就是如何實現get_sum,我們先看看我的之前的get(x)的作用,是求出第x個數字的大小。我們通過下面的操作
發現了黑色的d對于我們黑色部分的貢獻度是i*d[i],我們的目標是
aim =(n+1)*get(n)-∑i*d[i],
我們不妨用一個樹狀數組來存儲一下這個i*d[i],那么我們的aim
aim=(n+1)*get(n,原來的樹狀數組) -get(n,i*d[i]的樹狀數組)
代碼:
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 200010;
long long n, m;
long long c[N], a[N], ci[N];
// 計算 x 的最低位 1 代表的值
long long lb(long long x) {
????return x & -x;
}
// 查詢前綴和
long long get(long long x, long long w[]) {
????long long res = 0;
????for (int i = x; i; i -= lb(i)) res += w[i];
????return res;
}
// 更新樹狀數組
void up(long long x, long long val, long long w[]) {
????for (int i = x; i < N; i += lb(i)) {
????????w[i] += val;
????}
}
int main() {
????scanf("%d %d", &n, &m);
????for (int i = 1; i <= n; i++) scanf("%lld", &a[i]);
????for (int i = 1; i <= n; i++) {
????????up(i, a[i] - a[i - 1], c);
????????up(i, i * (a[i] - a[i - 1]), ci);
????}
????for (int i = 0; i < m; i++) {
????????char w;
????????scanf(" %c", &w); ?// 注意這里的空格,用于消耗掉之前輸入的換行符
????????if (w == 'Q') {
????????????int l,r;
????????????scanf("%d%d", &l,&r);
????????????long long rr=get(r, c) * (r + 1) - get(r, ci);
????????????long long ll=get(l-1, c) * (l ) - get(l-1, ci);
????????????printf("%lld\n", rr-ll);
????????} else {
????????????int l, r, d;
????????????scanf("%d %d %d", &l, &r, &d);
????????????up(l, d, c);
????????????up(r + 1, -d, c);
????????????up(l, l * d, ci);
????????????up(r + 1, -(r + 1)*d , ci);
????????}
????}
????return 0;
} ?
謎一樣的牛:
有?nn?頭奶牛,已知它們的身高為?1~n1~n?且各不相同,但不知道每頭奶牛的具體身高。
現在這?nn?頭奶牛站成一列,已知第?ii?頭牛前面有?AiAi?頭牛比它低,求每頭奶牛的身高。
輸入格式
第?11?行:輸入整數?nn。
第?2..n2..n?行:每行輸入一個整數?AiAi,第?ii?行表示第?ii?頭牛前面有?AiAi?頭牛比它低。
(注意:因為第?11?頭牛前面沒有牛,所以并沒有將它列出)
輸出格式
輸出包含?nn?行,每行輸出一個整數表示牛的身高。
第?ii?行輸出第?ii?頭牛的身高。
分析:
二分+樹狀數組,從后面向前遍歷,通過二分找到第k大的數,然后記錄答案即可,如果不會二分也可以用兩個優先隊列,然后從一個里面pop出來k個,放到另外一個,然后取到第k大的后,都放在一個隊列里面,如此往復
代碼:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int a[N];
int ans[N];
int tr[N];
int lowbit(int x)
{
????return x & -x;
}
void add(int x, int c)
{
????for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int sum(int x)
{
????int res = 0;
????for (int i = x; i; i -= lowbit(i)) res += tr[i];
????return res;
}
int main()
{
????scanf("%d", &n);
????for (int i = 2; i <= n; i ++ ) scanf("%d", &a[i]);
????for (int i = 1; i <= n; i ++ ) add(i,1);
????for (int i = n; i; i -- )
????{
????????int k = a[i] + 1;
????????int l = 1, r = n;
????????while (l < r)
????????{
????????????int mid = l + r >> 1;
????????????if (sum(mid) >= k) r = mid;
????????????else l = mid + 1;
????????}
????????ans[i] = r;
????????add(r, -1); ????//刪除
????}
????for (int i = 1; i <= n; i ++ ) printf("%d\n", ans[i]);
????return 0;
}