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 < a 1 , b 2 < a 2 , . . . b n < a n b_1<a_1,b_2<a_2,...b_n<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(nlogk−1n)
用树状数组优化
用树状数组做二维数点的方法解决二维偏序,也是 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分治巧妙避免写一大堆数据结构。
例题
整体二分
主要用来解决什么区间第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大还是小,大的放到一起做,小的放到一起做,递归下去。
还是比较好理解的。