很久前就想写篇CDQ分治的blog了,现在填坑。
CDQ分治是一种分治算法,一般用于高维数据结构的降维。比如二维数据结构,可以通过CDQ分治变成一个一维的问题。
CDQ分治本质还是个分治。一般分治操作就是,我想知道一个长度为n的区间产生的贡献有多少,那我可以把区间平均划分成两部分,那么此时问题变成左区间产生的贡献+右区间产生的贡献+左区间对右区间产生的贡献+右区间对左区间产生的贡献。然后递归下去,分别再去算出左区间的贡献和右区间的贡献。假设左区间对右区间产生的贡献+右区间对左区间产生的贡献这部分可以om求出,m为区间长度,那么递归一层的复杂度为on。因为每次都是平均划分下去,所以最多递归logn层。因此复杂度为nlogn。
CDQ分治是对操作时间进行分治。我们知道,一般数据结构的操作都是查询,修改等等。我们认为第i个操作是在第i个时间点进行的。我们回顾下前面的分治,如果我们用普通的分治,很容易想到对时间去分治。也就是说我每次把操作分成两部分,然后考虑左区间对右区间产生的贡献+右区间对左区间产生的贡献如何快速求出来。
举个常见的例子。现在给你n个数字,初始状态为0。现在我有两种操作,一种操作是把某个数的数值加上val,另一个操作是查询区间[l, r]的数字总和。这是很经典的单点更新区间查询问题,可以用线段树或者树状数组去做。现在我们考虑CDQ如何解决这个问题。
首先我对操作时间进行分治,左区间产生的贡献和右区间产生的贡献可以递归求,左区间对右区间产生的贡献+右区间对左区间产生的贡献才是重点。我们发现在这个问题中,只有修改操作会影响到查询。也就是说,只有先出现的修改操作会影响后出现的查询操作。比如说我现在先把坐标为1的点增加1,再查询区间[1, 2]的数字和,再把坐标为2的点增加2。只有第一次修改会对查询有影响。那我可以左区间对右区间产生的贡献+右区间对左区间产生的贡献转化成左区间的修改操作对右区间的查询操作的贡献。总结一下就是,我每次把操作划分成两部分,对于左区间我只考虑修改操作,对于右区间我只考虑查询操作,查询只考虑左区间产生的贡献。然后递归下去。
现在问题是如何查询区间和,我们先利用前缀和思想,把查询sum(l, r)转化成sum(1, r) - sum(1, l - 1)。考虑修改了一个数,会对哪些点的前缀和有影响。显然,如果我修改了第i个点,那么第i个点到第n个点的前缀和都会有影响。换句话说,只有下标小的点会影响下标大的点的查询。那我们可以考虑对操作(修改+查询)按照点的下标排序,下标小的在前面。然后按顺序执行每一步操作,维护一个变量sum,修改操作就对这个sum修改,查询就查询这个sum。(PS:这里有个细节,如果对于同一个点既有查询又有修改,要优先修改)
现在问题是,我们真的需要每次都排序吗?分治+排序,或许我们会联想到某种排序算法——归并排序。归并排序是基于分治的排序,他的思想是要让序列变成有序序列,先把序列分成两部分,先递归处理左右两部分,使得左右两部分是有序的。现在只要考虑如何把两个有序序列合并起来。很简单,因为序列是有序的,所以我们可以每次找出两个序列中最小的值,提取出来放到一个新数组里,并重复k次,k为两个序列的数字总数。这样就可以o(k)得到一个新的有序序列。递归logn层,所以复杂度是nlogn。
显然我们可以利用这种思想把排序部分优化掉。采取和归并排序一样的操作,分治的同时保证序列有序。
到此,这题就可以用CDQ分治解决了。复杂度为nlogn,n为序列长度。
模板题测试:https://vjudge.net/problem/HDU-1166
代码:
#include <bits/stdc++.h> using namespace std; const int maxn = 5e4 + 10; struct node { int op, pos, val; // op = 1, add; op = 2, del, op = 3, addquery, op = 4, delquery // pos 代表操作数的下标 // val 对于修改操作代表修改的值,对于查询操作代表是第几个查询 node(int _op = 0, int _pos = 0, int _val = 0) { op = _op, pos = _pos, val = _val; } bool operator < (const node &rhs) const { // 优先下标,然后优先查询操作。 if (pos != rhs.pos) { return pos < rhs.pos; } else { return op < rhs.op; } } }Q[maxn * 3], temp[maxn * 3]; int ans[maxn]; void CDQ(int l, int r) { if (l >= r) { return ; } int mid = (l + r) >> 1; CDQ(l, mid), CDQ(mid + 1, r); int cnt = 0, L = l, R = mid + 1, sum = 0; while (L <= mid && R <= r) { if (Q[L] < Q[R]) { temp[cnt++] = Q[L]; //每次找个小的放到临时数组中 //同时维护sum if (Q[L].op == 1) { sum += Q[L].val; } else if (Q[L].op == 2) { sum -= Q[L].val; } L++; } else { temp[cnt++] = Q[R]; if (Q[R].op == 3) { ans[Q[R].val] += sum; } else if (Q[R].op == 4) { ans[Q[R].val] -= sum; } R++; } } while (L <= mid) { temp[cnt++] = Q[L]; if (Q[L].op == 1) { sum += Q[L].val; } else if (Q[L].op == 2) { sum -= Q[L].val; } L++; } while (R <= r) { temp[cnt++] = Q[R]; if (Q[R].op == 3) { ans[Q[R].val] += sum; } else if (Q[R].op == 4) { ans[Q[R].val] -= sum; } R++; } for (int i = 0; i < cnt; i++) { Q[l + i] = temp[i]; } } int main() { int T; scanf("%d", &T); for (int c = 1; c <= T; c++) { int cnt = 0, Qcnt = 0; int n, x, l, r; char op[10]; scanf("%d", &n); // 如果初始状态不为零,可以当成一开始就进行了n次add操作 for (int i = 1; i <= n; i++) { scanf("%d", &x); Q[++cnt] = node(1, i, x); } while (true) { scanf("%s", op); if (op[0] == 'E') { break; } scanf("%d%d", &l, &r); if (op[0] == 'A') { Q[++cnt] = node(1, l, r); } else if (op[0] == 'S') { Q[++cnt] = node(2, l, r); } else if (op[0] == 'Q') { //查询操作按照前缀和拆分 Q[++cnt] = node(3, r, Qcnt); Q[++cnt] = node(4, l - 1, Qcnt); Qcnt++; } } CDQ(1, cnt); printf("Case %d: ", c); for (int i = 0; i < Qcnt; i++) { printf("%d ", ans[i]); ans[i] = 0; } } return 0; }
补充:关于前面讲过,CDQ分治可以代替掉一维的数据结构,所以如果需要做二维数据结构,可以对一维进行归并排序,第二维用个树状数组维护。举个例子,比如求矩阵和,可以把矩阵分解成四个前缀矩阵加加减减。然后对其中一维进行归并,但是由于第二维不是有序的,所以要用树状数组维护。