CDQ分治题型总结
概述
CDQ分治是一种通过降维来解决高维偏序问题的分治算法,可避免使用某些复杂数据结构。
算法考虑的关键在于:将两个答案计算完毕的区间合并时,左段区间将会对右段区间造成怎样的影响。
范例
例1:(二维偏序)【BHOJ 14】求逆序对 (其实第一维就是数组下标,已经有序了)
非CDQ解法:
- 权值树状数组 / 权值线段树:把数离散化,每读入一个数就查询在当前数之前的、且比当前数大的数的个数,然后累加进 s u m sum sum,最后输出 s u m sum sum 即可。
- 平衡树:更简单了,边插入边累加 i − r a n k i - rank i−rank,最后输出 s u m sum sum 即可。
CDQ解法:
二元组设定:设二元组 < a , b > <a, b> <a,b> 表示数组第 a a a 个元素的值是 b b b,那求逆序对就是问总共存在多少对 < a i , b i > 、 < a j , b j > <a_i, b_i>、<a_j, b_j> <ai,bi>、<aj,bj> 使得 a i < a j a_i < a_j ai<aj(在它前面)并且 b i > b j b_i > b_j bi>bj(比它大)。
降维预处理:按
a
a
a 把整个二元组数组排升序(其实就是按照原本数组的顺序来…这一步实际不用操作)
由此可以保证每次分治开始前,左段任意一项的
a
a
a 都小于右段任意一项的
a
a
a (1)
“分治”目的:让分治完毕的区间按
b
b
b 升序(按
a
a
a 可能就无序了)。
由于上次分治达成目的,所以现在拿到的左右两段内部都是按
b
b
b 升序的(2)
为了自洽,这次分治需要将两段按
b
b
b 升序进行归并(3)
“分治”过程:
- 由 (1)(2)(3),每次分治过程中产生的新的答案数就可求了。记左右段名字分别为 L 、 R L、R L、R,长度分别为 l e n l 、 l e n r len_l、len_r lenl、lenr。
- 若
∃
i
∈
[
0
,
l
e
n
l
)
、
j
∈
[
0
,
l
e
n
r
)
s
.
t
.
L
[
i
]
.
b
>
R
[
j
]
.
b
\exist i \in [0, len_l)、j\in [0, len_r)\ \ s.t.\ \ L[i].b > R[j].b
∃i∈[0,lenl)、j∈[0,lenr) s.t. L[i].b>R[j].b:
- 由 (2) 知 L [ i . . . l e n l ) . b L[i\ ...\ len_l).b L[i ... lenl).b 都 > R [ j ] . b >R[j].b >R[j].b
- 由 (1) 知 L [ i . . . l e n l ) . a L[i\ ...\ len_l).a L[i ... lenl).a 都 < R [ j ] . a <R[j].a <R[j].a
- 因此统计所有满足条件的 i 、 j i、j i、j,就得到了这次归并过程产生的新的答案数。
- 为了自洽还需要做 (3),因此我们需要在归并的同时找满足条件的 i、j。这显然是易实现的,因为按 b b b 升序归并的过程恰能不重不漏地遇见每一个满足条件的 i、j。
代码:
权值树状数组:
#include <stdio.h>
#include <string.h>
#include <algorithm>
constexpr int MN(1e6+7);
#define lowbit(x) (x)&-(x)
int a[MN];
class Bitree
{
private:
int n;
int a[MN];
public:
inline void init(const int n)
{
this->n = n;
memset(a, 0, sizeof(*a) * (n+1));
}
void add(int i)
{
while (i <= n)
++a[i], i += lowbit(i);
}
int sum(int i)
{
int s = 0;
while (i > 0)
s += a[i], i -= lowbit(i);
return s;
}
} bt;
struct Node
{
int val;
int id;
inline bool operator <(const Node &o) const
{
return val < o.val;
}
} t[MN];
int p[MN];
int main()
{
int n;
while (~scanf("%d", &n))
{
for (int i=1; i<=n; t[i].id=i, ++i)
scanf("%d", &t[i].val);
// discretization
std::sort(t+1, t+n+1);
int tot = 0;
p[t[1].id] = ++tot;
for (int i=2; i<=n; ++i)
p[t[i].id] = t[i].val == t[i-1].val ? tot : ++tot;
// dynamic counting
unsigned ans = 0;
bt.init(n);
for (int i=1; i<=n; ++i)
{
bt.add(p[i]);
ans += i - bt.sum(p[i]);
}
printf("%u\n", ans);
}
}
平衡树(以 SBT 为例):
#include <stdio.h>
#include <string.h>
#define LL long long
#define _BS 1048576
char _bf[_BS];
char *__p1=_bf,*__p2=_bf;
#define _IO char *_p1=__p1,*_p2=__p2;
#define _OI __p1=_p1,__p2=_p2;
#ifdef _KEVIN
#define GC getchar()
#else
#define GC (_p1==_p2&&(_p2=(_p1=_bf)+fread(_bf,1,_BS,stdin),_p1==_p2)?EOF:*_p1++)
#endif
#define PC putchar
#define _Q_(x) {register char _c=GC,_v=1;for(x=0;_c<48||_c>57;_c=GC)if(_c==45)_v=-1;
#define _H_(x) for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=GC);if(_v==-1)x=-x;}
#define sc(x) _Q_(x)_H_(x)
#define se(x) _Q_(x)else if(_c==EOF)return 0;_H_(x)
template<class T>
void UPRT(const T _){if(_>=10)UPRT(_/10);PC(_%10+48);}
constexpr int MN(1e6+7);
template <typename vint, typename xint>
class SBT
{
private:
xint root, tot;
struct SBN
{
xint li, ri, sz;
vint v;
} t[MN];
inline void l_r(xint &i)
{
const xint ri = t[i].ri;
t[i].ri = t[ri].li, t[ri].li = i, t[ri].sz = t[i].sz;
t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;
i = ri;
}
inline void r_r(xint &i)
{
const xint li = t[i].li;
t[i].li = t[li].ri, t[li].ri = i, t[li].sz = t[i].sz;
t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;
i = li;
}
void mt(xint &i, const bool rr)
{
if (rr)
{
if (t[t[t[i].ri].ri].sz > t[t[i].li].sz)
l_r(i);
else if (t[t[t[i].ri].li].sz > t[t[i].li].sz)
r_r(t[i].ri), l_r(i);
else
return;
}
else
{
if (t[t[t[i].li].li].sz > t[t[i].ri].sz)
r_r(i);
else if (t[t[t[i].li].ri].sz > t[t[i].ri].sz)
l_r(t[i].li), r_r(i);
else
return;
}
mt(t[i].li, 0), mt(t[i].ri, 1), mt(i, 0), mt(i, 1);
}
void _insert(xint &i, const vint v)
{
if (i)
{
++t[i].sz;
if (v < t[i].v)
_insert(t[i].li, v), mt(i, 0);
else
_insert(t[i].ri, v), mt(i, 1);
}
else
{
i = ++tot;
t[i].v = v;
t[i].sz = 1;
}
}
xint _lower_rk(xint i, const vint v) const
{
xint rk = 1;
while (i)
{
if (t[i].v >= v)
i = t[i].li;
else
rk += t[t[i].li].sz + 1, i = t[i].ri;
}
return rk;
}
xint _upper_rk(xint i, const vint v) const
{
xint rk = 0;
while (i)
{
if (t[i].v > v)
i = t[i].li;
else
rk += t[t[i].li].sz + 1, i = t[i].ri;
}
return rk;
}
public:
inline void init(const int n)
{
memset(t, 0, sizeof(*t) * (n+2));
root = tot = 0;
}
inline void insert(const vint v)
{
_insert(this->root, v);
}
inline xint lower_rk(const vint v) const
{
return _lower_rk(this->root, v);
}
inline xint upper_rk(const vint v) const
{
return _upper_rk(this->root, v);
}
};
SBT<int, int> sbt;
int main()
{
_IO
int n, tp;
LL sum;
while (1)
{
se(n)
sbt.init(n), sum = 0;
for (int i=1; i<=n; ++i)
{
sc(tp)
sbt.insert(tp);
sum += i - sbt.upper_rk(tp);
}
UPRT(sum), PC(10);
}
return 0;
}
CDQ:
#include <stdio.h>
#include <string.h>
constexpr int MN(1e6);
int a[MN], t[MN];
unsigned tot;
void merge(int *L, int len_l, int len_r)
{
int *R = a + len_l;
int top = 0, i = 0, j = 0;
while (i<len_l && j<len_r)
{
if (L[i] > R[j])
{
t[top++] = R[j++];
tot += len_l - i; // a[i ... len_l-1] 共len_l-1-i+1 == len_l-i个数都比b[j]大,逆序对数+=len_l-i
}
else
t[top++] = L[i++];
}
while (i < len_l)
t[top++] = L[i++];
while (j < len_r)
t[top++] = R[j++];
memcpy(L, t, sizeof(*L) * top);
}
void cdq(int *a, int n)
{
if (n <= 1)
return;
int mid = n >> 1;
cdq(a, mid);
cdq(a+mid, n-mid);
merge(a, mid, n-mid);
}
int main()
{
int n;
while (~scanf("%d", &n))
{
tot = 0;
for (int i=0; i<n; ++i)
scanf("%d", a+i);
cdq(a, n);
printf("%u\n", tot);
}
return 0;
}
好吧上面这个问题好像还不足以体现CDQ的高明之处,现在我们稍微加大难度,看一下三维偏序问题:(大名鼎鼎的板子题陌上花开~)
例2:(三维偏序)【P3810】【模板】三维偏序(陌上花开)
非CDQ解法:第一维排序、第二维树状数组、第三维平衡树
树状数组套平衡树嘛…
先说一下平衡树和树状数组各自的任务:
- 先记所有元素中最大的 b b b 值是 m a x b max_b maxb
- 再记当前处理到第 n o w now now 个元素,其 a , b , c a,\ b,\ c a, b, c 值分别记为 a n o w , b n o w , c n o w a_{now},\ b_{now},\ c_{now} anow, bnow, cnow
- 平衡树:共有 m a x b max_b maxb 棵。每棵树对应一个 b b b 值,记为 t r e e . b tree.b tree.b,这棵树里面存放着 [ 0 , n o w ] [0,\ now] [0, now] 这些元素中 b b b 值等于 t r e e . b tree.b tree.b 的元素的 c c c 值。
- 树状数组:维护 m a x b max_b maxb 这么多棵平衡树的“前缀和”。比如处理到第 n o w now now 个元素的时候,我们所查询的“前缀和”的定义是: t r e e . b ∈ [ 0 , b n o w ] tree.b \in [0, b_{now}] tree.b∈[0,bnow] 的这么多棵树的 c n o w c_{now} cnow 的 r a n k rank rank 之和。
所以如果我们按 a a a 非降序遍历,遍历到 n o w now now 的时候可以就保证前面的 a a a 值都满足 ≤ \le ≤ 条件。此时再查询区间 [ 0 , b n o w ] [0, b_{now}] [0,bnow] 内的 “前缀和”,就又同时满足了 b 、 c b、c b、c 值的 ≤ \le ≤ 条件,就可以得到 n o w now now 这个元素对应的答案了。
实际写代码的时候要注意两个地方:
- 1.最外层按照 a a a 的遍历应是一批一批地进行的。也就是说要一次性插入一批 a a a 相等的元素,然后再挨个查询“前缀和”并记录答案。
- 2.注意这里是小于等于,所以 r a n k rank rank 实际上应该是一个被称为 u p p e r _ r a n k upper\_rank upper_rank 的东西。(并列的时候按最后一个的排名算)
然后会发现树套树这货码量又大常数又大(q^q)!所以偏序问题还是CDQ大法好啊!
CDQ解法:
二元组设定:设二元组 < a , b > <a, b> <a,b> 表示数组第 a a a 个元素的值是 b b b,那求逆序对就是问总共存在多少对 < a i , b i > 、 < a j , b j > <a_i, b_i>、<a_j, b_j> <ai,bi>、<aj,bj> 使得 a i < a j a_i < a_j ai<aj(在它前面)并且 b i > b j b_i > b_j bi>bj(比它大)。
降维预处理:按
a
a
a 把整个二元组数组排升序(其实就是按照原本数组的顺序来…这一步实际不用操作)
由此可以保证每次分治开始前,左段任意一项的
a
a
a 都小于右段任意一项的
a
a
a (1)
“ 分治 ” 目的:让分治完毕的区间按
b
b
b 升序(按
a
a
a 可能就无序了)。
由于上次分治达成目的,所以现在拿到的左右两段内部都是按
b
b
b 升序的(2)
为了自洽,这次分治需要将两段按
b
b
b 升序进行归并(3)
“ 分治 ” 过程:
- 由 (1)(2)(3),每次分治过程中产生的新的答案数就可求了。记左右段名字分别为 L 、 R L、R L、R,长度分别为 l e n l 、 l e n r len_l、len_r lenl、lenr。
- 若
∃
i
∈
[
0
,
l
e
n
l
)
、
j
∈
[
0
,
l
e
n
r
)
s
.
t
.
L
[
i
]
.
b
>
R
[
j
]
.
b
\exist i \in [0, len_l)、j\in [0, len_r)\ \ s.t.\ \ L[i].b > R[j].b
∃i∈[0,lenl)、j∈[0,lenr) s.t. L[i].b>R[j].b:
- 由 (2) 知 L [ i . . . l e n l ) . b L[i\ ...\ len_l).b L[i ... lenl).b 都 > R [ j ] . b >R[j].b >R[j].b
- 由 (1) 知 L [ i . . . l e n l ) . a L[i\ ...\ len_l).a L[i ... lenl).a 都 < R [ j ] . a <R[j].a <R[j].a
- 因此统计所有满足条件的 i 、 j i、j i、j,就得到了这次归并过程产生的新的答案数。
- 为了自洽还需要做 (3),因此我们需要在归并的同时找满足条件的 i、j。这显然是易实现的,因为按 b b b 升序归并的过程恰能不重不漏地遇见每一个满足条件的 i、j。
代码:
树状数组套平衡树(以 SBT 为例):
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>
#define LL long long
#define _BS 1048576
char _bf[_BS];
char *__p1=_bf,*__p2=_bf;
#define _IO char *_p1=__p1,*_p2=__p2;
#define _OI __p1=_p1,__p2=_p2;
#ifdef _KEVIN
#define GC getchar()
#else
#define GC (_p1==_p2&&(_p2=(_p1=_bf)+fread(_bf,1,_BS,stdin),_p1==_p2)?EOF:*_p1++)
#endif
#define PC putchar
#define sc(x) {register char _c=GC,_v=1;for(x=0;_c<48||_c>57;_c=GC)if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=GC);if(_v==-1)x=-x;}
template<class T>
void UPRT(const T _){if(_>=10)UPRT(_/10);PC(_%10+48);}
constexpr int MN(1e5+7), MV(2e5+7);
template <typename vint, typename xint = int>
class SBT
{
public:
xint root[MV], tot;
struct SBN
{
xint li, ri, sz;
vint v;
} t[30 * MV];
public:
inline void l_r(xint &i)
{
const xint ri = t[i].ri;
t[i].ri = t[ri].li, t[ri].li = i, t[ri].sz = t[i].sz;
t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;
i = ri;
}
inline void r_r(xint &i)
{
const xint li = t[i].li;
t[i].li = t[li].ri, t[li].ri = i, t[li].sz = t[i].sz;
t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;
i = li;
}
void mt(xint &i, const bool rr)
{
if (rr)
{
if (t[t[t[i].ri].ri].sz > t[t[i].li].sz)
l_r(i);
else if (t[t[t[i].ri].li].sz > t[t[i].li].sz)
r_r(t[i].ri), l_r(i);
else
return;
}
else
{
if (t[t[t[i].li].li].sz > t[t[i].ri].sz)
r_r(i);
else if (t[t[t[i].li].ri].sz > t[t[i].ri].sz)
l_r(t[i].li), r_r(i);
else
return;
}
mt(t[i].li, 0), mt(t[i].ri, 1), mt(i, 0), mt(i, 1);
}
void insert(xint &i, const vint v)
{
if (i)
{
++t[i].sz;
if (v < t[i].v)
insert(t[i].li, v), mt(i, 0);
else
insert(t[i].ri, v), mt(i, 1);
}
else
{
i = ++tot;
t[i].v = v;
t[i].sz = 1;
}
}
xint lower_rk(xint i, const vint v) const
{
xint rk = 1;
while (i)
{
if (t[i].v >= v)
i = t[i].li;
else
rk += t[t[i].li].sz + 1, i = t[i].ri;
}
return rk;
}
xint upper_rk(xint i, const vint v) const
{
xint rk = 0;
while (i)
{
if (t[i].v > v)
i = t[i].li;
else
rk += t[t[i].li].sz + 1, i = t[i].ri;
}
return rk;
}
};
SBT<int> sbt;
int k;
inline void add(int x, const int v)
{
while (x <= k)
sbt.insert(sbt.root[x], v), x += x&-x;
}
inline int sum(int x, const int v)
{
int sum = 0;
while (x)
sum += sbt.upper_rk(sbt.root[x], v), x -= x&-x;
return sum;
}
struct Node
{
int a, b, c;
inline bool operator <(const Node &o) const
{
return a<o.a;
}
} a[MN];
bool vis[MN];
int ans[MN], f[MN];
int main()
{
_IO
int n;
sc(n)sc(k)
for (int i=0 ;i<n; ++i)
{
sc(a[i].a)
sc(a[i].b)
sc(a[i].c)
}
std::sort(a, a+n);
for (int i=0; i<n; ++i)
{
if (!vis[i])
for (int j=i; a[j].a == a[i].a && j<n; ++j)
add(a[j].b, a[j].c), vis[j] = true;
f[i] = sum(a[i].b, a[i].c);
}
for (int i=0; i<n; ++i)
++ans[f[i]];
for (int i=1; i<=n; ++i)
UPRT(ans[i]), PC(10);
return 0;
}
CDQ:
#include <stdio.h>
#include <string.h>
constexpr int MN(1e6);
int a[MN], t[MN];
unsigned tot;
void merge(int *L, int len_l, int len_r)
{
int *R = a + len_l;
int top = 0, i = 0, j = 0;
while (i<len_l && j<len_r)
{
if (L[i] > R[j])
{
t[top++] = R[j++];
tot += len_l - i; // a[i ... len_l-1] 共len_l-1-i+1 == len_l-i个数都比b[j]大,逆序对数+=len_l-i
}
else
t[top++] = L[i++];
}
while (i < len_l)
t[top++] = L[i++];
while (j < len_r)
t[top++] = R[j++];
memcpy(L, t, sizeof(*L) * top);
}
void cdq(int *a, int n)
{
if (n <= 1)
return;
int mid = n >> 1;
cdq(a, mid);
cdq(a+mid, n-mid);
merge(a, mid, n-mid);
}
int main()
{
int n;
while (~scanf("%d", &n))
{
tot = 0;
for (int i=0; i<n; ++i)
scanf("%d", a+i);
cdq(a, n);
printf("%u\n", tot);
}
return 0;
}