cdq分治和整体二分学习笔记

CDQ分治

以n维偏序为例,了解CDQ分治的思路。

n维偏序即给出若干个n维空间的点,对每个点 ( a 1 , a 2 , . . . a n ) (a_1,a_2,...a_n) (a1,a2,...an)求有多少个点 ( b 1 , b 2 , . . . , b n ) (b_1,b_2,...,b_n) (b1,b2,...,bn)满足 b 1 &lt; a 1 , b 2 &lt; a 2 , . . . b n &lt; a n b_1&lt;a_1,b_2&lt;a_2,...b_n&lt;a_n b1<a1,b2<a2,...bn<an

CDQ分治的关键在于将对一个点的贡献分多次完成,把多个点的贡献一起完成。所以下面会提到贡献者和接受者,意思就是满足条件的贡献者将把答案贡献给接受者。

一维偏序

假设序列已经有序,那么只要 O ( n ) O(n) O(n)扫一遍序列,记录前缀贡献者个数,加给接受者就好了。

二维偏序

对第一维排序,这时第二维并不有序,所以分治。做法是对于每一个区间,二分分成了左右两块,左边是贡献者,右边是接受者。此时可以发现第一维已经没有用了(已经转化为贡献和被贡献),那么按照第二维排序,做子问题一维偏序

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

n维偏序

既然二维偏序可以归并变成一维偏序,那么三维偏序也可以转化成二维偏序和一维偏序来解决。方法也是一样,先排掉一维,分出贡献者和接受者,那么这一维就可以丢掉啦 (降维打击)

假如是k维的话复杂度: O ( n log ⁡ k − 1 n ) O(n\log^{k-1}n) O(nlogk1n)

用树状数组优化

用树状数组做二维数点的方法解决二维偏序,也是 O ( n log ⁡ n ) O(n\log n) O(nlogn),但是常数好像会小不少。

模板

对比一下可以发现三维和四维本质上是一样的,几乎只需要修改一下const S就好了。那现在你就会了任意维的偏序啦!

三维偏序:

这里为了突出CDQ分治的嵌套,并没有使用树状数组,而且为了能够魔改成四维偏序,结构体用得有点多,导致常数巨大,这个代码可以用于理解,但平常不要这么打。而且想要树状数组的话,只要把递归边界d==0改成d==1然后数个点就好了。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, S = 3;
struct Point{
	int x[S], id, mark, cnt; // x[i]是第i维坐标,mark(0)贡献者,mark(1)接受者,mark(2)两者都是,mark(-1)两个都不是
	bool operator == (const Point &u)const{
		for (int i = 0; i < S; ++ i)
			if (x[i] != u.x[i]) return false;
		return true;
	}
}p[S][N], tmp[N];
int n, nn, k, ans[N], cnt[N];

void solve(int l, int r, int d)
{
	if (l == r){
		if (p[d][l].mark == 2)
			ans[p[d][l].id] += p[d][l].cnt;
		return;
	}
	if (d == 0){ // 一维偏序
		for (int i = l, cntt = 0; i <= r; ++ i){
			if (p[d][i].mark == 2 || p[d][i].mark == 0) cntt += p[d][i].cnt;
			if (p[d][i].mark == 2 || p[d][i].mark == 1) ans[p[d][i].id] += cntt;
		}
		return;
	}
	int mid = (l + r) / 2;
	solve(l, mid, d); // 先做左右两边
	solve(mid + 1, r, d);
	for (int i = l, j = mid+1, k = l; k <= r; ++k){ // 再归并
		if (i <= mid && (j > r || p[d][i].x[d-1] <= p[d][j].x[d-1])){
			tmp[k] = p[d-1][k] = p[d][i]; i++;
			if (p[d-1][k].mark == 2 || p[d-1][k].mark == 0) p[d-1][k].mark = 0;
			else p[d-1][k].mark = -1;
		}
		else{
			tmp[k] = p[d-1][k] = p[d][j]; j++;
			if (p[d-1][k].mark == 2 || p[d-1][k].mark == 1) p[d-1][k].mark = 1;
			else p[d-1][k].mark = -1;
		}
	}
	for (int i = l; i <= r; ++ i)
		p[d][i] = tmp[i];
	solve(l, r, d-1); // 做子问题d-1维偏序
}

bool cmp(Point u, Point v)
{
	for (int i = S-1; i >= 0; --i)
		if (u.x[i] != v.x[i])
			return u.x[i] < v.x[i];
	return 0;
}

void init()
{
	p[S-1][n+1].x[0] = k+1;
	nn = n; n = 0;
	for (int i = 1; i <= nn;){
		p[S-1][++n] = p[S-1][i];
		for (int j = i; j <= nn+1; ++ j){
			if (p[S-1][j] == p[S-1][i])
				++p[S-1][n].cnt;
			else{i = j; break;}
		}
	}
}

int main()
{
	scanf("%d%d", &n, &k);
	for (int i = 1; i <= n; ++ i){
		for (int j = 0; j < S; ++ j)
			scanf("%d", &p[S-1][i].x[j]);
		p[S-1][i].id = i;
		p[S-1][i].mark = 2;
	}
	sort(p[S-1] + 1, p[S-1] + n + 1, cmp);
	init();
	solve(1, n, S-1);
	for (int i = 1; i <= n; ++ i)
		cnt[ans[p[S-1][i].id]] += p[S-1][i].cnt;
	for (int i = 1; i <= nn; ++ i)
		printf("%d\n", cnt[i]);
	return 0;
}

四维偏序:

#include<bits/stdc++.h>
using namespace std;
const int N = 6e4 + 10, S = 4;
struct Point{
	double x[S];
	int id, mark;
	bool operator == (const Point &u)const{
		for (int i = 0; i < S; ++ i)
			if (x[i] != u.x[i]) return false;
		return true;
	}
}p[S][N], tmp[N];
int n, m, k, ans[N];

void solve(int l, int r, int d)
{
	if (l == r) return;
	if (d == 0){
		for (int i = l, cntt = 0; i <= r; ++ i){
			if (p[d][i].mark == 2 || p[d][i].mark == 0) cntt++;
			if (p[d][i].mark == 2 || p[d][i].mark == 1) ans[p[d][i].id] += cntt;
		}
		return;
	}
	int mid = (l + r) / 2;
	solve(l, mid, d);
	solve(mid + 1, r, d);
	for (int i = l, j = mid+1, k = l; k <= r; ++k){
		if (i <= mid && (j > r || p[d][i].x[d-1] <= p[d][j].x[d-1])){
			tmp[k] = p[d-1][k] = p[d][i]; i++;
			if (p[d-1][k].mark == 2 || p[d-1][k].mark == 0) p[d-1][k].mark = 0;
			else p[d-1][k].mark = -1;
		}
		else{
			tmp[k] = p[d-1][k] = p[d][j]; j++;
			if (p[d-1][k].mark == 2 || p[d-1][k].mark == 1) p[d-1][k].mark = 1;
			else p[d-1][k].mark = -1;
		}
	}
	for (int i = l; i <= r; ++ i)
		p[d][i] = tmp[i];
	solve(l, r, d-1);
}

bool cmp(Point u, Point v)
{
	for (int i = S-1; i >= 0; --i)
		if (u.x[i] != v.x[i])
			return u.x[i] < v.x[i];
	return 0;
}

int main()
{
	scanf("%d", &n);
	for (int i = 1; i <= n; ++ i){
		for (int j = 0; j < S; ++ j)
			scanf("%lf", &p[S-1][i].x[j]);
		p[S-1][i].mark = 0;
	}
	scanf("%d", &m);
	for (int i = n+1; i <= n+m; ++ i){
		for (int j = 0; j < S; ++ j)
			scanf("%lf", &p[S-1][i].x[j]);
		p[S-1][i].id = i-n;
		p[S-1][i].mark = 1;
	}
	sort(p[S-1] + 1, p[S-1] + n+m+1, cmp);
	solve(1, n+m, S-1);
	for (int i = 1; i <= m; ++ i)
		printf("%d\n", ans[i]);
	return 0;
}

总结

上面只是CDQ分治的一个小小的应用,CDQ分治的思想应用范围还是很广的。在做一些DP或者其他题的时候,可以用CDQ分治巧妙避免写一大堆数据结构。

例题

[CQOI2011]动态逆序对

整体二分

主要用来解决什么区间第k小的问题(当然是离线的)。据说长得很像决策单调性的分治优化。

过程

自然语言是无力的,不如直接看伪代码

solve(l, r, L, R) // l,r表示待处理的操作的区间,[L,R]是[l,r]区间答案的取值范围
{
	mid = (l + r) / 2;
	for (i : l -> r, j = 0, k = 0)
		if (是修改操作){
			if (权值 <= mid) 放到左边
			else 放到右边
		}
	把左边的操作做掉,存到权值线段树或树状数组中
	for (i : l -> r, j = 0, k = 0)
		if (是查询操作){
			if (左边对他的贡献 >= k) 放到左边
			else 减掉左边对他的贡献, 放到右边
		}
	solve(左边, L, mid)
	solve(右边, mid+1, R)
}

其实意思就是,二分一个值,看看一个查询的答案比这个mid大还是小,大的放到一起做,小的放到一起做,递归下去。

还是比较好理解的。

例题

K-th number

Dynamic Ranking

[ZJOI2013]K大数查询

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值