天天看點

樹狀數組樹狀數組:例題1.簡單的整數問題l2.簡單的整數問題llEnd

樹狀數組:

1.單點修改,區間求和

 1.快速求字首和 o(logn)

 2.修改某一個數 o(logn)

2.拓展-區間修改,求單點和(結合差分)

3.拓展-區間修改,區間求和

lowbit函數用于取x二進制最低位的1與後面的0組成的數字

原理:二進制的負數是正數取反加一

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

原理

假設x = 2ik + 2ik-1 +…+ 2i1

ik >= ik-1 >= … i1

将區間劃分成(左開右閉):

 (x - 2i1, x]

包含2i1個數,2i1是右邊界二進制表示的最後一位1與後面的0構成的數

 (x - 2i1 - 2i2, x - 2i1]

包含2i2個數,2i2是右邊界二進制表示的最後一位1與後面的0構成的數

 (x - 2i1 - 2i2 - 2i3, x - 2i1 - 2i2] …

 …

 (0, x - 2i1 - 2i2 -…- 2ik-1)

包含2ik個數,包含2ik個數,2ik是右邊界二進制表示的最後一位1與後面的0構成的數

 -> 對于其中任意區間 (L,R] 長度是R的二進制表示的最後一位1所對應的數

 -> 則對于其中的每一個區間,可以表示為 [R - lowbit(R) + 1, R] (左閉右閉)

 -> 可以用數組c[R]表示這個區間總和

   -> c[x - lowbit(x) + 1, x] 表示以x為右端點長度為lowbit(x)+1的區間的所有數的和

   -> 對于1~x所有數的和,最多可以拆成logx個c[x]的和

樹如圖所示

---------------------------------------------------------------------c16
  ------------------------------------c8
  ----------------c4                      -------------c12
  ------c2            ------c6            -----c10        -----c14    
  -c1       -c3       -c5       -c7       -c9     -c11    -c13    -c15
a 1    2    3    4    5    6    7    8    9   10  11  12  13  14  15  16

c16  =  a16  +  c15  +  c14  +  c12  +  c8
10000           01111   01110   01100   01000
           

tr[i]表示,以i結尾,長度是lowbit(i)的區間内元素的和!!!

求x的子節點

先将x減去1(eg 10000變成01111),每個1對應一個兒子(eg 01111 01110 01100 01000)

c[x] = a[x] + c[x-1] + c[x-1-lowbit(x-1)] + c[x-1-lowbit(x-1)-lowbit(x-2)] +…+ 0

對于x-1-lowbit(x-1)表示将x-1的最後一個一去掉,即實作01111 -> 01110

===========================

代碼實作

1.通過父節點找子節點

對于x-1,有k個兒子,即循環k次,每次減掉最後的1即找到每一個兒子( eg求字首和)

int sum(int x){ 
   int res = 0;
   for ( int i = x; i; i -= lowbit(i) ) tr[x] += tr[i];
   return res;
}
           

2.通過子節點找父節點

更改x的值,即更改(上圖)包含x的所區間

假如x 0011100 -> x父節點p 0100000(唯一的) 0011100 + 0000100 = 0100000

-> p = x + lowbit(x) -> p2 = p + lowbit§ -> p3 = … -> 直到等于n

最多持續logx次(eg某個數加c)

void add(int x, int c){ 
 for ( int i = x; i <= n; i += lowbit(i) ) tr[i] += c;
}
           

3.初始化樹狀數組

for ( int i = 1; i <= n; i ++ ) add(i, a[i])
           

->時間複雜度o(nlogn)

========================================================

例題

1.簡單的整數問題l

區間修改,單點求值

題目

給定長度為 N 的數列 A,然後輸入 M 行操作指令。

 C l r d,表示把數列中第 l∼r 個數都加 d

 Q x,表示詢問數列中第 x 個數的值

對于每個詢問,輸出一個整數表示答案。

思路

樹狀數組的拓展

将某一區間的每個數加上c,求a[x] -> 差分

将問題變成單點修改,區間求和

樹狀數組中存儲a[i]的差分

構造差分:

b[i] = a[i] - a[i - 1]
           

代碼

#include <iostream>
#include <cstring>
using namespace std;

typedef long long LL;
const int N = 1e5 + 10;
int a[N], tr[N], n, m;

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;
}

LL sum(int x)
{
    LL res = 0;
    for ( int i = x; i; i -= lowbit(i) ) res += tr[i];
    return res;
}

int main()
{
    cin >> n >> m;
    for ( int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
    for ( int i = 1; i <= n; i ++ ) add(i, a[i] - a[i - 1]); //樹狀數組中存a[i]的差分
    while ( m -- )
    {
        char op[2];
        scanf("%s", op);
        if ( *op == 'C' ) 
        {
            int l, r, d;
            scanf("%d%d%d", &l, &r, &d);
            add(l, d), add(r + 1, -d);  //差分日常操作
        }
        else 
        {
            int x;
            scanf("%d",&x);
            printf("%lld\n", sum(x));     //差分數組的字首和便是a[i]
        }
    }
    return 0;
}
           

~~~~~~~~~~~~~~~~~

2.簡單的整數問題ll

(樹狀數組+差分+巧妙運算)區間修改,區間求和**

題目

給定長度為 N 的數列 A,然後輸入 M 行操作指令

 C l r d,表示把數列中第 l∼r 個數都加 d

 Q l r,表示詢問區間 [l, r] 内元素的和

對于每個詢問,輸出一個整數表示答案。

思路

構造a數組的差分數組b

區間求和:求a1 + a2 +…+ ax (ax = b1 + b2 +…+ bx)

//樸素法(時間複雜度極高)

  for ( int i = 1; i <= x; i ++ )
      for ( int j =1; j <= i; j ++ ) 
              res += b[j];

//優化法
1.a1到ax的和如圖1)所示,要求圖1)的和,先将圖1)補成圖2)

                      2)
1)                    b1 b2 b3 b4 ... bx    注:在沒有b的第0行也要補,補完後是x+1行
b1                    b1 b2 b3 b4 ... bx
b1 b2                 b1 b2 b3 b4 ... bx
b1 b2 b3     ->       b1 b2 b3 b4 ... bx
...                   ...
b1 b2 b3 b4 ... bx    b1 b2 b3 b4 ... bx

2)的求和:(b1 + b2 + ... + bx) * (x + 1)
1)的求和:(b1 + b2 + ... + bx) * (x + 1) - (b1 + 2*b2 + ... + x*bx)
    -> 1)的和即為 2)減去i*b的字首和
    -> 用tr1維護bi的字首和,tr2維護i*bi的字首和
    -> sum(tr1, x) * (x + 1) - sum(tr2, x)
           

代碼

#include <iostream>
#include <cstring>
using namespace std;

typedef long long LL;
const int N = 1e5 + 10;
LL tr1[N], tr2[N];
int a[N], n, m;

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

void add(LL tr[], int x, LL c)
{
    for ( int i = x; i <= n; i += lowbit(i) ) tr[i] += c;
}

LL sum(LL tr[], int x)
{
    LL res = 0;
    for ( int i = x; i; i -= lowbit(i) ) res += tr[i];
    return res;
}

LL prefix_sum(int x)
{
    return sum(tr1, x) * (x + 1) - sum(tr2, x);
}

int main()
{
    cin >> n >> m;
    for ( int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
    for ( int i = 1; i <= n; i ++ )
    {
        add(tr1, i, a[i] - a[i - 1]);
        add(tr2, i, (LL)(a[i] - a[i - 1]) * i); //可能會爆int
    }
    
    while ( m -- )
    {
        char op[2];
        int l, r, d;
        scanf("%s%d%d", op, &l, &r);
        if ( *op == 'C' ) 
        {
            scanf("%d", &d);
            add(tr1, l, d), add(tr2, l, l * d);
            add(tr1, r + 1, -d), add(tr2, r + 1, (r + 1) * -d);
        }
        else printf("%lld\n", prefix_sum(r) - prefix_sum(l - 1));
    }
    return 0;
}
           

End

繼續閱讀