CDQ分治学习 + 例题(洛谷P3810 && 洛谷P4169)

一般分治是先解决左右小区间然后区间合并为大区间

CDQ分治则是解决左右小区间,然后合并成大区间的时候只有左边小区间会给右边小区间带来影响。

一个最简单的雏形则是二维偏序问题 (首先默认你会归并求二维偏序)

首先先对第一个坐标排序,然后归并合并第二个坐标(其实也是CDQ),计算逆序数

在求偏序对合并的时候,也就是merge左右小区间之后。虽然说l~r区间整体是无序的,但是l~mid  , mid+1~r 这两个区间却是有序的,因此计算偏序的时候存在于l~mid区间内的数两两之间不再会产生偏序的贡献,只有l~mid 和 mid+1~r之间才会产生贡献,我们就只需要考虑l~mid 区间对 mid+1~r区间的影响即可。

那么CDQ如何解决三维偏序问题的呢?

一般来说就是这样:

第一纬使用排序,第二纬使用分治(CDQ),第三纬使用数据结构(树状数组/线段树等)

因为二维偏序问题相比于三维偏序问题,其限制条件少了一个,使用分治的时候只要看到左边区间的y坐标小于右边区间的y坐标即可累加答案,但是三维多了一个z的限制,所以此时需要查询所有符合条件(y1<y2&&z1<z2)

首先先按x轴排序,然后对y轴使用CDQ分治,分治时候的合并操作时,当遇到y1<=y2的时候,我们就需要找出此时所有既符合y1<=y2,又符合z1<=z2的,可以使用树状数组维护这一点,详细可以看下面的例题+代码。

三维逆序数模板题: 洛谷P3810     https://www.luogu.org/problem/P3810

值得注意的是

在一个小区间归并排序完之后,表示该区间内对答案的贡献已经计算完毕,这个时候就需要将树状数组清0以免重复计算答案。

还有一个就是去重操作,为什么要去重呢,因为cdq分治中只有左边对右边才有贡献,比如有两个元素ab,它们相等,但是此时只是a对b有贡献,b对a就没贡献了,因此需要去重,将它们两个合并

代码:

#include<bits/stdc++.h>
#define lowbit(i) i&(-i)
using namespace std;
const int maxn1 = 2e5 + 7;
const int maxn2 = 3e5 + 7;
struct node {
	int x, same, id, y, z; //x ,y,z为坐标  same为相同的点有多少个  id为输入顺序
}s[maxn1], b[maxn1];
int c[maxn2], ans[maxn1], out[maxn2]; //c是树状数组使用的 , ans为记录不同点对的答案, out为题目输出条件用的
int tim[maxn1], t; //tim:time ,记录该树状数组被哪一个递归函数使用过 , t:记录当前递归函数的id
int n, k, len;
bool cmp(node a, node b) {
	if (a.x != b.x) return a.x < b.x;
	else if (a.y != b.y) return a.y < b.y;
	else return a.z < b.z;
}
bool equal(const node &a, const node &b) {
	if (a.x == b.x&&a.y == b.y&&a.z == b.z) return true;
	else return false;
}
void add(int x, int y) {
	for (int i = x; i <= k; i += lowbit(i)) {
		if (t != tim[i]) tim[i] = t, c[i] = 0; //不同递归函数的话就需要先清零
		c[i] += y;
	}
}
int getsum(int x) {
	int ans = 0;
	for (int i = x; i; i -= lowbit(i)) {
		if (t == tim[i]) ans += c[i]; //仅仅在该该层递归函数里加上答案
	}
	return ans;
}
void cdq(int l, int r) {
	if (l >= r) return;
	int mid = (l + r) >> 1;
	cdq(l, mid);
	cdq(mid + 1, r);
	t++; //t不能放在上面orz。。。
	int ll = l, rr = mid + 1;
	for (int i = l; i <= r; i++) {
		if ((ll <= mid && s[ll].y <= s[rr].y) || rr > r) {
			add(s[ll].z, s[ll].same); //假如这个点对后面的有影响,那就塞进去树状数组
			b[i] = s[ll++];
		}
		else {
			ans[s[rr].id] += getsum(s[rr].z); //查询小于该点z坐标的所有点的数量
			b[i] = s[rr++];
		}
	}
	for (int i = l; i <= r; i++) s[i] = b[i];
}
int main() {
	cin >> n >> k;
	for (int i = 1; i <= n; i++) scanf("%d %d %d", &s[i].x, &s[i].y, &s[i].z);
	sort(s + 1, s + 1 + n, cmp);
	int l, r;
	l = 1, len = 0;
	while (l <= n) { //这一步是去重+统计相同的个数
		r = l + 1;
		while (r <= n && equal(s[l], s[r])) r++;
		r--;
		//s[++len].same = r - l + 1;
		//s[len].id = len;
		//s[len] = s[l];  啊这个要写在前面
		s[++len] = s[l];
		s[len].same = r - l + 1;
		s[len].id = len;
		l = r + 1;
	}
	t = 0;
	cdq(1, len);
	for (int i = 1; i <= len; i++) out[ans[s[i].id] + s[i].same - 1] += s[i].same; 
	for (int i = 0; i < n; i++) printf("%d\n", out[i]);
	return 0;
}

 

例题2:洛谷p4169  https://www.luogu.org/problem/P4169

这道题的话可以看成三维偏序问题,时间维+2个坐标纬

因为后面插入的坐标对前面的查询没有影响,只对更后面的查询有影响

可以以时间为第一纬,也可以以坐标轴为第一纬,我选择以时间为第一纬,因为已经排好序了(读入顺序)

注意这道题查询的是左上,右上,左下,右下四个方向,因此我们可以使用4次cdq,每次只查询该点的左下角,每次将坐标x轴翻转,y轴翻转以及x,y轴都翻转即可

树状数组维护的则是x+y的最大值,而不是单纯一个坐标的值,x+y最大则减出来的距离最小嘛

详情可以看代码:(注意因为我写的代码跑起来太慢了所以要勾上O2优化才能过orz.....)

#include<bits/stdc++.h>
#define lowbit(i) i&-i
using namespace std;
const int maxn1 = 3e5 + 7, maxn2 = 1e6 + 7;
const int INF = 0x3f3f3f3f;
struct node {
	int x, y, opt, ind; //opt :option 表示该点是查询还是插入点   ind:index 表示第几个询问
}s[maxn2], b[maxn2], temp[maxn2];
int ans[maxn2];
int n, m, tot, mx; // mx记录x,y轴的最大值以便我翻转
struct TreeArray { //树状数组维护前缀最大值
	int c[maxn2+maxn1];
	inline void add(int x, int y) { 
		for (int i = x; i <= mx; i += lowbit(i)) c[i] = max(c[i], y);
	}
	inline int getmax(int x) {
		int ans = 0;
		for (int i = x; i; i -= lowbit(i)) ans = max(ans, c[i]);
		//return ans;
		return ans ? ans : -INF;
	}
	inline void clear(int x) { 
		while (c[x]) {
			c[x] = 0;
			x += lowbit(x);
		}
	}
}tre;
inline void cdq(int l, int r) {
	if (l >= r) return;
	int mid = (l + r) >> 1;
	cdq(l, mid);
	cdq(mid + 1, r);
	int ll = l, rr = mid + 1;
	for (register int i = l; i <= r; i++) {
		if (ll <= mid && b[ll].x <= b[rr].x || rr > r) {
			if (b[ll].opt == 1) tre.add(b[ll].y, b[ll].x + b[ll].y); //假如是插入才对后面有影响,查询的话前面的cdq已经处理完毕
			temp[i] = b[ll++];
		}
		else {
			if (b[rr].opt == 2) { //假如是查询才操作,插入的话前面的cdq已经处理完毕
				int res = tre.getmax(b[rr].y); //查询小于y的最大x+y
				ans[b[rr].ind] = min(ans[b[rr].ind], b[rr].x + b[rr].y - res);
			}
			temp[i] = b[rr++];
		}
	}
	for (register int i = l; i < ll; i++) if (b[i].opt == 1) tre.clear(b[i].y); //将该层影响消除
	for (register int i = l; i <= r; i++) b[i] = temp[i];
}
int main() {
	cin >> n >> m;
	memset(ans, INF, sizeof(ans));
	tot = 0;
	for (register int i = 1; i <= n; i++) {
		tot++;
		scanf("%d %d", &s[tot].x, &s[tot].y);
		s[tot].x++, s[tot].y++; //对于存在0的+1处理
		s[tot].opt = 1; s[tot].ind = 0;  //假如是插入点的话ind就是0
		mx = max(mx, max(s[tot].x, s[tot].y));
	}
	int que = 0;//que :question 记录有几个询问
	for (register int i = 1; i <= m; i++) {
		tot++;
		scanf("%d %d %d", &s[tot].opt, &s[tot].x, &s[tot].y);
		s[tot].x++, s[tot].y++;
		s[tot].ind = s[tot].opt == 1 ? 0 : ++que; 
		mx = max(mx, max(s[tot].x, s[tot].y));
	}
	mx++; // 为了翻转之后不产生0
	for (register int i = 1; i <= tot; i++) b[i] = s[i]; //复制一次
	cdq(1, tot);

	for (register int i = 1; i <= tot; i++) { //翻转x
		b[i] = s[i];
		b[i].x = mx - s[i].x;;
	}
	cdq(1, tot);
	for (register int i = 1; i <= tot; i++) {
		b[i] = s[i];
		b[i].y = mx - s[i].y;
	}
	cdq(1, tot);
	for (register int i = 1; i <= tot; i++) {
		b[i] = s[i];
		b[i].x = mx - s[i].x;
		b[i].y = mx - s[i].y;
	}
	cdq(1, tot);
	for (register int i = 1; i <= que; i++) printf("%d\n", ans[i]);
	return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值