离线分治算法:cdq 分治

0x00:前置芝士

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

0x01:介绍

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

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

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

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[imid] 都构成逆序对,可以直接统计答案。

时间复杂度 O(nlogn)

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

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

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

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

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

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

时间复杂度 O(nlog2n)

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 @   Brilliant11001  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示