CDQ分治和整体二分都属于离线算法
CDQ分治
CDQ分治的流程与普通分治的流程区别在于,当我们递归解决[L,M]和[M+1,R]区间后,除了将两个子区间的答案合并还要再加上右区间对左区间的贡献。
CDQ分治主要用于求解偏序问题。
下面用一个简单的示例说明CDQ的基本思想。
CDQ分治求逆序数
求逆序数实质是一个二维偏序问题,相当于一个由有序点对$(x,y)$组成的集合,求对于每个$(x_i,y_i)$有多少对$(x_j,y_j)$使得$x_i>x_j且y_i<y_j$的和。
首先一个区间其下标必然是有序的,于是我们只用考虑用$y$如何进行统计。那么参照上面所说的CDQ分治的思想,我们分别计算左右区间的逆序对个数并求和,然后算出右区间对左区间的贡献,总和即为整个当前区间的逆序对数。
1 | ll CDQ(int *p, int len) { |
其他问题
值得一提的是,通常在进行CDQ分治之前我们会将数组排序,来确保可以通过计算右区间中每个点对左区间的贡献来得到正确答案。
对于多维偏序问题,我们依然可以使用CDQ分治,例如三维偏序,我们可以对第一维排序、第二维CDQ、第三维用数据结构或CDQ统计答案。
来看一个三维偏序的问题。
洛谷P3810 【模板】三维偏序(陌上花开)
我们很容易就想到,先以a为第一关键字、b为第二关键字、c为第三关键字排序,每次将区间分成两半,这样左区间的a都是小于等于右区间的a的,这样我们只需要计算对右边每对(b,c)左边有多少对满足条件的(b,c)即可
显然,b如果在两个区间内分别有序,我们的计算将会非常方便,用双指针扫一遍,若右区间的b大于等于左区间的b,则此时左区间的c就有可能对右区间的c产生贡献,因此我们用一个c值域上的树状数组维护当前有多少产生贡献的c,在右区间的b小于左区间的b时查询此时的贡献。
但是这里存在一个问题,此处我们用CDQ分治,要求只能是左区间对右区间产生贡献,但如果有a b c相同的元素分别属于左右区间,这时候右区间对左区间也能产生贡献。而我们的解决方法就是在开始的时候就对整个数组进行去重操作,记录下每个元素的个数有多少,然后对不同元素间进行上述分治+树状数组求解,每个元素的答案再加上相同元素个数-1(其他所有元素都对他产生贡献)就是该元素真正的答案。
1 |
|
事实上,CDQ是可以不断套娃的,三维偏序问题还可以通过CDQ套CDQ来解决。
第一层CDQ与上面的过程类似,只不过我们不需要进行树状数组的操作,只是在原来的update和query处打上tag标记,表示这个位置是计入答案或者统计答案。
在排序完成后我们在CDQ内对同一区间进行第二层CDQ,此时的区间是以b为第一关键字,c为第二关键字排序后的结果,因此跟一层CDQ就非常类似的,唯独是在第二层CDQ中计入和统计答案时要注意第一层CDQ中打的标记,比如如果当前左区间的c小于等于右区间的c,但是第一层中并没有给他打标记,就说明当前右区间的元素实际上在第一层时是在左区间,于是就不能进行贡献统计。
1 | void CDQ2(Node *p, int len) { |
以此类推,其实无论多少维的偏序问题我们都可以通过CDQ套娃来解决。
整体二分
咕咕咕