[演算法] 一些分治

Peppa_Even_Pig發表於2024-08-06

普通分治

其實沒啥,每次只計算跨越分治中心的區間的貢獻,剩下的遞迴到左右兩邊進行分治;

時間複雜度:分治樹高度為 $ \Theta (\log n) $,乘上其他操作的複雜度即可;

例題一:現在有一個 $ n $ 階排列 $ a $,計算:

\[ \sum^{n}_{i = 1} \sum^{n}_{j = i} \min(a_i, a_{i + 1} ,..., a_j) \]

其中 $ n \leq 200000 $

題意簡述:找一個給定的序列的所有子區間的最小值的和;

可以線性做,對於每一個 $ a_i $,記錄其向左和向右第一個小於它的值,計算一下即可;

這裡講一下分治的做法;

其實對於這種求所有區間中符合條件的區間的題目,一般都可以分治做;

考慮跨過分治中心的區間,設分治中心為 $ mid $, 我們可以從分治中心向左維護出對於任意一個左端點 $ l $,區間 $ [l, mid] $ 的最小值並存放在一個陣列 $ b $ 中;

處理完上述步驟後,我們開始從分治中心向右遍歷右端點,每遍歷到一個右端點 $ r $,我們發現它的所有跨過分治中心的區間的最小值有兩種情況:

  1. 是區間 $ [mid, r] $ 中的值;

  2. 是 $ mid $ 左邊的值;

對於第一種情況,我們每次遍歷時維護一個最小值 $ mi $ 即可;

對於第二種情況,暴力做法肯定不行,那怎麼辦呢?

挖掘一下性質,我們發現 $ b $ 陣列中的值是非嚴格單調遞減的(因為最小值只能不變或者更小,不會變大);

所以,我們可以每次遍歷右端點時用二分查詢找出 $ b $ 陣列中第一個大於 $ mi $ 的位置,然後小於等於它的最小值不變,大於它的最小值變為 $ mi $;

為了避免手寫二分(懶,不想寫),我們可以將 $ b $ 陣列 $ reverse $ 一下,然後用 $ upper \ bound $ 即可;

當然,還要維護一個字首和(注意是 $ reverse $ 後的);

時間複雜度:分治 + 遍歷 $ \Theta(n \log n) $,二分查詢 $ \Theta(\log n) $,總的 $ \Theta(n \log^2 n) $ (時間複雜度確實劣了一些);

點選檢視程式碼
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
long long n;
long long a[500005];
long long b[500005], sum[500005];
long long c[500005];
long long ans;
void solve(long long l, long long r) {
	if (l >= r) return;
	if (r - l + 1 == 2) {
		ans += min(a[l], a[r]);
		return;
	}
	long long mid = (l + r) >> 1;
	long long o = 0;
	for (int i = 0; i <= mid - l; i++) b[i] = 0x3f3f3f3f;
	for (long long i = mid - 1; i >= l; i--) {
		o++;
		b[o] = min(b[o - 1], a[i]);
	}
	long long cnt = 0;
	for (long long i = o; i >= 1; i--) {
		c[++cnt] = b[i];
	}
	for (long long i = 0; i <= cnt; i++) {
		sum[i] = 0;
	}
	for (long long i = 1; i <= cnt; i++) {
		sum[i] = sum[i - 1] + c[i];
	}
	long long mi = a[mid];
	for (long long j = mid + 1; j <= r; j++) {
		mi = min(mi, a[j]);
		long long pos = upper_bound(c + 1, c + 1 + cnt, mi) - c;
		if (pos <= cnt) ans += (cnt - pos + 1) * mi;
		ans += sum[pos - 1];
	}
	solve(l, mid);
	solve(mid, r);
}
int main() {
	cin >> n;
	for (long long i = 1; i <= n; i++) {
		cin >> a[i];
	}
	solve(1, n);
	for (long long i = 1; i <= n; i++) ans += a[i];
	cout << ans;
	return 0;
}

其實很多分治的套路是維護字首和 + 發現性質,做的時候可以注意一下;

例題二: Luogu P4062 [Code+#1] Yazid 的新生舞會

這題貌似題解中的主流做法是用資料結構維護高階字首和,這裡講一下分治做法;

還是求所有區間中符合條件的區間,可以考慮分治;

找區間中的絕對眾數,我們可以借鑑一下摩爾投票法,設現在我們考慮的眾數為 $ x $,將不是 $ x $ 的數看為 $ -1 $,是 $ x $ 的數看為 $ 1 $,最後判斷一下整個區間的和與 $ 0 $ 的關係即可;

首先,對於一個區間 $ [l, r] $,其絕對眾數是 $ [l, k] $ 的絕對眾數或 $ [k + 1, r] $ 的絕對眾數,其中 $ l \leq k \leq r $;

所以可以令 $ k = mid $,然後分治求解;

分治時,還是先從分治中心向左找符合條件的絕對眾數以及區間和所出現的次數,向右遍歷時統計一下區間和是否 $ > 0 $ 即可;

由於我們要遍歷絕對眾數,而絕對眾數最多變化 $ \Theta (\log n) $ 次(相當於每次砍一半才能更新一次絕對眾數),所以總的時間複雜度為 $ \Theta (n \log^2 n) $;

寫的比較粗略,可以參考一下原題解區的題解;

點選檢視程式碼
#include <iostream>
#include <cstdio>
using namespace std;
int n, ddd;
int a[1000005];
long long ans;
int pos[1000005], vis[1000005], num[1000005], cnt[1000005];
void solve(int l, int r) {
	if (l == r) {
		ans++;
		return;
	}
	int mid = (l + r) >> 1;
	num[0] = 0;
	for (int i = mid; i >= l; i--) {
		if (++cnt[a[i]] > (mid - i + 1) / 2) {
			if (!pos[a[i]]) {
				pos[a[i]] = ++num[0];
				num[pos[a[i]]] = a[i];
			}
		}
	}
	for (int i = mid + 1; i <= r; i++) {
		if (++cnt[a[i]] > (i - mid) / 2) {
			if (!pos[a[i]]) {
				pos[a[i]] = ++num[0];
				num[pos[a[i]]] = a[i];
			}
		}
	}
	for (int i = l; i <= r; i++) {
		pos[a[i]] = 0;
		cnt[a[i]] = 0;
	}
	for (int i = 1; i <= num[0]; i++) {
		int sum = r - l + 1;
		int ma = sum;
		int mi = sum;
		cnt[sum] = 1;
		for (int j = l; j < mid; j++) {
			if (a[j] == num[i]) {
				sum++;
			} else {
				sum--;
			}
			ma = max(ma, sum);
			mi = min(mi, sum);
			cnt[sum]++;
		}
		if (a[mid] == num[i]) {
			sum++;
		} else {
			sum--;
		}
		for (int j = mi; j <= ma; j++) {
			cnt[j] += cnt[j - 1];
		}
		for (int j = mid + 1; j <= r; j++) {
			if (a[j] == num[i]) {
				sum++;
			} else {
				sum--;
			}
			ans += cnt[min(sum - 1, ma)];
		}
		for (int j = mi; j <= ma; j++) {
			cnt[j] = 0;
		}
	}
	solve(l, mid);
	solve(mid + 1, r);
}
int main() {
	cin >> n >> ddd;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	solve(1, n);
	cout << ans;
	return 0;
}

貓樹分治

實話說,我就做過一個關於這個的題,感覺和普通分治沒有什麼區別;

直接粘我以前寫的題解了(出處):

Luogu P6240 好吃的題目

暴力一:每次跑一邊 $ DP $;

暴力二:使用揹包的合併操作,時間複雜度 $ \Theta(n^2) $ ?;

正解:貓樹分治;

這玩意聽著這麼像資料結構,其實就是一個套路;

好像它的發明者受到了線段樹分治的啟發?

和普通的分治沒什麼區別,難的是想到分治(所以才給它起了個名字嘛);

每次只計算跨過分治中心的區間,首先預處理出從分治中心向左和向右的每個點到終點這段區間的所有 $ 200 $ 個最優值,然後進行合併,注意要將不跨過分治中心的區間篩選出來,分別放在左右兩邊,然後繼續遞迴;

所以我們需要四個指標,兩個記錄現在處理的序列上的左右端點,另外兩個記錄現在處理的問題的區間(這裡的 “區間” 並不絕對,只要是沒處理的,都能出現在這一段區間),然後正常遞迴即可;

時間複雜度:$ T(200n \log n) $;

點選檢視程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n, m;
int h[500005], w[500005];
struct sss{
	int l, r, t;
}b[500005];
int ans[500005], p[500005], s[500005], ncnt;
int f[50005][205];
void solve(int l, int r, int L, int R) {
	if (L > R) return;
	int mid = (l + r) >> 1;
	int Mid = L - 1;
	for (int i = 0; i <= 200; i++) f[mid][i] = 0;
	for (int i = mid + 1; i <= r; i++) {
		for (int j = 0; j < h[i]; j++) f[i][j] = f[i - 1][j];
		for (int j = h[i]; j <= 200; j++) {
			f[i][j] = max(f[i - 1][j], f[i - 1][j - h[i]] + w[i]);
		}
	}
	for (int i = h[mid]; i <= 200; i++) f[mid][i] = w[mid];
	for (int i = mid - 1; i >= l; i--) {
		for (int j = 0; j < h[i]; j++) f[i][j] = f[i + 1][j];
		for (int j = h[i]; j <= 200; j++) {
			f[i][j] = max(f[i + 1][j], f[i + 1][j - h[i]] + w[i]);
		}
	}
	ncnt = 0;
	int u = 0;
	for (int i = L; i <= R; i++) {
		u = p[i];
		if (b[u].r <= mid) p[++Mid] = u;
		else if (mid < b[u].l) s[++ncnt] = u;
		else {
			int ret = 0;
			for (int i = 0; i <= b[u].t; i++) {
				ret = max(ret, f[b[u].l][i] + f[b[u].r][b[u].t - i]);
			}
			ans[u] = ret;
		}
	}
	for (int i = 1; i <= ncnt; i++) {
		p[Mid + i] = s[i];
	}
	R = ncnt + Mid;
	solve(l, mid, L, Mid);
	solve(mid + 1, r, Mid + 1, R);
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m;
	for (register int i = 1; i <= n; i++) {
		cin >> h[i];
	}
	for (register int i = 1; i <= n; i++) {
		cin >> w[i];
	}
	for (register int i = 1; i <= m; i++) {
		cin >> b[i].l >> b[i].r >> b[i].t;
		if (b[i].l == b[i].r) {
			if (b[i].t >= h[b[i].l]) ans[i] = w[b[i].l];
		} else {
			p[++ncnt] = i;
		}
	}
	solve(1, n, 1, ncnt);
	for (int i = 1; i <= m; i++) {
		cout << ans[i] << endl;
	}
	return 0;
}

線段樹分治

在區間上的操作,線段樹好像都能幹,並且它長得就很能分治,所以用它也並不奇怪;

Luogu P5787 二分圖 /【模板】線段樹分治

看見這種在時間線上的題,一般好像可以用線段樹分治來做;

以前好像還有一道,但是忘了;

首先判斷二分圖,我們使用可撤銷的擴充套件域並查集,具體可以看看原題解區;

具體的,我們對於這一條時間線開一個線段樹,每個節點開一個動態陣列存這個點所管轄的時間段內所加邊的下標,最後從根開始 $ dfs $ 一下即可;

點選檢視程式碼
#include <iostream>
#include <cstdio>
#include <vector>
#include <stack>
using namespace std;
int n, m, k;
stack<pair<int, pair<int, int> > > s;
int fa[500005];
int u[500005], t[500005];
int siz[500005];
int find(int x) {
	return (x == fa[x]) ? x : find(fa[x]);
}
namespace seg{
	inline int ls(int x) {
		return x << 1;
	}
	inline int rs(int x) {
		return x << 1 | 1;
	}
	struct sss{
		int l, r;
		vector<int> v;
	}tr[500005];
	void bt(int id, int l, int r) {
		tr[id].l = l;
		tr[id].r = r;
		if (l == r) {
			return;
		}
		int mid = (l + r) >> 1;
		bt(ls(id), l, mid);
		bt(rs(id), mid + 1, r);
	}
	void add(int id, int l, int r, int d) {
		if (tr[id].l >= l && tr[id].r <= r) {
			tr[id].v.push_back(d);
			return;
		}
		int mid = (tr[id].l + tr[id].r) >> 1;
		if (l <= mid) add(ls(id), l, r, d);
		if (r > mid) add(rs(id), l, r, d);
	}
}
void merge(int x, int y) {
	if (x == y) return;
	if (siz[x] > siz[y]) swap(x, y);
	s.push({y, {siz[x], x}});
	fa[x] = y;
	siz[y] += siz[x];
}
void dfs(int id) {
	bool vis = true;
	int o = s.size();
	for (int i = 0; i < seg::tr[id].v.size(); i++) {
		int x = seg::tr[id].v[i];
		int uu = find(u[x]);
		int tt = find(t[x]);
		if (uu == tt) {
			for (int j = seg::tr[id].l; j <= seg::tr[id].r; j++) cout << "No" << endl;
			vis = false;
			break;
		}
		merge(find(u[x] + n), tt);
		merge(find(t[x] + n), uu);
	}
	if (vis) {
		if (seg::tr[id].l == seg::tr[id].r) {
			cout << "Yes" << endl;
		} else {
			dfs(seg::ls(id));
			dfs(seg::rs(id));
		}
	}
	while(s.size() > o) {
		siz[s.top().first] -= s.top().second.first;
		fa[s.top().second.second] = s.top().second.second;
		s.pop();
	}
}
int main() {
	cin >> n >> m >> k;
	seg::bt(1, 1, k);
	int l, r;
	for (int i = 1; i <= m; i++) {
		cin >> u[i] >> t[i] >> l >> r;
		if (l != r) {
			seg::add(1, l + 1, r, i);
		}
	}
	for (int i = 1; i <= 2 * n; i++) {
		fa[i] = i;
		siz[i] = 1;
	}
	dfs(1);
	return 0;
}

可能以後看見了新的套路等等還會再來補充;

相關文章