st表、樹狀數組與線段樹 筆記與思路整理

已更新(2/3):st表、樹狀數組node

 

st表、樹狀數組與線段樹是三種比較高級的數據結構,大多數操做時間複雜度爲O(log n),用來處理一些RMQ問題或相似的數列區間處理問題。ios


 

1、ST表(Sparse Table

st表預處理時間複雜度O(n log n),查詢O(1),但不支持在線更改,不然要從新進行預處理。c++

使用一個二維數組:st[i][j]存儲i爲起點,長度爲2j的一段區間最值,即arr[i, i + 2j - 1]。算法

具體步驟(以最小值爲例):數組

  1. 將st[i][0]賦值爲arr[i];
  2. 利用動態規劃思想,dp出st[i][j] = min(st[i][j - 1], st[i + 2j - 1][j - 1])  (1 ≤ i ≤ n, 1 ≤ j ≤ log2 n);
  3. 查詢時,定義len爲log2(r - l + 1),區間[l, r]的最小值爲min(st[l][len],st[r - 2len + 1][len])。

總時間複雜度爲O(n log n + q),q爲請求數。網絡

代碼實現(兩個st表分別求最大最小值):數據結構

#include <bits/stdc++.h>
using namespace std;
int stmin[60010][20], stmax[60010][20];
int n, q, arr[60010], minans, maxans;
void init(){
    for(int j = 1 ; j <= n ; j++)stmax[j][0]=stmin[j][0]=arr[j];
    for(int i = 1 ; i <= log2(n) ; i++){
        for(int j = 1  ; j <= n ; j++){
            stmax[j][i] = stmax[j][i-1];
            if(j + (1 << (i-1)) <= n ) stmax[j][i] = max(stmax[j][i], stmax[j+(1<<(i-1))][i-1]);
            stmin[j][i] = stmin[j][i-1];
            if(j + (1 << (i-1)) <= n ) stmin[j][i] = min(stmin[j][i], stmin[j+(1<<(i-1))][i-1]);
        }
    }
}
void query(int l,int r){
    int len = log2(r - l + 1);
    minans = min(stmin[l][len],stmin[r - (1 << len) + 1][len]);
    maxans = max(stmax[l][len],stmax[r - (1 << len) + 1][len]);
}
int main(){
    scanf("%d %d", &n, &q);
    for(int i = 1 ; i <= n  ; i++)
        scanf("%d", &arr[i]);
    init();
    int l,r;
    for(int i = 1 ; i <= q ; i++ ){
        scanf("%d %d", &l, &r);
        query(l, r);
        printf("%d %d\n", minans, maxans);
    }
    return 0;
}
View Code

2019.9.13 upd:ide

一點優化:每次計算2n或log2n會比較慢,能夠事先用兩個數組初始化2n或log2n的值。遞推公式:優化

Bin[0] = 1; for(int i=1; i<20; i++) Bin[i] = Bin[i-1] * 2;  //Bin[i]表示2的i次方
Log[0] = -1; for(int i=1; i<=200000; i++) Log[i] = Log[i/2] + 1;  //Log[i]表示以2爲底i的對數

2019.9.20 upd:spa

預處理Bin數組(Bin[i] = 2i)與 1<<i 時間基本一致(可是log2(i)仍是比較慢的,最好仍是初始化Log數組)

 


 

2、樹狀數組(Binary Indexed Tree(B.I.T), Fenwick Tree)

樹狀數組是一種樹狀的結構(廢話),可是隻須要 O(n) 的空間複雜度。區間查詢和單一修改複雜度都爲 O(log n) ,通過差分修改後區間修改也能夠達到 O(log n) ,但此時不能區間查詢。經過維護多個數組能夠達到 O(log n) 的區間修改與查詢。

先來看一棵樹(僞)。

一棵二叉樹。

 (圖片均盜自網絡QwQ)

若是要在一棵樹上存儲一個數組而且便於求和,咱們能夠想到讓每一個父節點存儲其兩個子節點的和。(就選擇是你啦!線段樹!)

爲了達到 O(n) 的空間複雜度,刪去一些節點(放棄線段樹)後以下:

 紅色的爲樹狀數組的節點,黑色爲原始數組。每一個樹狀數組的節點存儲以其爲根節點的子樹上的全部值之和

設 a[] 爲原數組, t[] 爲樹狀數組,則:

t[1] = a[1];
t[2] = a[1] + a[2];
t[3] = a[3];
t[4] = a[1] + a[2] + a[3] + a[4];
t[5] = a[5];
t[6] = a[5] + a[6];
t[7] = a[7];
t[8] = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8];

因此說,這棵樹的(我本身沒推出來的)規律是:

t[i] = a[i - 2+ 1] + a[i - 2k + 2] + ... + a[i]; //k爲i的二進制中從最低位到高位連續零的長度

i的前綴和sum[i] = t[i] + t[i-2k1] + t[(i - 2k1) - 2k2] + ...;

設lowbit(i) = 2k , 則能夠遞推以下:

void add_node(int pos, int val){  //將節點pos增長val
    for(int i=pos; i<=n; i+=lowbit(i)){
        t[i] += val;
    }
}
int ask(int pos){  //求節點pos前綴和
    int ans = 0;
    for(int i=pos; i>0; i-=lowbit(i)){
        ans += t[i];
    }
    return ans;
}
int query_sum(int l, int r){  //利用前綴和求[l, r]總和
    return ask(r) - ask(l);
}

那麼問題來了,怎麼求這個 2k 呢?

有一個巧妙的(我本身也沒推出來的)算法是:

lowbit(x) = x & (-x);

抄一段證實以下:

這裏利用的負數的存儲特性,負數是以補碼存儲的,對於整數運算 x&(-x)有
       ● 當x爲0時,即 0 & 0,結果爲0;//所以實際運算的時候若是真的出現了lowbit(0)會卡死,要從1開始存儲
       ●當x爲奇數時,最後一個比特位爲1,取反加1沒有進位,故x和-x除最後一位外前面的位正好相反,按位與結果爲0。結果爲1。
       ●當x爲偶數,且爲2的m次方時,x的二進制表示中只有一位是1(從右往左的第m+1位),其右邊有m位0,故x取反加1後,從右到左第有m個0,第m+1位及其左邊全是1。這樣,x& (-x) 獲得的就是x。 
       ●當x爲偶數,卻不爲2的m次方的形式時,能夠寫做x= y * (2^k)。其中,y的最低位爲1。實際上就是把x用一個奇數左移k位來表示。這時,x的二進制表示最右邊有k個0,從右往左第k+1位爲1。當對x取反時,最右邊的k位0變成1,第k+1位變爲0;再加1,最右邊的k位就又變成了0,第k+1位由於進位的關係變成了1。左邊的位由於沒有進位,正好和x原來對應的位上的值相反。兩者按位與,獲得:第k+1位上爲1,左邊右邊都爲0。結果爲2^k。
        總結一下:x&(-x),當x爲0時結果爲0;x爲奇數時,結果爲1;x爲偶數時,結果爲x中2的最大次方的因子。

一、區間查詢與單點修改

具體講解見上。

完整的樹狀數組單點修改和區間查詢實現爲:

(針對模板題:Luogu P3374

#include <bits/stdc++.h>
using namespace std;
int a[500010], t[500010];
int n, m;
int lowbit(int x){
    return x & (-x);
}
void add_node(int pos, int val){
    for(int i=pos; i<=n; i+=lowbit(i)){
        t[i] += val;
    }
}
int query_node(int pos){
    int ans = 0;
    for(int i=pos; i>0; i-=lowbit(i)){
        ans += t[i];
    }
    return ans;
}
int query_range(int l, int r){
    return query_node(r) - query_node(l-1);
}
int main(){
    cin >> n >> m;
    int opt, pos, l, r, num;
    for(int i=1; i<=n; i++){
        scanf("%d", &a[i]);
        add_node(i, a[i]);
    }
    while(m--){
        scanf("%d", &opt);
        if(opt == 1){
            scanf("%d%d", &pos, &num); 
            add_node(pos, num);
        }
        if(opt == 2){
            scanf("%d%d", &l, &r);
            printf("%d\n", query_range(l, r));
        }
    }
    return 0;
}
View Code

二、單點查詢與區間修改

那麼,如何讓線段樹支持區間更改與單點查詢呢?

設數組 b[i] = a[i] - a[i-1] ,用 t[] 表示 b[] 。

模擬算一次:

a[] = 1, 5, 4, 2, 3, 1, 2, 5

b[] = 1, 4, -1, -2, 1, -2, 1, 3

將區間[2, 5]加上1:

a[] = 1, 6, 5, 3, 4, 2, 2, 5

b[] = 1, 5, -1, -2, 1, -2, 0, 3

能夠看到,只有 b[2] 和 b[6] 發生了變化。(即更改區間[l, r]時的節點l與節點r+1)所以,以 b[] 爲原數組的 t[] 只須要執行兩次 add_node() 便可。可是,在查詢 a[i] 的時候就須要查詢 b[1...i] 之和,在 log n 時間裏只能查詢單個節點的值。

完整的區間修改與單點查詢代碼實現:

(針對模板題:Luogu P3368

 

#include <bits/stdc++.h>
using namespace std;
int a[500010], t[500010];
int n, m;
int lowbit(int x){
    return x & (-x);
}
void add_node(int pos, int val){
    for(int i=pos; i<=n; i+=lowbit(i)){
        t[i] += val;
    }
}
void add_range(int l, int r, int val){
    add_node(l, val);
    add_node(r+1, -val);
}
int query_node(int pos){
    int ans = 0;
    for(int i=pos; i>0; i-=lowbit(i)){
        ans += t[i];
    }
    return ans;
}
int main(){
    cin >> n >> m;
    int opt, pos, l, r, num;
    for(int i=1; i<=n; i++){
        scanf("%d", &a[i]);
        add_node(i, a[i] - a[i-1]);
    }
    while(m--){
        scanf("%d", &opt);
        if(opt == 1){
            scanf("%d%d%d", &l, &r, &num);
            add_range(l, r, num);
        }
        if(opt == 2){
            scanf("%d", &pos);
            printf("%d\n", query_node(pos));
        }
    }
    return 0;
}
View Code

 

三、區間查詢與區間修改

簡單談一下區間查詢與區間修改的操做:

(本段參考了xenny的博客

ni = 1a[i] = ∑ni = 1 ∑ij = 1t[j];

則 a[1] + a[2] + ... + a[n]

= (t[1]) + (t[1] + t[2]) + ... + (t[1] + t[2] + ... + t[n]) 

= n * t[1] + (n-1) * t[2] + ... + t[n]

= n * (t[1] + t[2] + ... + t[n]) - (0 * t[1] + 1 * t[2] + ... + (n - 1) * t[n])

因此上式能夠變爲∑ni = 1a[i] = n*∑ni = 1t[i] -  ∑ni = 1( t[i] * (i - 1) );

所以,維護兩個樹狀數組,t1[i] = t[i],t2[i] = t[i] * (i - 1);

具體修改及查詢公式見完整代碼實現:

 (針對模板題:POJ 3468

#include<iostream>
#include<cstdio>
using namespace std;
int n, m, maxn = 1;
long long a[500010], t1[500010], t2[500010];
int lowbit(int x){
    return x & (-x);
}
void add_node(int pos, long long val){
    for(int i=pos; i<=n; i+=lowbit(i)){
        t1[i] += 1ll * val;
        t2[i] += 1ll * val * (pos-1);
    }
}
void add_range(int l, int r, long long val){
    add_node(l, val);
    add_node(r+1, -val);
}
long long query_node(int pos){
    long long ans = 0;
    for(int i=pos; i>0; i-=lowbit(i)){
        ans += 1ll * pos * t1[i] - t2[i];
    }
    return ans;
}
long long query_range(int l, int r){
    return query_node(r) - query_node(l-1);
}
int main(){
    ios::sync_with_stdio(false);
    cin >> n >> m;
    char opt;
    int pos, l, r, num;
    for(int i=1; i<=n; i++){
        cin >> a[i];
        add_node(i, a[i] - a[i-1]);
    }
    
    while(m--){
        cin >> opt;
        if(opt == 'C'){
            cin >> l >> r >> num;
            add_range(l, r, num);
        }
        if(opt == 'Q'){
            cin >> l >> r;
            cout << query_range(l, r) << endl;
        }
    }
    return 0;
}
View Code

 

3、線段樹

每次基本操做(插入或刪除)O(log n),可是能夠在不改變時間複雜度的狀況下修改數據。

(正在更新)咕咕咕

相關文章
相關標籤/搜索