整體二分

dingzibo_qwq發表於2024-05-05

1 概念

在很多題目中,我們可以使用二分法來得出答案。但是如果說這一類題目有多次詢問,並且多次詢問分別二分會 TLE 時,我們就需要引入一個東西叫整體二分。

整體二分的主要思路就是將多個查詢一起解決,因此它是一種離線演算法。

整體二分的具體操作步驟如下:

首先記 \([l,r]\) 為答案的值域,\([L,R]\) 是答案的定義域。這代表著我們在求答案時考慮下標在 \([L,R]\) 上的操作,這當中的詢問的答案都在 \([l,r]\)

首先我們現將所有操作按照時間軸存入陣列,然後開始分治。在每一層分治中,我們利用一些東西統計當前查詢的答案和 \(mid\) 的關係。

根據這個關係(小於等於 \(mid\) 和大於 \(mid\)),我們將操作序列分成兩半,然後遞迴處理。

那麼我們透過例題來具體瞭解整體二分的過程。

2 基礎例題

2.1 靜態全域性第 k 小

在一個數列中查詢第 \(k\) 小的數。

顯然我們可以直接排序。那如果用二分呢?我們可以二分數字,然後查詢這個數字的排名;這樣看上去有點多此一舉,我們看下一題。

在一個數列中多次查詢第 \(k\) 小的數。

我們可以分開二分,但是也可以放在一起二分。

首先我們可以假設當前所有詢問的答案都是 \(mid\),然後我們一次判斷真正的答案與 \(mid\) 的關係。也就是應該小於等於 \(mid\) 還是大於 \(mid\),並分成兩個部分。假如原先我們查詢的值域為 \([l,r]\),那麼現在兩個區間的值域就是 \([l,mid],(r,mid]\)。在值域裡繼續二分查詢,直到 \(l=r\)

可以理解為我們本來是一個一個二分,現在我們將他們放到一起同時做,這樣可以省去當中重複運算的時間。

2.2 靜態區間第 k 小

我們來看一道模板題:【模板】可持久化線段樹 2。我們發現這是一道靜態查詢區間第 \(k\) 小問題,可以考慮整體二分。

我們在每一次二分中利用樹狀陣列記錄下當前區間內小於等於 \(mid\) 的數有哪些,用這個來幫助計算區間中小於等於指定數的數量。同時,為了提高效率,我們可以在統計時只對值域在 \([l,r]\) 之間的數進行統計,將他們單獨拿出來之後在上面做二分。

時間複雜度 \(O(n\log ^2 n)\),比主席樹 \(O(n\log n)\) 較劣。但是仍然可以過掉上面的模板題。

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
const int Maxn = 5e5 + 5;

int n, m;
int mn = 2e9, mx;
struct node {
	int opt, l, r, k, id;
}q[Maxn], q1[Maxn], q2[Maxn];

int tot = 0;
int ans[Maxn];

struct BIT {
	int c[Maxn];
	int lowbit(int x) {
		return x & (-x);
	}
	void mdf(int x, int val) {
		for(int i = x; i <= n; i += lowbit(i)) {
			c[i] += val;
		}
	}
	int query(int x) {
		int sum = 0;
		for(int i = x; i; i -= lowbit(i)) {
			sum += c[i];
		}
		return sum;
	}
}B;

void obs(int l, int r, int pl, int pr) {
    //[l,r] 是答案值域,[pl,pr] 是當前二分的查詢區間
	if(pl > pr) return ;
	if(l == r) {
		for(int i = pl; i <= pr; i++) {//答案全部為 l
			if(q[i].opt == 2) {
				ans[q[i].id] = l;
			}
		}
		return ;
	}
	int mid = (l + r) >> 1, p1 = 0, p2 = 0;
	for(int i = pl; i <= pr; i++) {
		if(q[i].opt == 1) {//是修改操作
			if(q[i].k <= mid) {//與 mid 比較
				B.mdf(q[i].id, 1);//更新樹狀陣列
				q1[++p1] = q[i];//比 mid 小的放到左半部分
			}
			else {
				q2[++p2] = q[i];//比 mid 大的放到右半部分
			}
		}
		else {
			int x = B.query(q[i].r) - B.query(q[i].l - 1);//查詢當前區間內 mid 的排名
			if(q[i].k <= x) {
				q1[++p1] = q[i];//比 mid 小的放到左半部分
			} 
			else {
				q[i].k -= x;//注意右半部分在計算之前要減掉左半部分的貢獻
				q2[++p2] = q[i];//比 mid 大的放到右半部分
			}
		}
	}
	for(int i = 1; i <= p1; i++) {
		if(q1[i].opt == 1) {
			B.mdf(q1[i].id, -1);
		}
	}
	for(int i = 1; i <= p1; i++) {
		q[pl + i - 1] = q1[i];
	}
	for(int i = 1; i <= p2; i++) {
		q[pl + p1 + i - 1] = q2[i];
	}
	obs(l, mid, pl, pl + p1 - 1);
	obs(mid + 1, r, pl + p1, pr);//分治求解
}

int main() {
	ios::sync_with_stdio(0);
	cin >> n >> m;
	for(int i = 1; i <= n; i++) {
		int p;
		cin >> p;
		mn = min(mn, p), mx = max(mx, p);
		q[++tot] = {1, -1, -1, p, i};
	}
	for(int i = 1; i <= m; i++) {
		int l, r, k;
		cin >> l >> r >> k;
		q[++tot] = {2, l, r, k, i};
	}
	obs(mn, mx, 1, tot);
	for(int i = 1; i <= m; i++) {
		cout << ans[i] << '\n';
	}
	return 0;
}

二維區間最小值例題:[國家集訓隊] 矩陣乘法

2.3 帶修區間第 k 小

例題:Dynamic Rankings

我們發現這樣一個問題:上面我們求靜態區間第 k 小的時候已經將初始序列當做了插入操作,那麼我們再做帶修區間第 k 小的時候應該比較容易。

首先,一次修改操作可以看做是一次刪除和一次插入操作組成的。而刪除與查詢操作本質上都是一樣的,無非就是在樹狀陣列上加一減一的區別。

程式碼與靜態區間第 k 小的非常相似,如下:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
const int Maxn = 5e5 + 5;

int n, m, a[Maxn];
int mn = 2e9, mx;
struct node {
	int opt, l, r, k, id;
}q[Maxn], q1[Maxn], q2[Maxn];

int tot, cnt;

struct BIT {
	int c[Maxn];
	int lowbit(int x) {
		return x & (-x);
	}
	void mdf(int x, int val) {
		for(int i = x; i <= n; i += lowbit(i)) {
			c[i] += val;
		}
	}
	int query(int x) {
		int sum = 0;
		for(int i = x; i; i -= lowbit(i)) {
			sum += c[i];
		}
		return sum;
	}
}B;

int ans[Maxn];

void obs(int l, int r, int ql, int qr) {
	if(ql > qr) return ;
	if(l == r) {
		for(int i = ql; i <= qr; i++) {
			if(q[i].opt == 3) {
				ans[q[i].id] = l;
			}
		}
		return ;
	}
	int mid = (l + r) >> 1, p1 = 0, p2 = 0;
	for(int i = ql; i <= qr; i++) {
		if(q[i].opt == 3) {
			int x = B.query(q[i].r) - B.query(q[i].l - 1);
			if(q[i].k <= x) {
				q1[++p1] = q[i];
			}
			else {
				q[i].k -= x;
				q2[++p2] = q[i];
			}
		}
		else {
			if(q[i].k <= mid) {
				if(q[i].opt == 1) B.mdf(q[i].id, 1);
				else B.mdf(q[i].id, -1);
				q1[++p1] = q[i]; 
			}
			else {
				q2[++p2] = q[i];
			}
		}
	}
	for(int i = 1; i <= p1; i++) {
		if(q1[i].opt == 1) B.mdf(q1[i].id, -1);
		else if(q1[i].opt == 2) B.mdf(q1[i].id, 1);
	}
	for(int i = 1; i <= p1; i++) {
		q[ql + i - 1] = q1[i]; 
	}
	for(int i = 1; i <= p2; i++) {
		q[ql + p1 + i - 1] = q2[i];
	}
	obs(l, mid, ql, ql + p1 - 1);
	obs(mid + 1, r, ql + p1, qr);
}

int main() {
	ios::sync_with_stdio(0);
	cin >> n >> m;
	for(int i = 1; i <= n; i++) {
		int p;
		cin >> p;
		mn = min(mn, p), mx = max(mx, p);
		a[i] = p;
		q[++tot] = {1, -1, -1, p, i};
	}
	for(int i = 1; i <= m; i++) {
		char opt;
		int l, r, k;
		cin >> opt >> l >> r;
		if(opt == 'C') {
			mn = min(mn, r), mx = max(mx, r);
			q[++tot] = {2, -1, -1, a[l], l};
			q[++tot] = {1, -1, -1, r, l};
			a[l] = r;
		}
		else {
			cin >> k;
			q[++tot] = {3, l, r, k, ++cnt};
		} 
	}
	obs(mn, mx, 1, tot);
	for(int i = 1; i <= cnt; i++) {
		cout << ans[i] << '\n';
	}
	return 0;
}

相關文章