樹狀數組的三大應用


前文我們探討了樹狀數組的原理。樹狀數組就是一種數據結構,它天生用來維護數組的前綴和,從而可以快速求得某一個區間的和,並支持對元素的值進行修改。但是樹狀數組並非只有這一種功能,變形后它還能衍生出兩個功能,本文我們就來分別討論下樹狀數組這三大功能。

永遠要記住,基本的樹狀數組維護的是數組的前綴和,所有的區間求值都可以轉化成用 sum[m]-sum[n-1] 來解,這點無論是在改點還是接下來要說的改段中都非常重要。

改點求段###


這也是樹狀數組的基本應用。我們可以來看一下這道題 敵兵布陣

如果看了前文 【前端也要學點數據結構】 神奇的樹狀數組,解法也就呼之欲出了,直接給出代碼:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
using namespace std;
#define N 50005
int lowbit(int x) { return x & (-x); }
int sum[N], cnt;

void update(int index, int val) {
  for (int i = index; i <= cnt; i += lowbit(i))
    sum[i] += val;
}

int getSum(int index) {
  int ans = 0;
  for (int i = index; i; i -= lowbit(i))
    ans += sum[i];
  return ans;
}

int main() {
  string str;
  int n, m, t, tmp, cas = 1;
  scanf("%d", &t);
  while (t--) {
    memset(sum, 0, sizeof(sum));
    scanf("%d", &cnt);
    for (int i = 1; i <= cnt; i++) {
      scanf("%d", &tmp);
      update(i, tmp);
    }
    
    printf("Case %d:\n", cas++);
    
    while (cin >> str) {
      if (str == "End") break;
      scanf("%d%d", &n, &m);
      if (str == "Query")
        printf("%d\n", getSum(m) - getSum(n - 1));
      else if (str == "Add")
        update(n, m);
      else update(n, -m);
    }
  }
  return 0;
}

改段求點###


改段求點和改點求段恰好相反,比如有一個數組 a = [x, 0, 0, 0, 0, 0, 0, 0, 0, 0],每次的修改都是一段,比如讓 a[1]~a[5] 中每個元素都加上10,讓 a[6]~a[9] 中每個元素都減去2,求任意的元素的值。

看例題 Color the ball

跟改點求段不同,這里要轉變一個思想。在改點求段中,sum[i]表示Ci節點所管轄的子節點的元素和,而在改段求點中,sum[i]表示Ci所管轄子節點的批量統一增量

還是看這個經典的圖:

比方說,C8管轄A1A8這8個節點,如果A1A8每個都染色一次,因為前面說了sum[i]表示i所管轄子節點的統一增量,那么也就是 sum[8]+=1,A5~A7都染色兩次,也就是 sum[6] +=2, sum[7] +=2 。如果要求A1被染色的次數,C8是能管轄到A1的,也就是說sum[8]的值和A1被染色的次數有關,仔細想想,也就是把能管轄到A1的父節點的sum值累積起來即可。兩個過程正好和改點求段相反。

完整代碼:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
using namespace std;
#define N 100005
int sum[N], n;
int lowbit(int x) { return x & (-x); }

void update(int index, int val) {
  while (index) {
    sum[index] += val;
    index -= lowbit(index);
  }
}

int query(int index) {
  int ans = 0;
  while (index <= n) {
    ans += sum[index];
    index += lowbit(index);
  }
  return ans;
}

int main() {
  int x, y;
  while (scanf("%d", &n) && n) {
    memset(sum, 0, sizeof(sum)); 
    for (int i = 1; i <= n; i++) {
      scanf("%d%d", &x, &y);
      update(y, 1);
      update(x - 1, -1);
    }
    
    for (int i = 1; i < n; i++)
      printf("%d ", query(i));
    printf("%d\n", query(n));
  }
  return 0;
}

改段求段###


改段求段也有道經典的模板題:A Simple Problem with Integers

我們還是從簡單的例子入手,比如有如下數組(a[1]=1,..a[9]=9):

1 2 3 4 5 6 7 8 9 10

假設我們將 a[1]~a[4] 這段增加5,對於我們要求的區間和來說,要么是 [1,2] 這種屬於所改段的子區間,要么是 [1,8] 這種屬於所改段的父區間(前面說了,所有的區間求值都可以用sum[m]-sum[n-1]來解,所以我們只考慮前綴和),我們分別討論。

如果所求是類似 [1,8] 這種,我們可以很開心地發現,我們將區間增量(4*5)全部加在 a[4] 這個元素上,對結果並沒有什么影響!於是變成了一般的改點求段。

如果所求是類似 [1,2] 這種,我們可以用類似改段求點中染色的思想進行處理。譬如 [1,4] 成段加5,如果我們要計算 [1,2] 的和。我們將 [1,3] 進行“染色”(節點4加上了4*5的權重),因為 [1,3] 在樹狀數組的划分中可以分為兩個區間,[1,2][3,3],所以我們用類似改段求點對這兩塊區域進行“染色”,染上的次數為5。我們要求的是 [1,2] 的區間和,我們只需找 2 被染色的次數,因為 [1,n] 進行染色。如果m(1<=m<=n)被染色,那么m的右邊肯定都被染色了。求出被染色的次數,然后乘上區間寬度,就是整段的和了。

這樣我們分別對兩種情況進行了處理,更重要的是,這兩種情況互不影響! 於是我們簡單地把兩個結果相加就ok了,而這兩個過程,分別正是改點求段和改段求點!

完整代碼:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define N 100005
#define ll __int64
ll b[N], c[N];
int n;

int lowbit(int x) {
  return x & (-x);
} 

void update_backwards(int index, ll val) {
  for (int i = index; i <= n; i += lowbit(i))
    b[i] += val;
}

void update_forward(int index, ll val) {
  for (int i = index; i; i -= lowbit(i))
    c[i] += val;
}

void update(int index, ll val) {
  update_backwards(index, index * val);
  update_forward(index - 1, val);
}

ll query_forward(int index) {
  ll ans = 0;
  for (int i = index; i; i -= lowbit(i))
    ans += b[i];
  return ans;
}

ll query_backwards(int index) {
  ll ans = 0;
  for (int i = index; i <= n; i += lowbit(i))
    ans += c[i];
  return ans;
}

ll query(int index) {
  return query_forward(index) + query_backwards(index) * index;
}

//---------------- main -------------- //
int main() {
  int t, x, y;
  ll z;
  char str[2];
  memset(b, 0, sizeof(b));
  memset(c, 0, sizeof(c));
  scanf("%d%d", &n, &t);
  n += 1;
  for (int i = 1; i < n; i++) {
    scanf("%I64d", &z);
    x = i + 1, y = i + 1;
    update(y, z);
    update(x - 1, -z);
  }
  
  while (t--) {
    scanf("%s", str);
    if (str[0] == 'C') {
      scanf("%d%d%I64d", &x, &y, &z);
      x += 1, y += 1;
      update(y, z);
      update(x - 1, -z);
    } else {
      scanf("%d%d", &x, &y);
      x += 1, y += 1;
      printf("%I64d\n", query(y) - query(x - 1));
    }
  }
  return 0;
}

這里有一點需要注意:一般的用數組數組來解的題,都是不用a[0]的,也就是元素是從a[1]~a[n],因為 sum[n~m]=sum[m]-sum[n-1],避免 n-1 為負數。**而本題中的改段求段中的元素是從 a[2]~a[n+1] **,因為 update()函數中的子函數 update_forward() 函數中 index-1 不能為負,所以參數 index 最小是1,所以 sum[n-1]n-1最小是1,所以n最小是2,所以元素下標必須從 2 開始。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM