引言:

什么是CDQ分治?其实这是一种思想而不是具体算法,因此CDQ分治覆盖的范围相当广泛,在 OI 界初见于陈丹琦 2008 年的集训队作业中,故被称为CDQ分治。

大致分为三类:

  • cdq分治解决与点对有关的问题
  • cdq分治优化1D/1D 动态规划的转移
  • 通过 cdq 分治,将一些动态问题转化为静态问题

先总体说一下CDQ分治:

通常用来解决一类“修改独立,允许离线”的数据结构题。实际上它的本质是按时间分治,即若要处理时间\([l,r]\)上的修改与询问操作,就先处理\([l,mid]\)上的修改对\([mid + 1,r]\)上的询问的影响,之后再递归处理\([l,mid]\)与\([mid + 1,r]\),根据问题的不同,这几个步骤的顺序有时也会不一样。

CDQ 分治会使得我们考虑的问题的思维难度与代码难度大大减小,通常利用 CDQ 分治能使得一个树套树实现的题目,能够去掉外层的树,改为用分治来进行求解。

算法描述

CDQ分治适用于满足一下两个条件的数据结构题:

  • 修改操作对询问的贡献独立,修改操作之间互不影响效果
  • 题目允许使用离线算法

我们不妨假设我们需要(按顺序)完成的操作序列称为S,考虑将整个操作序列等分为前后两个部分,那么我们可以发下以下两个性质:

  • 显然,后一半操作序列中的修改操作对前一半操作序列中的询问结果不会产生任何影响。
  • 后一半操作序列中的询问操作只受两方面影响:一是前一半操作序列中的所有修改操作;二是后一半操作序列中,在该询问操作之前的修改操作。

容易发现,因为后一半操作序列的修改操作完全不会影响前一半操作序列中的询问结果,因此前一半操作序列的查询实际是与后一半操作序列完全独立的,是与原问题完全相同的子问题,可以递归处理。

接下来我们来考虑后一半操作序列中的询问操作。我们发现,影响后一半操作序列询问的答案的因素中,第二部分“后一半操作序列中,在该询问操作之前的修改操作”也是与前一半序列完全无关的(因为我们前面已经假定题目中的修改操作互相独立互不影响,而询问操作更不会影响修改操作了)因此,这部分因素也是与原问题完全相同的完全独立的子问题,可以递归处理。

我们还可以发现,影响后一半操作序列询问答案案的因素的第一部分“前一半操作序列中的所有修改操作”虽然与前一半序列密切相关,但它有一个非常好的性质,就是现在后一半操作序列的询问都是在前一半操作序列的所有修改完成之后执行的,那么对于影响后半部分询问的前半部分修改,他们对于任意一个后半部分询问的影响都是一模一样的。这时,原问题的动态修改操作便不再存在了,而被转化为了离线的、与原问题同样规模的“一开始给出所有修改”然后“回答若干询问”的更简单的问题,从而简化算法。

不妨设“解决无动态修改操作的原问题”的复杂度为\(O(f(n))\),那么由主定理,我们知道这样分治的时间复杂度是\(O(f(n)logn)\)

因此,只要数据结构题满足我们上文假定的两个要求:修改独立,允许离线,就可以以一个\(log\)的代价,将原问题中的动态修改去掉,变为没有动态修改的简化版问题,极大简化我们的思维与代码难度。

这种对时间分治的方法要求操作之间是互相独立的,因此如果有形如“撤销某次操作”这样的操作,就不能直接按上面的方法来解决问题,因为现在的插入操作可能会被后面的删除操作撤销掉,修改操作并不是完全独立的。

我们依然把操作序列等分为前后凉拌。我们考虑前一半操作中的询问操作,它们的答案显然与后半部分的插入与删除操作无关,因此他们仍然可以直接递归。

考虑后一半操作中的询问操作,它们的答案只与两部分内容有关:

  • 前一半操作序列中的所有插入操作中在该询问之前未被删除的部分。
  • 后一半操作序列中,在该询问操作之前的且未被删除的插入操作。

我们发现,因为后一半序列中的插入操作显然不会在前一半序列中被删除,因此“后一半操作序列中,在该询问操作之前的且未被删除的插入操作“这一部分与前一半操作序列完全无关,也就是与原问题相同的子问题,可以递归求解。

因此现在我们实际要处理的内容是:

“前一半操作序列中的所有插入操作中未被删除的部分”对“后一半操作序列的询问操作”的贡献。

因此,我们的实际任务是初始时给定一些插入操作,然后现在有个操作序列,每个操作会是删除一个插入操作,或是一个询问操作。

这个问题直接解决还是非常困难,但我们发现现在问题中只有删除没有插入,于是一个经典的解决办法就可以派上用场了,那就是“时光倒流”。

因为我们使用的是离线算法,我们完全可以预知转化后的问题中“初始的插入操作”,“改定的删除操作与询问操作”等所有需要的信息,那么我们能实现求出到最后都没有被删除掉的那些初始的插入操作,接着我们逆序处理操作序列,这样询问操作没有变,而删除操作变为了插入操作。利用时光倒流,我们再次把文义转化为了与之前相同的问题。

现在,只要插入操作贡献独立,插入操作之间互不影响,且题目允许离线,即使有身处或者变更操作(变更操作实际等价于西安深处源操作,再插入新操作),我们也可以利用CDQ分治与时光倒流,以2个log的时间复杂度为代价,把题目假话为没有动态插入,没有动态身处,没有动态变更的完全静态版问题。

按照开篇说的三类问题来具体分析的话

CDQ分治解决与点对有关的问题

这类问题一般是给你一个长度为\(n\)的序列,然后当你统计有一些特性的点对\((i,j)\)有多少个,又或者说是找到一对点\((i,j)\)使得一些函数的最大之类的问题

那么CDQ分治机遇这样一个算法流程解决这类问题:

  • 找到这个序列的重点\(mid\)

  • 讲所有点对\((i,j)\)划分为3类

    • 第一种是$ 1 \leq i \leq mid,1 \leq j \leq mid$的点对
    • 第二种是\(1 \leq i \leq mid,mid +1 \leq j \leq n\)的点对
    • 第三种是\(mid +1 \leq i \leq n,mid + 1 \leq j \leq n\)
  • 讲\((1,n)\)这个序列祭城两个序列\((1,mid)\)和\((mid + 1,n)\)会发现第一类点对和第三类点对都在这两个序列之中,递归的去解决这两类点对

  • 想方设法处理一下第二类点对的信息

实际应用时候我们通常都是写一个函数\(solve(l,r)\)表示我们在处理\(l \leq i \leq r,l \leq j \leq r\)的点对

所以刚才的算法流程中的递归部分我们就是通过\(solve(l,mid),solve(mid,r)\)来实现的

所以说cdq分治知识一种十分模糊的思想,可以看大这种思想就是不断的把点对通多递归的方式分给左右两个区间

至于我们设计出来的算法真正干活的部分就是第四部分需要我们想方设法解决的部分

所以我们上几道例题看一下第四部分一般该怎么写

比如说我们来一个cdq分治的经典问题——三维偏序

三维偏序

给定一个序列,每个点有两个属性\((a,b)\),试求:这个序列里有多少对点对\((i,j)\)满足\(i < j,a_i < a_j,b_i<b_j\)

统计序列里点对的个数?我们套一个cdq

假设我们现在正在\(solve(l,r)\)并且通过某些奥妙重重的手段搞定了\(solve(l,mid)\)和\(solve(mid + 1,r)\)

那么我么现在就是统计满足\(l \leq i \leq mid,mid +1 \leq j \leq r\)的点对\((i,j)\)中,有多少个点对还满足\(i < j,a_i < a_j,b_i < b_j\)的限制条件

然后可以发现\(i < j\)的限制条件没有用,既然\(i\)比\(mid\)小,\(j\)比\(mid\)大,那\(i\)一定比\(j\)小_

还可以发现还剩下两个限制条件\(a_i<a_j,b_i<b_j\),根据这个限制条件我们就可以枚举\(j\),求出有多少个满足条件的\(i\)

为了方便枚举,我们把\((l,mid)\)和\((mid + 1,r)\)中的点全部按照\(a\)值从小到大排个序,之后我们依次枚举每一个\(j\),把所有\(a_i<a_j\)的点\(i\)全部插入一个数据结构里。此时我们只要对这个数据结构进行询问:里面有多少个点的\(b\)值是小雨\(b_j\)的,我们就对于这个点\(j\)求出了有多少个\(i\)可以和他合法的匹配了

这个数据结构用树状数组就可了

当我们插入一个\(b\)值等于\(x\)的点时,我们就令树状数组\(x\)这个位置单点+1,而查询数据结构里有多少个点小于\(x\)的操作实际上就是在求前缀和,只要我们实现对于所有的\(b\)值做了离散化我们的复杂度就是对的

文义又来了,钓鱼台有每一个\(j\)我们都需要将所有\(a_i < a_j\)的点插入树状数组中,这样的话我们总共要对树状数组做\(O(n^2)\)次操作。

由于我们把所有的\(i\)和\(j\)都事先按照\(a\)值排好序了,我们以双指针的方式在树状数组里插入点,这样的话我们就只需要做\(O(n)\)次插入操作就行了

所以通过这样一个算法流程我们就用\(O(nlogn)\)的时间处理完了关于第2类点对的信息了

这样的话算法复杂度就是

\[T(n) = T(\lfloor \frac{n}{2} \rfloor) + T(\lceil \frac{n}{2} \rceil) + O(nlogn) = O(nlog^2n)
\]

例题[CQOI2011]动态逆序对

和三维偏序差不多,差不多是一个板子题

#define B cout << "BreakPoint" << endl;
#define O(x) cout << #x << " " << x << endl;
#define O_(x) cout << #x << " " << x << " ";
#define Msz(x) cout << "Sizeof " << #x << " " << sizeof(x)/1024/1024 << " MB" << endl;
#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
#include<stack>
#define LL long long
#define inf 1000000009
const int N = 1e5 + 3,M = 2e5 + 3;
using namespace std;
inline int read() {
int s = 0,w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
w = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
s = s * 10 + ch - '0';
ch = getchar();
}
return s * w;
}
int n,m,c[N],p;
struct treearray {
int ta[N];
void ub(int& x) { x += x & (-x); }
void db(int& x) { x -= x & (-x); }
void c(int x, int t) {
for (; x <= n + 1; ub(x)) ta[x] += t;
}
inline int sum(int x) {
int r = 0;
for (; x > 0; db(x)) r += ta[x];
return r;
}
} ta;
struct data {
int val,del,ans;
} a[N];
LL res;
bool cmp1(const data& a, const data& b) { return a.val < b.val; }
bool cmp2(const data& a, const data& b) { return a.del < b.del; }
void solve(int l, int r) {
if (r - l == 1) return;
int mid = (l + r) / 2;
solve(l, mid), solve(mid, r);
int i = l + 1,j = mid + 1;
while(i <= mid) {
while (a[i].val > a[j].val && j <= r) ta.c(a[j].del, 1),j++;
a[i].ans += ta.sum(m + 1) - ta.sum(a[i].del);
i++;
}
i = l + 1,j = mid + 1;
while(i <= mid) {
while(a[i].val > a[j].val && j <= r) ta.c(a[j].del, -1),j++;
i++;
}
i = mid,j = r;
while(j > mid) {
while(a[j].val < a[i].val && i > l) ta.c(a[i].del,1),i--;
a[j].ans += ta.sum(m + 1) - ta.sum(a[j].del);
j--;
}
i = mid,j = r;
while(j > mid) {
while(a[j].val < a[i].val && i > l) ta.c(a[i].del,-1),i--;
j--;
}
sort(a + l + 1, a + r + 1, cmp1);
return;
}
int main() {
n = read(),m = read();
for(int i = 1; i <= n; i++) a[i].val = read(),c[a[i].val] = i;
for(int i = 1; i <= m; i++) p = read(),a[c[p]].del = i;
for(int i = 1; i <= n; i++) if(a[i].del == 0) a[i].del = m + 1;
for(int i = 1; i <= n; i++) res += ta.sum(n + 1) - ta.sum(a[i].val),ta.c(a[i].val, 1);
for(int i = 1; i <= n; i++) ta.c(a[i].val, -1);
solve(0, n);
sort(a + 1, a + n + 1, cmp2);
for(int i = 1; i <= m; i++) printf("%lld\n", res),res -= a[i].ans;
return 0;
}

CDQ 分治的更多相关文章

  1. 【教程】简易CDQ分治教程&学习笔记

    前言 辣鸡蒟蒻__stdcall终于会CDQ分治啦!       CDQ分治是我们处理各类问题的重要武器.它的优势在于可以顶替复杂的高级数据结构,而且常数比较小:缺点在于必须离线操作. CDQ分治的基 ...

  2. BZOJ 2683 简单题 ——CDQ分治

    [题目分析] 感觉CDQ分治和整体二分有着很本质的区别. 为什么还有许多人把他们放在一起,也许是因为代码很像吧. CDQ分治最重要的是加入了时间对答案的影响,x,y,t三个条件. 排序解决了x ,分治 ...

  3. HDU5618 & CDQ分治

    Description: 三维数点 Solution: 第一道cdq分治...感觉还是很显然的虽然题目不能再傻逼了... Code: /*=============================== ...

  4. 初识CDQ分治

    [BZOJ 1176:单点修改,查询子矩阵和]: 1176: [Balkan2007]Mokia Time Limit: 30 Sec  Memory Limit: 162 MBSubmit: 200 ...

  5. HDU5322 Hope(DP + CDQ分治 + NTT)

    题目 Source http://acm.hdu.edu.cn/showproblem.php?pid=5322 Description Hope is a good thing, which can ...

  6. BZOJ4170 极光(CDQ分治 或 树套树)

    传送门 BZOJ上的题目没有题面-- [样例输入] 3 5 2 4 3 Query 2 2 Modify 1 3 Query 2 2 Modify 1 2 Query 1 1 [样例输出] 2 3 3 ...

  7. BZOJ2683 简单题(CDQ分治)

    传送门 之前听别人说CDQ分治不难学,今天才知道果真如此.之前一直为自己想不到CDQ的方法二很不爽,今天终于是想出来了一道了,太弱-- cdq分治主要就是把整段区间分成两半,然后用左区间的值去更新右区 ...

  8. BNUOJ 51279[组队活动 Large](cdq分治+FFT)

    传送门 大意:ACM校队一共有n名队员,从1到n标号,现在n名队员要组成若干支队伍,每支队伍至多有m名队员,求一共有多少种不同的组队方案.两个组队方案被视为不同的,当且仅当存在至少一名队员在两种方案中 ...

  9. 【BZOJ-3262】陌上花开 CDQ分治(3维偏序)

    3262: 陌上花开 Time Limit: 20 Sec  Memory Limit: 256 MBSubmit: 1439  Solved: 648[Submit][Status][Discuss ...

  10. 【BZOJ-1176&2683】Mokia&简单题 CDQ分治

    1176: [Balkan2007]Mokia Time Limit: 30 Sec  Memory Limit: 162 MBSubmit: 1854  Solved: 821[Submit][St ...

随机推荐

  1. php7 安装redis拓展

    配置之前应该是环境已经搭好了,phpinfo的页面可以加载出来.   使用git clone下载git上的phpredis扩展包 git clone  https://github.com/phpre ...

  2. dubbox生产者与消费者案例

    一.首先要将dubbox添加到本地maven仓库     参考: https://blog.csdn.net/try_and_do/article/details/83383861     二.目录结 ...

  3. Selenium3+python自动化013-自动化数据驱动及模型介绍

    一.查看当前运行的浏览等相关信息 driver=webdriver.Chrome() print(driver.capabilities["version"]) #浏览器版本 pr ...

  4. SpringMVC处理中文乱码

    SpringMVC自带过滤器 添加至web.xml文件 <filter> <filter-name>encoding</filter-name> <filte ...

  5. javascript当中的构造函数的用法

    5)构造函数的用法: 例 3.5.1 <head>    <meta http-equiv="content-type" content="text/h ...

  6. 与大神聊天1h

    与大神聊天1h 啊,与大神聊天1h真的是干货满满 解bug问题 之所以老出bug是因为我老是调用别人的包啊,在调参数的时候,并不知道内部机制 其实就自己写一个函数,然后能把功能实现就好了. 问题是,出 ...

  7. Eclipse导入工程Some projects cannot be imported because they already exist in the workspace

    记录一下本次出错原因,以及解决方法 错误原因: 第一次导入后,删除工程,没有没有勾选Delete project contents on disk(cannot be undone) 解决方法: 1 ...

  8. 计蒜客 - A1633.蒜头君的数轴

    我感觉出的很好的一道题,首先不难想到(其实我刚开始没想到),加点的个数就是找已有点两两形成区间的gcd,那么问题就出在了复杂度上,每次循环哪个区间不要复杂度过高,所以运用正反两次前缀和(?好像不能这么 ...

  9. CQYZOJ P1392 拔河问题

    题目\(1\) Description 一个学校举行拔河比赛,所有的人被分成了两组,每个人必须(且只能够)在其中的一组,且两个组内的所有人体重加起来尽可能地接近. Input 第\(1\)行是一个\( ...

  10. (转)漫游Kafka入门篇之简单介绍

    转自:http://blog.csdn.net/honglei915/article/details/37564521 原文地址:http://blog.csdn.net/honglei915/art ...