【2024-ZR-C Day 5】資料結構(3):莫隊(帶修莫隊、回滾莫隊)、邊分治、點分治、樹分治、動態點分治

心灵震荡發表於2024-07-22

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\).

image

此時,每次只需列舉三條邊。

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)\).

相關文章