天天看點

樹狀數組模闆樹狀數組用樹狀數組解決二維偏序問題

樹狀數組

樹狀數組模闆樹狀數組用樹狀數組解決二維偏序問題

為什麼需要樹狀數組?

很多場景下,需要通過O(n)的複雜度求字首和,之後通過差分O(1)的複雜度,求指定區間的區間和。

但是,對于已經建立起來的字首和數組,如果改變某個單點的值,修改的代價是O(n)的。

樹狀數組的定義

  • 可以通過樹狀數組實作O(logn)的修改,實作O(logn)的查詢
  • 樹狀數組的大小于員數列大小相同
  • 與字首和不同的是,樹狀數組的 i 位置的元素存儲的并不是前 i 個元素 而是從 i 開始,包括第 i 個元素的前 ti 個元素的和。
  • 其中 ti 是最大的可以整除 i 的2的幂
  • 例如:s[ i ] 為樹狀數組,d[ i ] 是原數組
  • i = 9, ti = 1      s [ 9 ] = d [ 9 ]
               
  • i = 6 ,   ti = 2      s [ 6 ] = d [ 5 ] + d [ 6 ] 
               
  • i = 24 , ti = 8      s[ 24 ] = d [17] + d [ 18 ] + d [ 19 ] + d [ 20 ] + d [ 21 ] +...+ d [ 24 ]    
               
  • 如果轉為二進制,ti 的 歸路更加明顯
  • i = 9 (1001) ti = 1
  • i = 6 (0110) ti = 2
  • i = 24 (11000) ti = 8

    ti 為二進制最低位的1,與其後的0組成的2進制

lowbit(i)

求解ti的函數

原數 i : 101011000

取反 : 010100111

取反+1:010101000

和原數相與:000001000

lowbit(i) = i & ((~1) + 1) = i & ( -i )

樹狀數組的查詢

樹狀數組模闆樹狀數組用樹狀數組解決二維偏序問題

要查詢的資料是指定位置x的字首和 [1, x ]的字首和

對于區間 [ 1, 11 ] 可以使用 是 s[ 11 ] + s [ 10 ] + s [ 8 ] 表示

對于任何一個區間,都能被完整的表示出來

因為s[ i ] 可以表示前 ti 個數的和,

ans += s[ i ]

令 i = i - ti

ans += s[ i ]

。。。直到i = 1

ans += s[ 1 ]

傳回 ans

#define lb(x) (x & (-x))
long long  ask(int x){
    long long  res = 0;
    for(int i = x;i >= 1;i -= lb(i))
    res += s[i];
    return res;
}
           

樹狀數組的性質

性質1 : 若目前結點為x,且令x + tx 為父節點 tx = lowbit(x) ,則梳妝數組将形成一個樹形結構

性質2 : 結點x 記錄區間 ( x - tx,x ] 的資訊,其子節點記錄的區間是( x - tx, x ] 的子集,且不會互相覆寫。

性質3 : 結點x 的記錄的區間為結點y 記錄的區間的子集,當且僅當y是結點x的祖先節點

樹狀數組的修改

修改的時候,改變原數組x位置的值,隻需要修改 s[x] 和所有祖先節點中的值,複雜度是O(logn)的,相比字首和,修改的複雜度小

怎麼找祖先節點?

樹狀數組模闆樹狀數組用樹狀數組解決二維偏序問題

在 i 位置 加上 lowbit(i),就是祖先的位置

在i 位置加 v,每個祖先節點都要加v

void upd(int x,int v){
    for(int i =x; i <= n;i += lb(i))
    s[i] += v;
}
           

例題

樹狀數組模闆樹狀數組用樹狀數組解決二維偏序問題

求區間[ l, r ]的和, ask(r)- ask(l-1)

#include"bits/stdc++.h"
#define lb(x) (x & (-x))
using namespace std;

long long  s[1000001];
int n,q;
long long  ask(int x){
    long long  res = 0;
    for(int i = x;i >= 1;i -= lb(i))
    res += s[i];
    return res;
}
void upd(int x,int v){
    for(int i =x; i <= n;i += lb(i))
    s[i] += v;
}
int main()
{
    
    //freopen("a.in","r",stdin);
    //freopen("a.out","w",stdout);
    memset(s,0,sizeof(s));

    cin>>n>>q;
    for(int i = 1 ;i <= n;i++)
        {
            int t;
            scanf("%d",&t);
            upd(i,t);
        }
    for(int i = 0;i < q;i++)
    {
        int op,num1,num2;

        scanf("%d %d %d",&op,&num1,&num2);
        if(op ==1)
        {
            upd(num1,num2);
        }
        else
        {
            long long ansl = ask(num1-1);
            long long  ansr = ask(num2);
            cout << (ansr - ansl) <<endl;
        }
    }
    return 0;
}
           

用樹狀數組解決二維偏序問題

  • 給定一個序列 a1, a2, a3, …, an,如果存在 i < j 且 ai >

    aj,那麼我們稱之為逆序對,求給定序列中逆序對的數目。其中 1 ≤ ai, n ≤ 10^5。

  • 對于任意一個ai,統計在其之前求大于 ai 的數的數量,即為以 ai 結尾的逆序對的數量
  • 隻要枚舉a1 ~ an分别計算,再求和,即為所求的答案

例題2 二維偏序問題

給定一個序列 a1, a2, a3, …, an,如果存在 i < j 且 ai > aj,那麼我們稱

之為逆序對,求給定序列中逆序對的數目。其中 1 ≤ ai, n ≤ 10^5。

思路:開一個數組s[MAXN],對于a1查詢 a[MAXN]到a[a1 + 1]所有的和,也就是目前比a1大的元素有多少個,查詢的結果加到ans上,更新 a[a1] +1

例子

樹狀數組模闆樹狀數組用樹狀數組解決二維偏序問題

兩維坐标,按照某一維i例如運作時間排序,求另一維的逆序對數

#include"bits/stdc++.h"
using namespace std;
#define lb(x) (x & (-x))
int s[1000010],n;
//int cnt[1000010];
int   ask(int x)
{
     int ans = 0;
    for(int i = x ;i >= 1; i -= lb(i))
        ans += s[i];
        return ans;
}

void upd(int x,int v)
{
    for(int i = x; i<=1000009;i += lb(i))
        s[i] += v;
}
int main()
{
    freopen("a.in","r",stdin);
    freopen("a.out","w",stdout);
    cin>>n;
    memset(s,0,sizeof(s));
    memset(cnt,0,sizeof(cnt));
    vector<pair<int,int>> a;
    for(int i= 0;i < n;i++)
    {
        int t,c;
        scanf("%d %d",&t,&c);
        a.push_back(make_pair(t+1,c+1));
    }
    sort(a.begin(),a.end());
    for(int i = 0;i <n;i++)
    {
        
        int  num = ask(1000009) - ask(a[i].second);
        int  score = i - num;
        cnt[score]++;
        upd(a[i].second,1);
    }
    for(int i = 0;i < n;i++)
    printf("%d\n",cnt[i]);
    return 0;
}
           

例題3 樹狀數組和動态規劃相結合 – 最長上升子序列

樹狀數組模闆樹狀數組用樹狀數組解決二維偏序問題

狀态:定義fi 表示以A為結尾的最長上升序列的長度

初始化 f1= 1

轉移過程: fi = max{ fj | j < i && Aj < Ai } + 1

輸出答案 max{f[i], i = 1…n}

再找最大的fj j < i && Aj < Ai 的過程中,要從1 到 i 周遊一遍才行,複雜度為O(n^2)

使用樹狀數組進行優化

樹狀數組儲存的内容,從前ti個資料的和, 變為前ti個數的最大值

如果讀到一個數 x ,ask(x - 1),得出結尾小于x的所有子序列的最長的長度,

tmp = ask(x - 1) + 1

upd(x , tmp)

繼續閱讀