目錄ios
本文參考視頻:https://www.bilibili.com/video/av69667943?from=search&seid=204489840113652018c++
int lowbit(int x) { return x & (-x); }
\(lowbit\) 操做是爲了求出一個數字 \(x\) 在二進制形態下,最低位的 \(1\) 的大小。算法
例如 \((110100)_2\) 中最低位 \(1\) 的大小是 \((100)_2\) 。數組
\(lowbit\) 求解的方法是,先將 \(x\) 的二進制按位取反,而後 \(+1\) ,再按位與原數字。數據結構
例如:\((110100)_2\)ide
- 按位取反 \((001011)_2\)
- \(+1\) \((001100)_2\)
- 按位與原數 \((000100)_2\)
因爲計算機中負數採用補碼存儲,因而第1、二步的操做能夠簡化爲 \(\times (-1)\)函數
那麼,\(lowbit\) 在樹狀數組中的做用究竟是什麼?實際上, \(lowbit(x)\) 表明樹狀數組中第 \(x\) 位元素覆蓋的區間長度,(能夠參考頂部圖片)即 \(t[x] = \sum_{i=x-lowbit(x)+1}^xa[i]\)。(\(t[]\) 表明樹狀數組,\(a[]\) 表明原數組)學習
也就是說,樹狀數組中第 \(x\) 位元素的值表明當前位置到前 \(lowbit(x)\) 位置的全部原數組元素之和。優化
//如下代碼,默認原數組爲 a[],樹狀數組爲 t[] void add(int pos, int x) { //pos位置加上x for (; pos <= n; pos += lowbit(pos)) { //n爲數組大小 t[pos] += x; } }
int query_presum(int pos) { //查詢pos位置的前綴和,即a1 + a2 + ... + apos int ans = 0; for (; pos > 0; pos -= lowbit(pos)) { ans += t[pos]; } return ans; } int query_sum(int l, int r) { //[l, r]區間查詢 return query_presum(r) - query_presum(l - 1); }
樹狀數組中,每一個節點 \(x\) 的父節點均可以表示爲 \(x + lowbit(x)\) 。利用這個性質,咱們就能夠作到 \(O(logn)\) 單點修改。例如,咱們想要給 \(a[3]+1\) ,那麼咱們須要對 \(t[3],t[4],t[8]\) \(+1\) 。spa
區間查詢咱們須要利用前綴和,例如求 \([l, r]\) 的區間和,咱們只需求 \(\sum_{i=1}^ra[i] - \sum_{i=1}^{l-1}a[i]\) 。利用 \(lowbit\) 的性質,咱們知道 \(x\) 位置的元素覆蓋的長度爲 \(lowbit(x)\) 。因而咱們只需每次將下標減去 \(lowbit(x)\) ,將當前位置的數值加上便可。例如 \(presum(7) = t[7] + t[6] + t[4]\) 。
連接:https://www.luogu.com.cn/problem/P3374
#define _CRT_SECURE_NO_WARNINGS #pragma GCC optimize(3) #pragma GCC optimize("Ofast") #pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native") #pragma comment(linker, "/stack:200000000") #include <bits/stdc++.h> #define SIZE 500010 #define rep(i, a, b) for (long long i = a; i <= b; ++i) #define ll long long using namespace std; void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); } int n, m; int t[SIZE]; int lowbit(int x) { return x & (-x); } void add(int pos, int x) { //pos位置加上x for (; pos <= n; pos += lowbit(pos)) { //n爲數組大小 t[pos] += x; } } int query_presum(int pos) { //查詢pos位置的前綴和,即a1 + a2 + ... + apos int ans = 0; for (; pos > 0; pos -= lowbit(pos)) { ans += t[pos]; } return ans; } int query_sum(int l, int r) { //[l, r]區間查詢 return query_presum(r) - query_presum(l - 1); } int main() { io(); cin >> n >> m; rep(i, 1, n) { int x; cin >> x; add(i, x); } rep(i, 1, m) { int op; cin >> op; if (op == 1) { int pos, x; cin >> pos >> x; add(pos, x); } else { int l, r; cin >> l >> r; ll ans = query_sum(l, r); cout << ans << '\n'; } } }
這一部分的建樹與以前不一樣,先前所述的單點修改和區間查詢,咱們只須要對於 \(a[i]\) 創建樹狀數組;可是如今咱們須要對 \(a[i]\) 的差分數組 \(p[i]\) 建樹。
void add(int l, int r, int x) { //[l, r] 區間+x add(l, x); add(r + 1, -x); }
int query_presum(int pos) { //單點查詢,即對差分數組求前綴和 int ans = 0; for (; pos > 0; pos -= lowbit(pos)) { ans += t[pos]; } return ans; }
爲了快速實現區間加和單點查詢操做,咱們須要維護一個差分數組 \(p[i] = a[i] - a[i-1]\) ,而後對 \(p[i]\) 建樹;咱們容易發現,對於差分數組求前綴和,即爲單點查詢:
\(\sum_{i-1}^xp[i]=(a[x]-a[x-1]) + (a[x-1]-a[x-2]) + ... + (a[2]-a[1]) + a[1] = a[x]\)
因而,對於一個差分數組,咱們能夠利用樹狀數組 \(O(logn)\) 求前綴和的性質,實現更快的單點查詢。那麼,如何實現區間修改操做?
咱們不難發現, \(a\) 數組的區間 \([l, r]\) 同時加上一個數值 \(x\) 時,它的差分數組只有首尾兩項的值會發生變化,由於差分數組維護的是相鄰數字的差值,因此一個區間同時加上一個數字時,這個區間中的相鄰數字的差值其實不會改變。因而,咱們只須要對 \(p[l]+x,p[r + 1-x]\) 便可,即進行兩次單點修改。
連接:https://www.luogu.com.cn/problem/P3368
#define _CRT_SECURE_NO_WARNINGS #pragma GCC optimize(3) #pragma GCC optimize("Ofast") #pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native") #pragma comment(linker, "/stack:200000000") #include <bits/stdc++.h> #define SIZE 500010 #define rep(i, a, b) for (long long i = a; i <= b; ++i) #define ll long long using namespace std; void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); } int n, m; int t[SIZE], a[SIZE]; int lowbit(int x) { return x & (-x); } void add(int pos, int x) { //pos位置加上x for (; pos <= n; pos += lowbit(pos)) { //n爲數組大小 t[pos] += x; } } void add(int l, int r, int x) { //[l, r] 區間+x add(l, x); add(r + 1, -x); } int query_presum(int pos) { //單點查詢,即對差分數組求前綴和 int ans = 0; for (; pos > 0; pos -= lowbit(pos)) { ans += t[pos]; } return ans; } int main() { io(); cin >> n >> m; rep(i, 1, n) cin >> a[i]; rep(i, 1, n) { int x = a[i] - a[i - 1]; add(i, x); } rep(i, 1, m) { int op; cin >> op; if (op == 1) { int l, r, x; cin >> l >> r >> x; add(l, r, x); } else { int pos; cin >> pos; ll ans = query_presum(pos); cout << ans << '\n'; } } }
對於單點修改,咱們能夠作到區間查詢;那麼,對於區間修改咱們是否只能作到單點查詢?答案是否認的,咱們仍然能夠經過維護差分數組的方法作到區間查詢。
void add(int pos, int x, int t[]) { //由於要維護兩個數組,加一個參數 for (; pos <= n; pos += lowbit(pos)) { t[pos] += x; } } void add(int l, int r, int x) { //[l, r] 區間+x add(l, x, t1); add(r + 1, -x, t1); add(l, l * x, t2); add(r + 1, -x * (r + 1), t2); } int query_presum(int pos, int t[]) { //單點查詢,即對差分數組求前綴和 int ans = 0; for (; pos > 0; pos -= lowbit(pos)) { ans += t[pos]; } return ans; } int query_sum2(int l, int r) { //區間修改下的區間查詢 int p1 = l * query_presum(l - 1, t1) - query_presum(l - 1, t2); int p2 = (r + 1) * query_presum(r, t1) - query_presum(r, t2); return p2 - p1; }
咱們仍然是從前綴和的角度出發,對於一個區間查詢操做,咱們看做兩次前綴和查詢。
所以咱們考慮求 \(presum(x)=\sum^{x}_{i=1}a[i]=\sum^{x}_{i=1}\sum^{i}_{j=1}p[j]\) 。顯然,這個式子難以計算,咱們須要對它變形:
\(\sum^{x}_{i=1}\sum^{i}_{j=1}p[j]=(x+1)\sum_{i=1}^{x}p[i]-\sum_{i=1}^{x}i\times p[i]\) (這步變換經過幾何意義更容易理解,可參考上文提到的視頻)
對於上述變形,咱們可使用另外一個樹狀數組維護 \(i\times p[i]\) 的前綴和來快速計算這個式子(想想爲何?由於 \(p[i]\) 是一個差分數組,區間修改只會改變兩項數值,所以 \(\times i\) 後,仍然只有首尾兩項變化)。即:
//區間 [l, r] + x 操做時,還須要維護新的差分數組 i * p[i] add1(l, x); add1(r + 1, -x); add2(l, l * x); add2(r + 1, -x * (r + 1)) //add1操做維護 p[i],add2操做維護 i * p[i]
連接:https://www.luogu.com.cn/problem/P3372
#define _CRT_SECURE_NO_WARNINGS #pragma GCC optimize(3) #pragma GCC optimize("Ofast") #pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native") #pragma comment(linker, "/stack:200000000") #include <bits/stdc++.h> #define SIZE 500010 #define rep(i, a, b) for (long long i = a; i <= b; ++i) #define int long long using namespace std; void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); } int n, m; int t1[SIZE], t2[SIZE], a[SIZE]; int lowbit(int x) { return x & (-x); } void add(int pos, int x, int t[]) { //由於要維護兩個數組,加一個參數 for (; pos <= n; pos += lowbit(pos)) { t[pos] += x; } } void add(int l, int r, int x) { //[l, r] 區間+x add(l, x, t1); add(r + 1, -x, t1); add(l, l * x, t2); add(r + 1, -x * (r + 1), t2); } int query_presum(int pos, int t[]) { //單點查詢,即對差分數組求前綴和 int ans = 0; for (; pos > 0; pos -= lowbit(pos)) { ans += t[pos]; } return ans; } int query_sum2(int l, int r) { //區間修改下的區間查詢 int p1 = l * query_presum(l - 1, t1) - query_presum(l - 1, t2); int p2 = (r + 1) * query_presum(r, t1) - query_presum(r, t2); return p2 - p1; } signed main() { io(); cin >> n >> m; rep(i, 1, n) cin >> a[i]; rep(i, 1, n) { int x = a[i] - a[i - 1]; add(i, x, t1); add(i, x * i, t2); } rep(i, 1, m) { int op; cin >> op; if (op == 1) { int l, r, x; cin >> l >> r >> x; add(l, r, x); } else { int l, r; cin >> l >> r; cout << query_sum2(l, r) << '\n'; } } }
連接:https://www.luogu.com.cn/problem/P1908
題解:求逆序對不只能夠歸併排序,還能用樹狀數組解決。因爲數據可能很大,因此咱們須要先對數據離散化。離散化實際上就是創建原數組到一個 \(1,2,3, ..., n\) 的數組的映射關係;例如 24 33 1 99 25
等價於 2 4 1 5 3
。
須要注意的是,原數組中若是有相等的元素,離散化後他們的相對位置不能變化
完成離散化後,咱們考慮如何對離散化數組求逆序對:對於任意一個位置 \(pos\) 的元素而言,咱們須要求的實際上就是在 \(a_{pos}\) 以前而且大於它的元素,聯繫到樹狀數組可以快速維護前綴和的性質,咱們不難發現咱們只須要把某一位置以前全部小於它的元素置爲 \(1\) ,小於它的元素置爲 \(0\) ,就能用前綴和快速計算貢獻。
設離散化後的數組爲 \(p[]\) ,對於這個數組咱們從 \(1\) 到 \(n\) 遍歷,在任意一個位置 \(j\) 作以下操做:
for (int j = 1; j <= n; ++j) { add(j, 1); //單點修改,將 p[j] 置爲 1 ans += j - presum(p[j]); //計算貢獻 }
顯然,對於 \(p_j\) 而言,要統計他的貢獻不須要考慮 \(j\) 位置以後的元素,上方所述的操做就能夠將全部逆序對找到。
#define _CRT_SECURE_NO_WARNINGS #pragma GCC optimize(3) #pragma GCC optimize("Ofast") #pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native") #pragma comment(linker, "/stack:200000000") #include <bits/stdc++.h> #define SIZE 500010 #define rep(i, a, b) for (long long i = a; i <= b; ++i) #define int long long using namespace std; void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); } int n, m, ans; int t[SIZE], a[SIZE]; int pos[SIZE]; //離散化 struct Node { int val; int id; bool operator< (const Node& b) { return (val < b.val) || (val == b.val && id < b.id); } }p[SIZE]; int lowbit(int x) { return x & (-x); } void add(int pos, int x) { for (; pos <= n; pos += lowbit(pos)) { t[pos] += x; } } int query_presum(int pos) { int ans = 0; for (; pos > 0; pos -= lowbit(pos)) { ans += t[pos]; } return ans; } signed main() { io(); cin >> n; rep(i, 1, n) cin >> p[i].val, p[i].id = i; sort(p + 1, p + 1 + n); rep(i, 1, n) pos[p[i].id] = i; rep(i, 1, n) { add(pos[i], 1); ans += i - query_presum(pos[i]); } cout << ans; }
連接:https://www.luogu.com.cn/problem/P1972
題解:剛開始想這道題的時候可能會認爲須要一些可持久化的數據結構維護,事實上咱們只須要經過樹狀數組維護便可。
首先,咱們先要想到能夠經過離線操做使得無序給出的查詢區間有序,使得咱們能夠避免重複更新區間。先將全部區間讀入,而後以區間右端點爲關鍵字排序。而後咱們維護一個樹狀數組,來記錄貝殼的種類數量;可是某個貝殼重複出現怎麼辦?事實上對於重複出現的貝殼,咱們只須要考慮最右邊的貝殼:例如 1 2 3 1 2
,其實是 0 0 1 1 1
。更新過程以下:(自上而下對應 \(5\) 次更新)
1 0 0 0 0 1 1 0 0 0 1 1 1 0 0 0 1 1 1 0 0 0 1 1 1
爲了實現這個過程,咱們還須要一個數組來記錄某種貝殼是否先前出現過,以及它出現的位置。因而,咱們就能夠對於每一個詢問區間更新到它的右端點,而且只保留最後出現的貝殼。這樣,對於每次詢問的區間 \([l,r]\) ,咱們只須要記錄 \(query\)_\(sum(l,r)\) 便可
#define _CRT_SECURE_NO_WARNINGS #pragma GCC optimize(3) #pragma GCC optimize("Ofast") #pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native") #pragma comment(linker, "/stack:200000000") #include <bits/stdc++.h> #define SIZE 500010 #define rep(i, a, b) for (long long i = a; i <= b; ++i) #define int long long using namespace std; void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); } int n, m, k, ans, nxt; int t[SIZE], a[SIZE]; int Map[SIZE]; int lowbit(int x) { return x & (-x); } struct Node { int l, r; int id; bool operator< (const Node& b) { return (r < b.r) || (r == b.r && l < b.l); } }p[SIZE]; void add(int pos, int x) { for (; pos <= n; pos += lowbit(pos)) { t[pos] += x; } } int query_presum(int pos) { int ans = 0; for (; pos > 0; pos -= lowbit(pos)) { ans += t[pos]; } return ans; } int query_sum(int l, int r) { return query_presum(r) - query_presum(l - 1); } signed main() { io(); cin >> n; rep(i, 1, n) cin >> a[i]; cin >> m; rep(i, 1, m) cin >> p[i].l >> p[i].r, p[i].id = i; sort(p + 1, p + 1 + m); nxt = 1; vector<int> vec(m + 1); rep(i, 1, m) { rep(j, nxt, p[i].r) { if (Map[a[j]]) add(Map[a[j]], -1); add(j, 1); Map[a[j]] = j; } nxt = p[i].r + 1; vec[p[i].id] = query_sum(p[i].l, p[i].r); } rep(i, 1, m) cout << vec[i] << '\n'; }
連接:https://www.luogu.com.cn/problem/P5673
題解:顯然,本題能夠看做是前一題的升級版。不一樣點在於,上一題一個區間內不能存在相同元素;而這一題能夠從右往左存在 \(k\) 個相同元素,處理方法和前一題相似。
#define _CRT_SECURE_NO_WARNINGS #pragma GCC optimize(3) #pragma GCC optimize("Ofast") #pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native") #pragma comment(linker, "/stack:200000000") #include <bits/stdc++.h> #define SIZE 500010 #define rep(i, a, b) for (long long i = a; i <= b; ++i) #define int long long using namespace std; void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); } int n, m, k, ans, nxt; int t[SIZE], a[SIZE], v[SIZE]; vector<int> q[SIZE]; vector<int> vec(SIZE >> 1); int lowbit(int x) { return x & (-x); } struct Node { int l, r; int id; bool operator< (const Node& b) { return (r < b.r) || (r == b.r && l < b.l); } }p[SIZE]; void add(int pos, int x) { for (; pos <= n; pos += lowbit(pos)) { t[pos] += x; } } int query_presum(int pos) { int ans = 0; for (; pos > 0; pos -= lowbit(pos)) { ans += t[pos]; } return ans; } int query_sum(int l, int r) { return query_presum(r) - query_presum(l - 1); } int main() { io(); cin >> n >> m >> k; --k; rep(i, 1, n) cin >> a[i]; rep(i, 1, n) cin >> v[i], add(i, v[i]); rep(i, 1, m) { cin >> p[i].l >> p[i].r; p[i].id = i; } sort(p + 1, p + 1 + m); nxt = 1; rep(i, 1, m) { rep(j, nxt, p[i].r) { if (q[a[j]].size() >= k) { add(q[a[j]][0], -v[q[a[j]][0]]); q[a[j]].erase(q[a[j]].begin()); } q[a[j]].emplace_back(j); } nxt = p[i].r + 1; vec[p[i].id] = query_sum(p[i].l, p[i].r); } rep(i, 1, m) cout << vec[i] << '\n'; }
連接:https://www.luogu.com.cn/problem/P3369
題解:首先確定要離線操做,而後離散化,注意操做 \(4\) 不須要離散化。
單點加減操做咱們已經很熟悉了,只須要分別對於元素所在位置 \(+1\) 和 \(-1\) 便可。
接着就是本題的核心操做求元素排名,第 \(k\) 大元素和前驅後繼。爲了方便表述,咱們將離散化後的數組表示爲 \(a[]\) 。
那麼求元素 \(a[pos]\) 的排名就變得至關簡單了,注意到增刪元素只是 \(±1\) ,所以 \(a[pos]\) 的排名即爲 \(query\)_\(presum(a[pos] - 1) + 1\) ,即求出全部比它小的元素數量而後 \(+1\) 。能夠注意的一點是,求逆序對的操做就是求排名。
對於第 \(k\) 大元素,注意到樹狀數組的二進制特徵,咱們可使用倍增快速找到其位置(不熟悉倍增思想能夠回想一下快速冪的實現。因爲樹狀數組的 \(lowbit\) 構成特徵,咱們能夠經過倍增優化算法而不是二分查找)。具體實現能夠參考代碼中的 \(kth()\) 函數,而且結合樹狀數組的構成圖理解。
有了上面的兩種思想,咱們不難發現求前驅和後繼就是上述操做的綜合,先求出元素 \(a[pos]\) 的排名 \(rank_{a[pos]}\) ,前驅和後繼就能分別表示爲第 \(rank_{a[pos]}-1\) 和 \(rank_{a[pos]} + 1\) 大元素。
#define _CRT_SECURE_NO_WARNINGS #pragma GCC optimize(3) #pragma GCC optimize("Ofast") #pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native") #pragma comment(linker, "/stack:200000000") #include <bits/stdc++.h> #define SIZE 500010 #define rep(i, a, b) for (long long i = a; i <= b; ++i) #define int long long using namespace std; void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); } int n, m, k, ans, cnt; int t[SIZE], op[SIZE], a[SIZE], p[SIZE]; int lowbit(int x) { return x & (-x); } void add(int pos, int x) { for (; pos <= n; pos += lowbit(pos)) { t[pos] += x; } } int query_presum(int pos) { int ans = 0; for (; pos > 0; pos -= lowbit(pos)) { ans += t[pos]; } return ans; } int query_sum(int l, int r) { return query_presum(r) - query_presum(l - 1); } int kth(int k) { int ans = 0, cnt = 0; for (int i = 20; i >= 0; i--) { ans += (1 << i); if (ans > n || cnt + t[ans] >= k) ans -= (1 << i); else cnt += t[ans]; } return ++ans; } int main() { io(); cin >> n; rep(i, 1, n) { cin >> op[i] >> a[i]; if (op[i] != 4) p[++cnt] = a[i]; } sort(p + 1, p + 1 + cnt); rep(i, 1, n) { //離散化 if (op[i] != 4) { a[i] = lower_bound(p + 1, p + 1 + cnt, a[i]) - p; } } rep(i, 1, n) { if (op[i] == 1) add(a[i], 1); else if (op[i] == 2) add(a[i], -1); else if (op[i] == 3) cout << query_presum(a[i] - 1) + 1 << '\n'; else if (op[i] == 4) cout << p[kth(a[i])] << '\n'; else if (op[i] == 5) cout << p[kth(query_presum(a[i] - 1))] << '\n'; else cout << p[kth(query_presum(a[i]) + 1)] << '\n'; } }
因爲硬盤損壞,許多數據丟失,保留的筆記先掛到博客上。