已更新(2/3):st表、樹狀數組node
st表、樹狀數組與線段樹是三種比較高級的數據結構,大多數操做時間複雜度爲O(log n),用來處理一些RMQ問題或相似的數列區間處理問題。ios
st表預處理時間複雜度O(n log n),查詢O(1),但不支持在線更改,不然要從新進行預處理。c++
使用一個二維數組:st[i][j]存儲i爲起點,長度爲2j的一段區間最值,即arr[i, i + 2j - 1]。算法
具體步驟(以最小值爲例):數組
總時間複雜度爲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; }
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數組)
樹狀數組是一種樹狀的結構(廢話),可是隻須要 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 - 2k + 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; }
那麼,如何讓線段樹支持區間更改與單點查詢呢?
設數組 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; }
簡單談一下區間查詢與區間修改的操做:
(本段參考了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; }
每次基本操做(插入或刪除)O(log n),可是能夠在不改變時間複雜度的狀況下修改數據。
(正在更新)咕咕咕