cdq分治 & 整体二分 小记(咕咕咕)

只保证自己看懂。
cdq分治和整体二分是两个比较重要的离线算法。


cdq分治

1.三维偏序

(f(i)) 表示满足 (a_j < a_i,b_j<b_i,c_j<c_i)(j) 的数量。
考虑只有一维的时候,就是排序。
考虑只有二维的时候,就是按 (a_j < a_i) 排序之后,从 (1)(n) 枚举 (i) ,把 (b_i) 加入树状数组,则 (f(i)) 为满足树状数组中满足 (b_j < b_i) 的数目。
考虑三维的时候,先按 (a_j < a_i) 排序,然后按 (b_j < b_i) 做归并排序(要保证 (a_j < a_i) 不变,归并排序是稳定的,所以可以保证)。
对于区间 ([l,r]) ,此时 ([l,mid])((mid,r]) 已经排好序并贡献答案了,对于 (j in [l,mid],i in (mid,r]) 总有 (b_j < b_i) ,所以我们在 merge 的时候套上一个树状数组,算出 (c_j < c_i) 的数目。
总复杂度为 (O(n log n log ext{值域大小})) ,实际上离散化之后就是 (O(n log^2 n))
例题:Luogu P3810
注意这题为“ (leq) ” ,所以我们要先去重再做,具体细节看下面代码。

#include <cstdio>
#include <cctype>
#include <algorithm>
#define MAX_N (100000 + 5)
#define MAX_K (200000 + 5)
using std::sort;

int n, K;
struct Node {
	int a, b, c;
	int cnt;
	int sum;
	bool operator < (const Node &x) const {
		if (a != x.a) return a < x.a;
		if (b != x.b) return b < x.b;
		return c < x.c;
	}
	bool operator != (const Node &x) const {
		return a != x.a || b != x.b || c != x.c;
	}
} a[MAX_N], b[MAX_N];
int s[MAX_K];
int ans[MAX_N];

int Read() {
	int res = 0;
	char ch = getchar();
	while (!isdigit(ch)) ch = getchar();
	while (isdigit(ch)) res = res * 10 + ch - '0', ch = getchar();
	return res;
}

int lowbit(int x) {
	return x & -x;
}

void Add(int x, int val) {
	for (; x <= K; x += lowbit(x))
		s[x] += val;
}

int Sum(int x) {
	int res = 0;
	for (; x; x -= lowbit(x))
		res += s[x];
	return res;
}

void cdq(int l, int r) {
	if (l == r) return;
	int mid = l + r >> 1;
	cdq(l, mid);
	cdq(mid + 1, r);
	int i = l, j = mid + 1, k = 0;
	while (i <= mid && j <= r) {
		if (a[i].b <= a[j].b) {
			Add(a[i].c, a[i].cnt);
			b[++k] = a[i++];
		} else {
			a[j].sum += Sum(a[j].c);
			b[++k] = a[j++];
		}
	}
	while (i <= mid) {
		Add(a[i].c, a[i].cnt);
		b[++k] = a[i++];
	}
	while (j <= r) {
		a[j].sum += Sum(a[j].c);
		b[++k] = a[j++];
	}
	for (int i = l; i <= mid; ++i)
		Add(a[i].c, -a[i].cnt);
	for (int i = 1; i <= k; ++i)
		a[l + i - 1] = b[i];
}

int main() {
	n = Read(), K = Read();
	for (int i = 1; i <= n; ++i) {
		a[i].a = Read(), a[i].b = Read(), a[i].c = Read();
		a[i].cnt = 1;
	}
	sort(a + 1, a + n + 1);
	int m = 0;
	for (int i = 1; i <= n; ++i) {
		if (a[i] != a[i + 1]) a[++m] = a[i];
		else a[i + 1].cnt += a[i].cnt;
	}
	cdq(1, m);
	for (int i = 1; i <= m; ++i)
		ans[a[i].sum + a[i].cnt - 1] += a[i].cnt;
	for (int i = 0; i < n; ++i)
		printf("%d
", ans[i]);
	return 0;
}

2.应用

例题:Luogu P3157
(j < i,a_j > a_i) 再加上时间,那就是三维偏序,直接套cdq分治就好了。


整体二分

1.静态区间第 (k)

整体二分是什么?
显然是二分啊。
我们先把输入 (a_i) 和询问看作两种操作,一起放入 (q_1,q_2,q_3,cdots,q_{n+m})
于是我们设当前二分的答案区间为 ([l,r]) ,令 (mid) 为其中值。
又设二分的操作序号区间为 ([L,R])
我们先将 ([L,R]) 中的满足 (a_i in [l,mid])(i) 加入一个数组 (t_i)中,若 (a_i) 满足条件则第 (i)(+1) ,即 (t_i gets t_i+1)
然后对于 ([L,R]) 中的每一个询问操作,设询问区间为 ([ql,qr]) ,则设 (res = sumlimits_{i=ql}^{qr}t_i) (即满足 (a_i in [ql,qr]) 的数量)。
(k leq res) ,则答案一定在 ([l,mid]) 中;若 (k > res) ,则答案一定在 ((mid,r]) 中。
我们在分别处理 ([l,mid])((mid,r]) 即可,注意要同时更新 ([L,R])
至于 (t_i) 其实可以用树状数组维护。
则二分的复杂度为 (O(nlog ext{值域大小})) ,树状数组修改和查询的复杂度为 (O(log n)) ,总复杂度为 (O(n log n log ext{值域大小}))
当然,离散化之后可以变成 (O(n log^2 n)) ,这里就不写了。
例题:Luogu P3834
具体细节看代码。
注意 mid = l + r >> 1 是向下取整, mid = (l + r) / 2 是向 (0) 取整。

#include <cstdio>
#include <cctype>
#define MAX_N (200000 + 5)
#define MAX_M (200000 + 5)

int n, m;
struct Opt {
	int l, r, k, idx, opt;
} q[MAX_N + MAX_M], q1[MAX_N + MAX_M], q2[MAX_N + MAX_M];
int s[MAX_N];
int ans[MAX_M];

int Read() {
	int res = 0, sign = 0;
	char ch = getchar();
	while (!isdigit(ch)) sign |= ch == '-', ch = getchar();
	while (isdigit(ch)) res = res * 10 + ch - '0', ch = getchar();
	return sign ? -res : res;
}

int lowbit(int x) {
	return x & -x;
}

void Add(int pos, int val) {
	for (; pos <= n; pos += lowbit(pos))
		s[pos] += val;
}

int Sum(int pos) {
	int res = 0;
	for (; pos; pos -= lowbit(pos))
		res += s[pos];
	return res;
}

void Solve(int l, int r, int L, int R) {
	if (L > R) return;
	if (l == r) {
		for (int i = L; i <= R; ++i)
			if (q[i].opt) ans[q[i].idx] = l;
		return;
	}
	int mid = l + r >> 1, cnt1 = 0, cnt2 = 0, res;
	for (int i = L; i <= R; ++i) {
		if (q[i].opt) {
			res = Sum(q[i].r) - Sum(q[i].l - 1);
			if (res >= q[i].k) q1[++cnt1] = q[i];
			else q[i].k -= res, q2[++cnt2] = q[i];
		} else {
			if (q[i].k <= mid) q1[++cnt1] = q[i], Add(q[i].idx, 1);
			else q2[++cnt2] = q[i];
		}
	}
	for (int i = 1; i <= cnt1; ++i) 
		q[L + i - 1] = q1[i];
	for (int i = 1; i <= cnt2; ++i)
		q[L + cnt1 + i - 1] = q2[i];
	for (int i = 1; i <= cnt1; ++i)
		if (!q1[i].opt) Add(q1[i].idx, -1);
	Solve(l, mid, L, L + cnt1 - 1);
	Solve(mid + 1, r, L + cnt1, R);
}

int main() {
	n = Read(), m = Read();
	for (int i = 1; i <= n; ++i) 
		q[i] = (Opt){0, 0, Read(), i, 0};
	for (int i = 1; i <= m; ++i)
		q[i + n] = (Opt){Read(), Read(), Read(), i, 1};
	Solve(-1e9, 1e9, 1, n + m);
	for (int i = 1; i <= m; ++i)
		printf("%d
", ans[i]);
	return 0;
}

2.动态区间第 (k)

其实把输入 (a_i) 也看作是操作 ( exttt{C} i a_i)
于是可以把输入操作看作 (operatorname{add}(i,1)) ,把修改操作拆成 (operatorname{add}(x,-1))(operatorname{add}(x,1))
记录每一次 (operatorname{add}) 操作时(a_x) 即可。
例题:Luogu P2617

#include <cstdio>
#include <cctype>
#include <cstring>
#define MAX_N (100000 + 5)
#define MAX_M (100000 + 5)

int n, m;
int a[MAX_N];
struct Opt {
	char opt;
	int l, r, k, idx;
} q[MAX_N + MAX_M * 2], q1[MAX_N + MAX_M * 2], q2[MAX_N + MAX_M * 2];
int s[MAX_N];
int ans[MAX_N];

int Read() {
	int res = 0;
	char ch = getchar();
	while (!isdigit(ch)) ch = getchar();
	while (isdigit(ch)) res = res * 10 + ch - '0', ch = getchar();
	return res;
}

int lowbit(int x) {
	return x & -x;
}

void Add(int pos, int val) {
	for (; pos <= n; pos += lowbit(pos))
		s[pos] += val;
}

int Sum(int pos) {
	int res = 0;
	for (; pos; pos -= lowbit(pos))
		res += s[pos];
	return res;
}

void Solve(int l, int r, int L, int R) {
	if (L > R) return;
	if (l == r) {
		for (int i = L; i <= R; ++i)
			if (q[i].opt == 'Q') ans[q[i].idx] = l;
		return;
	}
	int mid = l + r >> 1, cnt1 = 0, cnt2 = 0, res;
	for (int i = L; i <= R; ++i) {
		if (q[i].opt == 'C') {
			if (q[i].k <= mid) q1[++cnt1] = q[i], Add(q[i].l, q[i].r);
			else q2[++cnt2] = q[i];
		} else {
			res = Sum(q[i].r) - Sum(q[i].l - 1);
			if (q[i].k <= res) q1[++cnt1] = q[i];
			else q[i].k -= res, q2[++cnt2] = q[i];
		}
	}
	for (int i = 1; i <= cnt1; ++i)
		q[L + i - 1] = q1[i];
	for (int i = 1; i <= cnt2; ++i)
		q[L + cnt1 + i - 1] = q2[i];
	for (int i = 1; i <= cnt1; ++i)
		if (q1[i].opt == 'C') Add(q1[i].l, -q1[i].r);
	Solve(l, mid, L, L + cnt1 - 1);
	Solve(mid + 1, r, L + cnt1, R);
}

int main() {
	n = Read(), m = Read();
	int cnt = 0;
	for (int i = 1; i <= n; ++i){
		a[i] = Read();
		q[++cnt] = (Opt){'C', i, 1, a[i], 0};
	}
	char ch;
	int x, y;
	for (int i = 1; i <= m; ++i) {
		ch = getchar();
		while (!isalpha(ch)) ch = getchar(); 
		x = Read(), y = Read();
		if (ch == 'C') {
			q[++cnt] = (Opt){'C', x, -1, a[x], 0};
			a[x] = y;
			q[++cnt] = (Opt){'C', x, 1, y, 0};
		} else 
			q[++cnt] = (Opt){'Q', x, y, Read(), i};
	}
	memset(ans, -1, sizeof ans);
	Solve(0, 1e9, 1, cnt);
	for (int i = 1; i <= m; ++i)
		if (~ans[i]) printf("%d
", ans[i]);
	return 0;
}
原文地址:https://www.cnblogs.com/kcn999/p/13770946.html