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;
}