CDQ分治

CDQ 分治的思想最早由 IOI2008 金牌得主陈丹琦在高中时整理并总结,它也因此得名。

CDQ分治的思想常用于离线解决点对之间的问题,最经典的如三维偏序。

解决这类问题的过程为:

  • 将序列 $ (l, r) $ 分为 $ (l, mid) $ 和 $ (mid + 1, r) $ 。

  • 递归处理序列$ (l, mid) $ 和 $ (mid + 1, r) $中的答案。

  • 设计算法处理对于点对 $ (i, j) (l \le i \le mid), (mid + 1 \le j \le r) $ 间的答案。

$ \color {#9d3dcf}P3810 \text【模板】三维偏序(陌上花开)$

题意

有 $ n $ 个元素,第 $ i $ 个元素有 $ a_i,b_i,c_i $ 三个属性,设 $ f(i) $ 表示满足 $ a_j \leq a_i $ 且 $ b_j \leq b_i $ 且 $ c_j \leq c_i $ 且 $ j \ne i $ 的 \(j\) 的数量。

对于 $ d \in [0, n) $,求 $ f(i) = d $ 的数量。

思路

考虑先将数组按 $ a $ 排序,然后开始分治。

假设当前在处理区间 \(l, r\) ,并且已经统计完$ (l, mid) $ 和 $ (mid + 1, r) $中的答案,那么就要思考如何处理点对 $ (i, j) (l \le i \le mid), (mid + 1 \le j \le r) $ 间的答案。

由于开始时已将数组按 $ a $ 排序,所以左区间内的所有点的 $ a $ 都小于右区间。

那么对于 $ j \in [mid + 1, r] $ , $ f(j) $ 应加上 $ b_i \le b_j $ && $ c_i \le c_j $ $ (i \in [l, mid]) $ 的 $ i $ 的数量。

为了处理第二维 $ b $ ,我们可以将区间都按 $ b $ 排序,然后使用双指针维护 $ b_i \le b_j $。

维护第三维 $ c $ , 我们可以使用树状数组。

在 $ i $ 指针向右移的过程中,我们把树状数组中 $ c_i $ 位上 $ +1 $,这样更新 $ f(j) $ 时只需在树状数组中查询 $ \le c_j $ 的个数即可。

注意:进入递归前需要对原数组进行去重,否则会出错。

代码

#include<bits/stdc++.h>
#define int long long
#define Debug puts("Oops!")
using namespace std;

const int N = 2e5 + 5, M = 5e5 + 5;

int n, m, k;
int res[N];

struct sb {
	int x, y, z;
	int cnt, f;
}p[N], a[N]; 

struct BIT {
    int t[N];
    int lowbit(int x) {return x & -x;}
    void add(int x, int c) {for(; x <= k; x += lowbit(x)) t[x] += c;}
    int ask(int x) {int res = 0; for(; x; x -= lowbit(x)) res += t[x]; return res;}
}T;

bool cmp1(sb x, sb y) {
	if(x.x != y.x) return x.x < y.x;
	if(x.y != y.y) return x.y < y.y;
	return x.z < y.z;
}

bool cmp2(sb x, sb y) {
	if(x.y != y.y) return x.y < y.y;
	return x.z < y.z;
}

void cdq(int l, int r) {
    if(l == r) return ;
    int mid = l + r >> 1;
    cdq(l, mid), cdq(mid + 1, r);
	sort(a + l, a + mid + 1, cmp2), sort(a + mid + 1, a + r + 1, cmp2);
	int i = l, j = mid + 1;
	while(j <= r) {
		while(i <= mid && a[i].y <= a[j].y) T.add(a[i].z, a[i].cnt), i++;
		a[j].f += T.ask(a[j].z);
		j++;
	}
	for(int k = l; k < i; k++) T.add(a[k].z, -a[k].cnt);
}

inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
	while(isdigit(c)) x = x * 10 + c - '0', c = getchar();
	return x * f;
}

signed main() {
//	freopen(".in", "r", stdin);
//	freopen(".out", "w", stdout);
	n = read(), k = read();
	for(int i = 1; i <= n; i++)	p[i].x = read(), p[i].y = read(), p[i].z = read();
	sort(p + 1, p + 1 + n, cmp1);
	int cnt = 0;
	for(int i = 1; i <= n; i++) {
		cnt++;
		if(p[i].x != p[i + 1].x || p[i].y != p[i + 1].y || p[i].z != p[i + 1].z)
			a[++m].x = p[i].x, a[m].y = p[i].y, a[m].z = p[i].z, a[m].cnt = cnt, cnt = 0;
	}
    cdq(1, m);
	for(int i = 1; i <= m; i++) res[a[i].f + a[i].cnt] += a[i].cnt;
	for(int i = 1; i <= n; i++) cout << res[i] << endl;
	return 0;
}

$ \color{#9d3dcf} P4390 \text[BalkanOI2007] Mokia 摩基亚 $

题意

有四种命令,意义如下:

命令 参数 意义
\(0\) \(w\) 初始化一个全零矩阵。本命令仅开始时出现一次。
\(1\) \(x\ y\ a\) 向方格 \((x, y)\) 中添加 \(a\) 个用户。\(a\) 是正整数。
\(2\) \(x_1\ y_1\ x_2\ y_2\) 查询 \(x_1\leq x\leq x_2\)\(y_1\leq y\leq y_2\) 所规定的矩形中的用户数量。
\(3\) 无参数 结束程序。本命令仅结束时出现一次。

对于 \(100\%\) 的数据,保证:

  • \(1\leq w\leq 2\times 10 ^ 6\)
  • \(1\leq x_1\leq x_2\leq w\)\(1\leq y_1\leq y_2\leq w\)\(1\leq x,y\leq w\)\(0<a\leq 10000\)
  • 命令 \(1\) 不超过 \(160000\) 个。
  • 命令 \(2\) 不超过 \(10000\) 个。

思路

首先想到的是二维树状数组,可惜矩阵大小高达 $ 2 \times 10 ^ 6 $ ,直接放弃。

设 $ S $ 为前缀和数组

显然,$ 2 $ 命令可以转化为四个操作:

求 $ S_{x_2, y_2} $, $ S_{x_2, y_1 - 1} $, $ S_{x_1 - 1, y_2} $, $ S_{x_1 - 1, y_1 - 1} $。

下文将求 $ S_{x, y} $ 称为查询操作。

可以发现如果某次 $ 1 $ 命令要对查询操作造成贡献,需要满足三个条件:

  • $ 1 $ 命令出现时间比查询操作小
  • $ 1 $ 命令中的 $ x $ 比查询操作中的 $ x $ 小
  • $ 1 $ 命令中的 $ y $ 比查询操作中的 $ y $ 小

哦——,这不就是三维偏序板子嘛!

注意:求前缀和时可能出现 $ x = 0 $ 或 $ y = 0 $ 的情况,最方便的处理方法是将命令中的 $ x, y $ 均加一

矩阵边长也要加!!!

代码

#include<bits/stdc++.h>
// #define int long long
#define Debug puts("Oops!")
using namespace std;

const int N = 2e6 + 5, M = 5e5 + 5;

int n, m;
int res[N];

struct sb {
	int x, y, z, f;
	int cnt, res;
}a[N]; 

void add(int x, int y) {
	a[++m].x = m, a[m].y = x, a[m].z = y, a[m].cnt = 0, a[m].f = 1;
}

struct BIT {
    int t[N];
    int lowbit(int x) {return x & -x;}
    void add(int x, int c) {for(; x <= n; x += lowbit(x)) t[x] += c;}
    int ask(int x) {int res = 0; for(; x; x -= lowbit(x)) res += t[x]; return res;}
}T;

bool cmp1(sb x, sb y) {
	if(x.x != y.x) return x.x < y.x;
	if(x.y != y.y) return x.y < y.y;
	return x.z < y.z;
}

bool cmp2(sb x, sb y) {
	if(x.y != y.y) return x.y < y.y;
	return x.z < y.z;
}

void cdq(int l, int r) {
    if(l == r) return ;
    int mid = l + r >> 1;
    cdq(l, mid), cdq(mid + 1, r);
	sort(a + l, a + mid + 1, cmp2), sort(a + mid + 1, a + r + 1, cmp2);
	int i = l, j = mid + 1;
	while(j <= r) {
		while(i <= mid && a[i].y <= a[j].y) T.add(a[i].z, a[i].cnt), i++;
		if(a[j].f) a[j].res += T.ask(a[j].z);
		j++;
	}
	for(int k = l; k < i; k++) T.add(a[k].z, -a[k].cnt);
}

inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
	while(isdigit(c)) x = x * 10 + c - '0', c = getchar();
	return x * f;
}

signed main() {
//	freopen(".in", "r", stdin);
//	freopen(".out", "w", stdout);
	vector<int> q;
	int op = read();
	while(op != 3) {
		if(op == 0) n = read() + 1;
		else if(op == 1) a[++m].x = m, a[m].y = read() + 1, a[m].z = read() + 1, a[m].cnt = read();
		else if(op == 2) {
			int ax = read() + 1, ay = read() + 1, bx = read() + 1, by = read() + 1;
			q.emplace_back(m + 1);
			add(bx, by), add(bx, ay - 1), add(ax - 1, by), add(ax - 1, ay - 1);
		}
		op = read();
	}
	cdq(1, m);
	sort(a + 1, a + 1 + m, cmp1);
	for(int u : q) cout << a[u].res - a[u + 1].res - a[u + 2].res + a[u + 3].res << endl;
	return 0;
}


posted @ 2024-08-10 14:30  zeta炀  阅读(34)  评论(0)    收藏  举报