數據結構之真別多想—樹狀數組

瓶頸

如何理解樹狀數組?ios

img

這個結構的思想和線段樹有些相似:用一個大節點表示一些小節點的信息,進行查詢的時候只須要查詢一些大節點而不是更多的小節點。c++

最下面的八個方塊就表明存入 a 中的八個數,如今都是十進制。算法

他們上面的良莠不齊的剩下的方塊就表明 a 的上級—— c 數組。數組

很顯然看出: c2 管理的是 a1 & a2 ; c4 管理的是 a1 & a2 & a3 & a4 ; c6 管理的是 a5 & a6 ;c8 則管理所有 8 個數。函數

因此,若是你要算區間和的話,好比說要算 a51 ~ a91 的區間和,暴力算固然能夠,那上百萬的數,那就 TLE 嘍。測試

——————摘自oi-wiki.org優化

 

初看這些文字,你可能會想:spa

「啊這這這???你講這些咱們怎麼聽得懂啊,這樹狀數組是啥,咋用,咱們仍是懵的啊」3d

 

固然,爲了解決問題而書寫算法的話,咱們不須要去理解這個結構的原理究竟是啥,咱們只須要知道這個東西code

用在哪?

怎麼用?

就足夠了

用在哪?

咱們都知道:

通常的普通數組單點操做的時間複雜度的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了,仍是段錯誤?

看了看測試數據,原來是咱們將數據看成下標,而數據的大小超過了數組大小的限制,並且也形成了空間的冗餘,這個時候,咱們想到一個方法:

離散化

離散化,便是將對象之間的關係模糊化,在不改變數據相對大小的條件下,對數據進行相應的縮小。

什麼意思呢?

好比說:

在{ 一、 二、 9999九、 3 }之間判斷逆序對和在{ 一、 二、 四、 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!!!

 

但願個人拋磚引玉能引發更多的思考! 😄 (蒟蒻鞠躬)

相關文章
相關標籤/搜索