1. 莫隊(Mo-Queue)
1.1. 普通莫隊
將詢問按第一關鍵字為左端點塊編號,第二關鍵字為右端點排序。
從 \(l=1, r=0\) 開始,對每個詢問依次讓 \(l, r\) 移動到詢問的左右端點上,期間動態維護當前區間的答案。
為避免重複操作,最好保證時時刻刻都有 \(l + 1 \le r\).
若設塊長為 \(T\),則時間複雜度為 \(O\left(qT + \frac{n}{T}\right)\).
本質上是二維的旅行商問題。
1.2. 帶修莫隊
給莫隊增加一個時間維度,一個區間記為 \([l, r, t]\).
可以認為,序列的值是隨著時間而變化的。
在處理每個區間時,先移動 \([l, r]\),再維護 \(t\) 的變化造成的序列中值的變化。
具體地,每次移動 \(t\),若當前的 \(t\) 時間有修改操作,且操作區間在 \([l, r]\) 內,則進行修改。
1.3. 回滾莫隊
不能處理加數/刪數的其中一種情況時使用。
例如,若莫隊不能處理刪數操作:
先按照同塊按 \(Q_i.r\) 為關鍵字,否則以 \(Q_i.l\) 為關鍵字將所有區間從小到大排序,再分塊,然後對於每個塊進行操作。
對於每個塊 \(i\):先將 \(l\) 設為 \(R_i + 1\),\(r\) 設為 \(R_i\),對左端點在該塊內的所有詢問進行處理。
做每個詢問時,
- 若 \({Q_i}.r \le R_i\),說明該詢問的區間長度不大於塊長 \(T\),可以直接暴力.
- 若 \({Q_i}.r > R_i\),移動 \(r\) 直到 \(r = {Q_i}.r\) 停止,移動 \(l\) 直到 \(l = {Q_i}.l\).
這樣做,就沒有了刪數的操作。
每次詢問後將 \(l\) 移回 \(R_i + 1\).
可以證明,當 \(T\) 取 \(\sqrt n\) 時,時間複雜度最優,為 \(O\left(q \sqrt{n}\right)\).
2. 整體二分
對一個長度為 \(n\) 的序列 ,有 \(q\) 次詢問,每次詢問區間 \([l, r]\) 中第 \(k\) 小的數。
\(n\) 個事件分別為:出現一個數 \(a_i\).
考慮從小到大對 \(a_i\) 排序,則每個詢問達成的條件為“在某次事件後,區間內恰好有 \(k\) 個數”,這第 \(k\) 個數就是第 \(k\) 小。
可以發現,所有區間在不同時刻上的數的個數都滿足單調性,所以可以放在一起二分。
每輪把所有詢問的 \(mid\) 和所有 \(a_i\) 放在一起排序。
問題轉化成:單點修改,區間求和。
void solve(int l, int r, vector<int> events)
{
int mid = (l + r) >> 1;
已知:[1, l - 1] 內部發生事件對所有詢問的影響
考慮 [l, mid] 內部發生事件對所有詢問的影響。
}
3. 邊分治
選擇一條邊 \(E\),利用 \(O(n)\) 的時間處理經過邊 \(E\) 的情況,然後處理不經過邊 \(E\) 的情況。
不經過邊 \(E\) 等價於把 \(E\) 這條邊斷開,將樹分裂成兩棵子樹後遞迴處理。
時間複雜度 \(T(n) = T(a) + T(b) + O(n)\),其中 \(a, b\) 分別是分裂後兩棵子樹的點數。
每次需要找到一條邊 \(E\) 使得分裂後子樹點數的最大值儘可能小,而顯然是和重心相連的。
設重心為 \(u\),利用 \(O(d_u)\) 的時間暴力列舉每條邊,找到最佳的邊即可。
但以上的操作會在菊花圖中被卡成 \(O(n^2)\),所以考慮奇怪的最佳化。
將這棵樹三度化,即新增虛點使得每個點度數不超過 \(3\).
此時,每次只需列舉三條邊。
4. 點分治
選擇一個點 \(x\),處理經過點 \(x\) 的情況,然後處理不經過點 \(x\) 的情況。
不經過點 \(x\) 等價於把 \(x\) 這個點刪掉,將樹分裂成 \(d_x\) 棵子樹後遞迴處理。
需要找到一個點 \(x\),使得去掉 \(x\) 後每棵子樹點數的最大值儘可能小。
這個 \(x\) 實際上就是樹的重心。
點分治的核心是,將一條路徑拆成兩個點到重心的路徑。
總時間複雜度為 \(O(n \log n)\).
5. 點分樹
一顆以原樹的重心為根的樹,每個節點的兒子都是其在原樹上所有子樹的重心。
根據點分治的性質,樹的高度約為 \(O(\log n)\),即每個點只會被 \(O(\log n)\) 個重心管轄。
把點分治的過程中所有的分治結構(即點分樹上以每個重心為根的所有兒子與答案)記錄下來,在每個分治結構中按到重心的距離用樹狀陣列維護。
struct problem
{
int coef;
vector<int> nodes, c; // nodes 是按權值排序後的所有節點,c 是上面提到的樹狀陣列
long long calc(int mid); // 求解 d[x] + d[y] <= mid 的 (x, y) 對數
} p[N << 1];
對於每次修改/查詢,列舉所有經過 \(x\) 的分治結構(對應的是點分樹上根到 \(x\) 路徑上的所有點),在對應樹狀陣列中進行操作。
為了防止相同子樹的貢獻多算,需要再對每個子樹維護樹狀陣列來減掉。
6. 例題
6.1. LG1903 [國家集訓隊] 數顏色 / 維護佇列
#include<bits/stdc++.h>
using namespace std;
#define N 233333
#define M 1111111
int cur, mp[M], a[N], ans[N], tot = 0, cnt = 0, n, m, bsiz;
struct node
{
int l, r, t, id;
bool operator < (const node &nd) const
{
int la = l / bsiz, lb = nd.l / bsiz;
int ra = r / bsiz, rb = nd.r / bsiz;
return (la != lb ? la < lb : (ra != rb ? ra < rb : t < nd.t));
}
} q[N], p[N];
inline void add(int x) {cur += !mp[x]++;}
inline void del(int x) {cur -= !--mp[x];}
inline void update(int x, int t)
{
if (q[x].l <= p[t].l && p[t].l <= q[x].r)
del(a[p[t].l]), add(p[t].r);
swap(a[p[t].l], p[t].r);
}
int main()
{
cin >> n >> m; bsiz = pow(n, 0.666);
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= m; i++)
{
char opt;
int l, r;
cin >> opt >> l >> r;
if (opt == 'Q') ++tot, q[tot].id = tot, q[tot].l = l, q[tot].r = r, q[tot].t = cnt;
else p[++cnt].l = l, p[cnt].r = r;
}
sort(q + 1, q + tot + 1);
int l = 1, r = 0, t = 0;
for(int i = 1; i <= tot; i++)
{
while(l > q[i].l) add(a[--l]);
while(l < q[i].l) del(a[l++]);
while(r > q[i].r) del(a[r--]);
while(r < q[i].r) add(a[++r]);
while(t > q[i].t) update(i, t--);
while(t < q[i].t) update(i, ++t);
ans[q[i].id] = cur;
}
for(int i = 1; i <= tot; i++) cout << ans[i] << '\n';
return 0;
}
6.2. LG6326 Shopping
對這棵樹進行點分治,重心要麼選,要麼不選。
- 考慮某個點必選的情況,只要以這個點為根進行樹形依賴揹包即可。
考慮按 DFS 序進行 DP,有 \(f_{i, j} \to f_{i + 1, k}\) 和 \(f_{i, j} \to f_{i + size_u, k}\) 兩種轉移,可以做到 \(O(nm \log d)\). - 若某個點必然不選,那麼去掉這個點也無妨,樹將會分裂成若干獨立的子樹,遞迴處理即可。
時間複雜度 \(O(nm \log n)\).