link

整体二分是一种东西,比如上面这道题。

先考虑一个不带修版本的,也就是经典问题区间 kth,显然我们可以主席树但是我知道你很想用主席树但是你先别用不用主席树,用一种离线的算法,叫整体二分。

首先先考虑处理单个询问,二分,简单吗?真简单,先二分一个 \(mid\),然后把区间内所有小于等于 \(mid\) 的数字丢进一个权值树状数组里,统计一下这些数字有多少个,就可以判定了。那么这个时候时间复杂度应该是 \(O((r - l + 1) \log_2 SIZE^2)\) 的(权值树状数组与二分带两只 log)。离散化就可以做到 \(O((r - l + 1)\log_2 (r - l + 1)^2)\),但一般没必要这么做(懒捏┓( ´∀` )┏)

我们学会了对一个询问这么做,那么多个询问呢?不难想到我们可以先二分一个 \(mid\),然后对于答案小于等于 \(mid\) 的询问,丢到一块,对于答案大于 \(mid\) 的询问也丢到一块。然后两块分别递归处理。

然后你就会发现你被骗了其实这样做是不行的,因为如果我们只把询问丢到一块,那么我们怎么更新元素呢?我们一个一个一个一个的扫描长度为 \(n\) 的数组 \(a\),然后将 \(a[i]\le mid\) 的元素丢到左半边,\(a[i]>mid\) 的丢到右半边?没必要啊!我们完全可以把初始序列看成 \(n\) 次对 \(i\) 位置添加一个权值为 \(a[i]\) 的操作。然后你就会发现,我们也应该一并把 \(a[i]\le mid\) 的添加操作丢到左半边,\(a[i] > mid\) 操作丢到右半边。

由于我语文太烂了,所以看不懂上面的很正常 QAQ简单来说,整体二分就是每次二分一个 \(mid\),然后将答案小于等于 \(mid\) 与会让答案小于等于 \(mid\) 的操作一块丢到左边,答案大于 \(mid\) 的查询和会让答案大于 \(mid\) 的操作丢到右边。这样就好啦(*^▽^*)。

这就是伟大的整体二分算法,是不是相当简单?总的来说,就是每次二分一个值,普通的二分是,如果只有一个询问,我们就把它划分,然后继续二分。整体二分,就是每次二分出来一个值,然后都得对一串进行划分。

这个东西出现在了 2013 年国家集训队论文里面,是许昊然这个巨佬讲的,所以建议直接看这篇文章反正他写的比我这个大蒟蒻写的好 1145141919810 倍我写这个就是给自己看的我有的时候都看不懂自己在写什么,比如语文考试的作文 QAQ。这篇文章里,谈到了整体二分的 5 个应用前提:

  1. 询问的答案具有可二分性。
  2. 修改对判定答案的贡献互相独立,修改之间互不影响效果
  3. 修改如果对判定答案有贡献,则贡献为一确定的与判定标准无关的值
  4. 贡献满足交换律、结合律,具有可加性
  5. 题目允许离线算法

以上出自许昊然巨佬论文的原文。第一点显而易见,不然你怎么整体二分。第五点也很显然,因为整体二分就是把所有操作放在一块处理,不能离线怎么放在一块。第二点为什么要“互相独立”呢?假设你的修改是“修改了 p 后才能修改 q”或者“修改了 p 就不能修改 q”这个时候就很不可做。大部分 DS 题都是修改之间互不影响效果的吧 qwq。第三点也很简单,就是说我们修改了后,判定还是照样的判定方法。拿上面这题举例子,刚开始的加数我们看成修改,显而易见,修改是修改了,但是我们判定就是小于等于 \(mid\),是不会有影响的。第四点也不难理解,拿上面那题举例子,每个点的贡献就是如果这个点小于等于 \(mid\),就会贡献一个 \(1\),满足交换律,结合律(你先更新哪个点后更新哪个点都一样),同时显而易见具有可加性。


回到动态区间 kth 来,不难发现,为了独立每个修改的贡献,我们完全可以把单点修改拆成“删除原来的值”和“加入更新的值”,如此一来这样贡献就能独立了。

删除原来的值和上面讨论过的“加入更新的值”就反着来就好了吧,还是很简单的说 awa

#include <iostream>
#include <cstring>
#define MAXN 200000 
using namespace std;
int tr[MAXN + 10], n, m, tot;
int lowbit(int x) {return (x & (-x));}
void add(int pos, int val) {
	for(; pos <= n; pos += lowbit(pos))
		tr[pos] += val;
}
int query(int pos) {
	int rest = 0;
	while(pos) {
		rest += tr[pos];
		pos -= lowbit(pos);
	}
	return rest;
} 

//type = 0 query
//type = 1 insert
//type = 2 delete 
struct node {
	int type, val, l, r, ind;
	//For type = 0
	//[l, r] val ind = i
	//For type = 1/2
	// l val ind = i
} Q[MAXN * 2 + 10], qa[MAXN * 2 + 10], qb[MAXN * 2 + 10];
int ans[MAXN + 10], a[MAXN + 10];

void twofen(int l, int r, int s, int t) {
	if(l > r) return ;
	if(s == t) {
		for(int p = l; p <= r; p++)
			if(!Q[p].type)
				ans[Q[p].ind] = s;
		return ;
	}
	int mid = (s + t) >> 1, ta = 0, tb = 0;
	for(int p = l; p <= r; p++) {
		if(Q[p].type == 0) {
			int qr = query(Q[p].r) - query(Q[p].l - 1);
			if(qr >= Q[p].val) qa[++ta] = Q[p];
			else Q[p].val -= qr, qb[++tb] = Q[p];
		}
		else if(Q[p].type == 1) {
			if(Q[p].val <= mid) add(Q[p].l, 1), qa[++ta] = Q[p];
			else qb[++tb] = Q[p];
		}
		else if(Q[p].type == 2) {
			if(Q[p].val <= mid) add(Q[p].l, -1), qa[++ta] = Q[p];
			else qb[++tb] = Q[p];
		}
	}
	
	for(int p = l; p <= r; p++)
		if(Q[p].val <= mid)
			if(Q[p].type == 1) add(Q[p].l, -1);
			else if(Q[p].type == 2) add(Q[p].l, 1); 
	
	for(int p = l, i = 1; i <= ta; p++, i++) Q[p] = qa[i];
	for(int p = l + ta, i = 1; i <= tb; p++, i++) Q[p] = qb[i];
	twofen(l, l + ta - 1, s, mid);
	twofen(l + ta, r, mid + 1, t);
}
int main() {
	freopen("read.txt", "r", stdin);
	freopen("write.txt", "w", stdout);
	cin >> n >> m;
	for(int p = 1; p <= n; p++) {
		tot++;
		cin >> a[p], Q[tot].val = a[p];
		Q[tot].type = 1, Q[tot].l = p;
	}
	char opt;
	int qt = 0;
	for(int p = 1; p <= m; p++) {
		cin >> opt;
		if(opt == 'Q') {
			tot++;
			cin >> Q[tot].l >> Q[tot].r >> Q[tot].val;
			Q[tot].ind = ++qt, Q[tot].type = 0;
		}
		else {
			int rt, rest; tot++;
			cin >> rt >> rest; 
			Q[tot].l = rt;
			Q[tot].val = a[rt];//这里一直写成 a[p] 调了好久,太菜了 QAQ
			Q[tot].type = 2;
			
			tot++;
			Q[tot].l = rt, Q[tot].val = rest;
			Q[tot].type = 1, a[rt] = rest;
		}
	}
	twofen(1, tot, 0, 1e9);
	for(int p = 1; p <= qt; p++)
		cout << ans[p] << endl;
}