离线分治算法:cdq 分治

\(\texttt{0x00}\):前置芝士

  1. 归并排序;
  2. 树状数组;
  3. 重载运算符(这个大概都会吧)。

\(\texttt{0x01}\):介绍

cdq 分治是一种离线分治算法,可用来处理以下几种问题:

  1. 解决和点对有关的问题。
  2. 1D 动态规划的优化与转移。
  3. 通过 CDQ 分治,将一些动态问题转化为静态问题。

它们的本质都是通过一系列转化将原问题转化为三维偏序问题。

\(\texttt{0x02}\):梦开始的地方

P3810 【模板】三维偏序(陌上花开)

我们都知道,求二维偏序(比如逆序对)可以用归并排序或树状数组,他们的本质都是先确定一维的顺序,再计算另一维贡献。

那拓展到三维偏序也是如此,既然归并和树状数组都能确定一维的顺序,那把他们两个合起来不就能做三维偏序了吗?

确实是这样的。

先回顾一下归并排序求逆序对。

void merge_sort(int l, int r) {
	if(l == r) return ;
	int mid = l + r >> 1;
	merge_sort(l, mid);
	merge_sort(mid + 1, r);
	int i = l, j = mid + 1;
	for(int k = l; k <= r; k++)
		if(j > r || i <= mid && a[i] <= a[j]) tmp[k] = a[i++];
		else tmp[k] = a[j++], res += mid - i + 1;
	for(int k = l; k <= r; k++) a[k] = tmp[k];
}

流程是这样的:将当前处理的序列一分为二,对于 \(i,j\) 在同一边的情况,递归求解即可,我们只需要考虑在两边的情况。

不妨设 \(i\) 在左,\(j\) 在右,由于左右区间已经递归按数值排好序了,下标一开始就是有序的,所以 \(i\) 的下标一定小于 \(j\) 的下标,又因为左右两边的数值是单调递增的,所以可以用双指针扫描一遍求出答案,具体的,当 \(a[i] > a[j]\) 时,说明 \(a[i]\) 是第一个大于 \(a[j]\) 的数且后面的数都大于 \(a[i]\),所以 \(a[j]\)\(a[i\sim mid]\) 都构成逆序对,可以直接统计答案。

时间复杂度 \(O(n\log n)\)

注意两个容易理解错的地方:

  1. 虽然说按照数值排序会打乱下标的顺序,但是排序是在区间内部进行的,也就是说,左右两部分是完全封闭地完成了排序,所以左边区间的下标一定小于右边区间内的下标;

  2. 由于分治是从下往上递归的,所以同区间内的两个数的贡献已经被算过,只用考虑跨区间的贡献。

现在在这个基础上加上一维,就再套个树状数组控制第三维就行了,具体说来,排序保证了第一维,归并保证了第二维,树状数组保证了第三维。

这道题还有一个魔鬼细节,就是这三维都可以取等号,也就是说假如 \(a,b\) 完全相同,\(a\)\(b\) 左边,那么 \(f(a) = f(b) = 1\) 才对,但是 cdq 分治只会算左边给右边的贡献,所以实际算出来为 \(f(a) = 0,f(b) = 1\)

先去重,思考一下不难发现假如有 \(k\) 个数都相同,那么树状数组中的所有修改都要从 \(1\) 变成 \(k\),同时在最后还要加上内部的贡献 \(k - 1\)

时间复杂度 \(O(n\log ^2n)\)

\(\texttt{Code}\)

#include <iostream>
#include <algorithm>
#define lowbit(x) x & -x
using namespace std;

const int N = 100010, M = 200010;

int n, vmax;
struct node{
	int a, b, c, s, res; //s是数量,res 是去重后每个元素满足条件的个数
	bool operator< (const node &o) const { //三关键字排序
		if(a != o.a) return a < o.a;
		if(b != o.b) return b < o.b;
		return c < o.c;
	}
	bool operator== (const node &o) const {
		return a == o.a && b == o.b && c == o.c;
	}
}e[N], tmp[N];
int ans[N];
int tr[M];

void add(int x, int y) {
	for(; x < M; x += lowbit(x)) tr[x] += y;
}

int ask(int x) {
	int res = 0;
	for(; x; x -= lowbit(x)) res += tr[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;
	for(int k = l; k <= r; k++) {
		if(j > r || i <= mid && e[i].b <= e[j].b) add(e[i].c, e[i].s), tmp[k] = e[i++]; //套一个树状数组
		else e[j].res += ask(e[j].c), tmp[k] = e[j++];
	}
	for(int k = l; k <= mid; k++) add(e[k].c, -e[k].s); //做完一次后必须要清空树状数组
	for(int k = l; k <= r; k++) e[k] = tmp[k]; //归并排序
}

int main() {
	scanf("%d%d", &n, &vmax);
	int a, b, c;
	for(int i = 0; i < n; i++) {
		scanf("%d%d%d", &a, &b, &c);
		e[i] = {a, b, c, 1};
	}
	sort(e, e + n);
	int tt = 1;
	for(int i = 1; i < n; i++) { //去重
		if(e[i] == e[tt - 1]) e[tt - 1].s++;
		else e[tt++] = e[i];
	}
	cdq(0, tt - 1);
	for(int i = 0; i < tt; i++) 
		ans[e[i].res + e[i].s - 1] += e[i].s; //注意加上内部贡献
	for(int i = 0; i < n; i++) printf("%d\n", ans[i]);
	return 0;
}
posted @ 2024-07-23 15:09  Brilliant11001  阅读(2)  评论(0编辑  收藏  举报