[学习笔记] CDQ分治

发布时间 2023-03-22 21:11:30作者: shiranui

引入 - 分治

分治,就是将讲原问题不断细分直到规模小到能够解决,然后一层层向上合并得到答案的过程。

归并排序

大致思想:把序列拆成左右两部分,分别归并排序,然后使用两个指针按序合并左右部分。

归并求逆序对

归并求逆序对是分治的一个经典例子。

要做的就是在合并过程中计算逆序对对数。

由于合并的是两个有序数组,那么当 \(num_{left}>num_{right}\) 时,\(num_{left},num_{left+1},...,num_{mid}\) 显然都是大于 \(num_{right}\) 的,那么就可以直接统计对数。

代码实现

const int N = 500010;
int n, a[N], tmp[N];
ll ans;
void merge(int l, int r) {
	if (l == r) return ;
	int mid = l + r >> 1;
	merge(l, mid), merge(mid + 1, r);
	int lf = l, rt = mid + 1, now = l;
	while (lf <= mid && rt <= r) {
		if (a[lf] <= a[rt]) tmp[now++] = a[lf++];
		else tmp[now++] = a[rt++], ans += (mid - lf + 1);
	}
	while (lf <= mid) tmp[now++] = a[lf++];
	while (rt <= r) tmp[now++] = a[rt++];
	for (int i = l; i <= r; i++) a[i] = tmp[i];
}
int main() {
	n = read();
	for (int i = 1; i <= n; i++) a[i] = read();
	merge(1, n);
	printf("%lld\n", ans);
	return 0;
}

可以发现在合并过程中,加上了左段对右段的贡献。这是CDQ分治的一个典型过程。

CDQ分治

优点:常数较小,可代替高级数据结构,易实现。

缺点:必须离线。

二维偏序

\(n\) 个元素,第 \(i\) 个元素有 \(a_i,b_i\) 两个属性,设 \(f(i)\) 表示满足 \(a_j<a_i\)\(b_j<b_i\)\(j\) 的数量。

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

思路

把第 \(i\) 个元素看成 \((a_i,b_i)\) 的一个点,那么求的就是以该点为左上角的矩形包含多少点。

可以先按 \(x(a_i)\) 排个序,用树状数组维护,\(sum_i\) 表示已经考虑过的点中 \(y\) 小于等于 \(y(b_i)\) 的点的个数。

代码

HDU1541 Stars

这题题面没说有多组数据,但是会WA。

const int N = 15010, M = 32010;
int n, mx;
struct point {
	int x, y;
	bool operator <(const point &b) {return x == b.x ? y < b.y : x < b.x;}
} a[N];
int sum[M];
void add(int x, int p) {
	while (x <= mx) {
		sum[x] += p;
		x += lowbit(x);
	}
}
int query(int x) {
	int res = 0;
	while (x) {
		res += sum[x];
		x -= lowbit(x);
	}
	return res;
}
int ans[N];
int main() {
	while (cin >> n) {
		memset(sum, 0, sizeof(sum)), memset(ans, 0, sizeof(ans));
		mx = 0;
		for (int i = 1; i <= n; i++) a[i].x = read() + 1, a[i].y = read() + 1, mx = max(mx, a[i].y);
		sort(a + 1, a + 1 + n);
		for (int i = 1; i <= n; i++) {
			ans[query(a[i].y)]++;
			add(a[i].y, 1);
		}
		for (int i = 0; i < n; i++) printf("%d\n", ans[i]);
	}
	return 0;
}

三维偏序

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

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

思路

\(a\) 排序,归并 \(b\),树状数组维护 \(c\)

代码实现

const int N = 100010;
int n, k;
struct node {
	int a, b, c, cnt, ans;
} a[N], tmp[N];
bool cmp(const node &a, const node &b) {
	if (a.a != b.a) return a.a < b.a;
	if (a.b != b.b) return a.b < b.b;
	return a.c < b.c;
}
int sum[N << 1 | 1];
void add(int x, int p) {
	while (x <= N << 1) {
		sum[x] += p;
		x += lowbit(x);
	}
}
int query(int x) {
	int res = 0;
	while (x) {
		res += sum[x];
		x -= lowbit(x);
	}
	return res;
}
ll ans[N];
queue <int> q;
void work(int l, int r) {
	if (l == r) return ;
	int mid = l + r >> 1;
	work(l, mid), work(mid + 1, r);
	int lf = l, rt = mid + 1, now = l;
	while (lf <= mid && rt <= r) {
		if (a[lf].b <= a[rt].b) add(a[lf].c, a[lf].cnt), q.push(lf), tmp[now++] = a[lf++];
		else a[rt].ans += query(a[rt].c), tmp[now++] = a[rt++];
	}
	while (lf <= mid) tmp[now++] = a[lf++];
	while (rt <= r) a[rt].ans += query(a[rt].c), tmp[now++] = a[rt++];
	while (q.size()) {
		int x = q.front(); q.pop();
		add(a[x].c, -a[x].cnt);
	}
	for (int i = l; i <= r; i++) a[i] = tmp[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 + 1 + n, cmp);
	int m = n; n = 0;
	for (int i = 1; i <= m; i++) {
		if (a[i].a == a[n].a && a[i].b == a[n].b && a[i].c == a[n].c) a[n].cnt++;
		else a[++n] = a[i];
	}
	work(1, n);
	for (int i = 1; i <= n; i++) ans[a[i].ans + a[i].cnt - 1] += a[i].cnt;
	for (int i = 0; i < m; i++) printf("%d\n", ans[i]);
	return 0;
}

参考资料

CDQ分治【分治(真得头疼)

[学习笔记]CDQ分治和整体二分