前言
CDQ分治是主要解决一类和点对相关的分治方法,并且要求离线,不太好支持修改,它和点分治有着异曲同工之妙,它主要处理线性数据,而点分治主要处理树上数据
核心思路是将线性数据 [ l , r ] [l, r] [l,r] 拆成两份: [ l , m i d ] , [ m i d + 1 , r ] [l, mid], [mid+1, r] [l,mid],[mid+1,r]递归处理两个区间,再找 ( i , j ) , i ∈ [ l , m i d ] , j ∈ [ m i d + 1 , r ] (i,j),i\in[l,mid],j\in[mid+1,r] (i,j),i∈[l,mid],j∈[mid+1,r]这样的点对,CDQ分治它是一种思想,不是具体算法
它主要可以解决以下三类问题:
- 解决和点对有关的问题
- 1维动态规划的优化与转移
- 通过 CDQ 分治,将一些动态问题转化为静态问题
注:2可以由1转化,
d
p
i
=
1
+
max
j
=
1
i
−
1
d
p
j
[
a
j
<
a
i
&
b
j
<
b
i
]
dp_i = 1 + \max_{j=1}^{i-1}dp_j[a_j<a_i \space \& \space b_j<b_i]
dpi=1+j=1maxi−1dpj[aj<ai & bj<bi] 即只有
j
<
i
&
a
j
<
a
i
&
b
j
<
b
i
j<i\&a_j<a_i\&b_j<b_i
j<i&aj<ai&bj<bi,进而这个转移可以在亚线性时间内处理
3可以由1、2转化而成,不过得要求强制离线,类似于莫队的处理
以上三类CDQ分治相关问题其实可以抽象成一个三维偏序的结构,下面重点讲述这个三维偏序结构,前置知识为:折半排序(本身蕴含着分治思想),以例题为例 ↓ ↓ ↓ ,建立如下算法:
首先设计节点对数据进行存储,struct node 包含以下信息: a i , b i , c i , c n t , r e s a_i,b_i,c_i,cnt,res ai,bi,ci,cnt,res分别代表三个属性值,以及重复节点计数(节点有可能重复),和 f ( i ) f(i) f(i)
然后对读入的节点按照三个属性作为三个关键字进行排序,小在前,大在后,然后对重复节点进行预处理,此时我们便开始进行CDQ分治
假设我们要处理的区间为
[
l
,
r
]
[l,r]
[l,r],我们设
m
i
d
=
(
l
+
r
)
/
2
mid = (l+r)/2
mid=(l+r)/2,
先分治处理
[
l
,
m
i
d
]
[
m
i
d
+
1
,
r
]
[l,mid] [mid+1,r]
[l,mid][mid+1,r],分段处理完以后它有着这样的性质:
i
∈
[
l
,
m
i
d
]
,
j
∈
[
m
i
d
+
1
,
r
]
,
n
o
d
e
[
i
]
.
a
<
n
o
d
e
[
j
]
.
a
i\in[l,mid],j\in[mid+1,r],node[i].a<node[j].a
i∈[l,mid],j∈[mid+1,r],node[i].a<node[j].a
i
,
j
∈
[
l
,
m
i
d
]
o
r
i
,
j
∈
[
m
i
d
+
1
,
r
]
,
i
f
i
<
j
:
n
o
d
e
[
i
]
.
b
<
n
o
d
e
[
j
]
b
i,j\in[l,mid]or\space i,j\in[mid+1,r],if\space i<j:node[i].b<node[j]b
i,j∈[l,mid]or i,j∈[mid+1,r],if i<j:node[i].b<node[j]b
且已更新完
[
l
,
m
i
d
]
[l,mid]
[l,mid]和
[
m
i
d
+
1
,
r
]
[mid+1,r]
[mid+1,r]对区间内部
n
o
d
e
[
i
]
.
r
e
s
node[i].res
node[i].res的贡献,
即
f
(
i
)
f(i)
f(i)受分治区间的贡献更新
然后我们更新
[
l
,
m
i
d
]
[l,mid]
[l,mid]内节点对
[
m
i
d
+
1
,
r
]
[mid+1,r]
[mid+1,r]内节点
f
(
i
)
f(i)
f(i)的贡献更新
(
[
m
i
d
+
1
,
r
]
[mid+1,r]
[mid+1,r]内的节点的a属性值大于
[
l
,
m
i
d
]
[l,mid]
[l,mid]内的,因此不会产生贡献)
再后我们寻找这样的节点:
i
∈
[
l
,
m
i
d
]
,
j
∈
[
m
i
d
+
1
,
r
]
,
n
o
d
e
[
i
]
.
b
<
n
o
d
e
[
j
]
.
b
&
n
o
d
e
[
i
]
.
c
<
n
o
d
e
[
j
]
.
c
i\in[l,mid],j\in[mid+1,r],node[i].b<node[j].b\&node[i].c<node[j].c
i∈[l,mid],j∈[mid+1,r],node[i].b<node[j].b&node[i].c<node[j].c
其实我们是边找边进行折半排序的,这样就保证了
i
f
i
<
j
:
n
o
d
e
[
i
]
.
b
<
n
o
d
e
[
j
]
.
b
if \space i<j:node[i].b<node[j].b
if i<j:node[i].b<node[j].b
折半排序过程中,当我们处理到当前节点为 p ∈ [ l , m i d ] p\in[l,mid] p∈[l,mid] 和 q ∈ [ m i d + 1 , r ] q\in[mid+1,r] q∈[mid+1,r] 时,那么它们是 b 属性比所有已处理完的节点都要大,
那么假如 n o d e [ p ] . b < n o d e [ q ] . b node[p].b<node[q].b node[p].b<node[q].b的话,那么将 p p p插入当前新序列,而由于 p ∈ [ l , m i d ] p\in[l,mid] p∈[l,mid]会产生贡献,那我们也将它插入树状数组中,并且不会 被贡献,也就不用 q u e r y query query
反之,假如 n o d e [ p ] . b > n o d e [ q ] . b node[p].b>node[q].b node[p].b>node[q].b的话,那么将 q q q插入当前新序列,而由于 q ∈ [ m i d + 1 , r ] q\in[mid+1,r] q∈[mid+1,r]不会产生贡献,但会被已有贡献更新 f ( i ) f(i) f(i),那我们就在树状数组中 q u e r y query query它所受的贡献
如此下去,折半排序结束后,节点的 b 属性排序成功, a 属性混乱,但没影响,清空树状数组,至此,我们便完成了整个CDQ分治过程
一、例题 p3810
题目链接:洛谷p3810
二、代码及思路
1.思路
CDQ分治的核心模型,套模型即可
2.代码
代码如下:
#include <algorithm>
#include <iostream>
using namespace std;
const int maxn = 2e5 + 10;
struct node {
int x, y, z;
int cnt, res;
} a[maxn], b[maxn];
int n, m, bit[maxn], ans[maxn], cnt;
bool cmp(node a, node b) {
if (a.x != b.x) return a.x < b.x;
if (a.y != b.y) return a.y < b.y;
return a.z < b.z;
}
int lowbit(int x) { return x & -x; }
void add(int x, int val) {
for (int i = x; i <= m; i += lowbit(i)) bit[i] += val;
}
int query(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += bit[i];
return res;
}
void cdq(int l, int r) {
if (l == r) return;
int mid = (l + r) >> 1;
cdq(l, mid); // 此时 [l,r] 已按照 y 升序排列
cdq(mid + 1, r); // [l,mid], [mid+1,r] 对 a[i].res 的贡献更新完毕
int p = l, q = mid + 1, tot = l;
while (p <= mid && q <= r) { // 归并排序
if (a[p].y <= a[q].y) // 此时仍有 a[p].x <= a[q].x
add(a[p].z, a[p].cnt), b[tot++] = a[p++]; // 将满足要求的z加入树状数组
else
a[q].res += query(a[q].z), b[tot++] = a[q++]; // 利用树状数组累加z贡献
}
while (p <= mid) add(a[p].z, a[p].cnt), b[tot++] = a[p++];
while (q <= r) a[q].res += query(a[q].z), b[tot++] = a[q++]; // 归并完毕
for (int i = l; i <= mid; i++) add(a[i].z, -a[i].cnt); // 清空树状数组
for (int i = l; i <= r; i++)
a[i] = b[i]; // 此时 [l,r] 已按照 y 升序排列,x 序已被破坏
}
signed main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d%d%d", &a[i].x, &a[i].y, &a[i].z), a[i].cnt = 1;
sort(a + 1, a + n + 1, cmp);
cnt = 1;
for (int i = 2; i <= n; i++) {
if (a[i].x == a[cnt].x && a[i].y == a[cnt].y && a[i].z == a[cnt].z)
a[cnt].cnt++;
else
a[++cnt] = a[i];
}
cdq(1, cnt); // a[i].res = \sum_{j=1}^{i-1} [a[j] is satisfied]
for (int i = 1; i <= cnt; i++) ans[a[i].res + a[i].cnt - 1] += a[i].cnt;
for (int i = 0; i < n; i++) printf("%d\n", ans[i]);
return 0;
}