cdq分治,一种广为人知的离线分治算法。大体的思想是:
第一步很简单,写两个递归就行了。关键在第二步。我们搞个cdq的经典问题——三维偏序来具体解释这个东西。
P3810 【模板】三维偏序(陌上花开)
三维偏序,顾名思义,要求三维都满足某个条件。具体地,对于每个元素有一个三元组\((a_i,b_i,c_i)\),我们要求对每个元素\(i\)满足偏序关系\(a_j\le a_i,b_j\le b_i,c_j\le c_i\)的元素个数。
回忆我们解决二维偏序的方法,按照第一维为第一关键字,第二维为第二关键字的顺序排序。然后顺序扫描每个元素,将其第二维插入树状数组并统计答案。显然,第一维已经通过排序保证有序,树状数组又可以保证第二维只查到比当前数小的数,因此算法正确。
现在我们将问题拓展到了三维,我们可以用cdq分治解决问题。首先我们还是按照第一维为第一关键字,第二维为第二关键字,第三维为第三关键字排序,这样我们的第一维就是有序的。
然后我们开始分治。首先我们左右区间内部的贡献可以递归处理,那么我们就只需要考虑区间\([l,mid]\)的元素对区间\([mid+1,r]\)内答案的贡献。此时,由于我们第一维已经排好序(显然,这样二分是不会有区间重复的,参考线段树),那么左区间\([l,mid]\)的第一维元素就会小于等于右区间\([mid+1,r]\)的元素,也就是不用考虑第一维。这样我们就转化成了普通的二维偏序。
关于二维偏序,我们可以将左右两边分别按第二维为第一关键字,第三维为第二关键字进行排序。这样我们左右两边区间的第一维和第二维就都是有序的了。我们可以用两个指针扫描左右区间,用树状数组统计第三维,更新答案。
然后上个代码。注意这个题因为有相同元素,本来相互间有贡献,但是二分的时候会分开,没法相互统计,所以直接去个重。
struct node{ int a,b,c,ans; }s[1000010]; bool cmp1(node a,node b){ return a.a==b.a?(a.b==b.b?a.c<b.c:a.b<b.b):a.a<b.a; } bool cmp2(node a,node b){ return a.b==b.b?a.c<b.c:a.b<b.b; } void cdq(int l,int r){ if(l==r)return;//边界为只有一个元素 int mid=(l+r)>>1; cdq(l,mid);cdq(mid+1,r);//第一步 递归处理两边 sort(s+l,s+mid+1,cmp2);sort(s+mid+1,s+r+1,cmp2); int j=l;//左区间指针 for(int i=mid+1;i<=r;i++){ while(s[j].b<s[i].b&&j<=mid){ update(s[j].c,s[j].cnt);j++; } s[i].ans+=query(s[i].c); } for(int i=l;i<j;i++)update(s[i].c,-s[i].cnt);//每次统计之后清空树状数组 } int main(){ //输入与去重 sort(s+1,s+n+1,cmp1); cdq(1,n); }
然后是另一个东西:cdq分治优化dp。
具体地说,有的题的dp方程长得像\(dp[i]=\max_{j=1}^{i-1}dp[j]([a_j<a_i][b_j<b_i])+k\)这种。发现这个东西,\(a\)是一维,\(b\)是一维,位置也是一维,于是就成了个三维偏序,可以用cdq来处理。
但是关于这个的cdq有一点要注意,就是一定要先递归左边,然后处理左边对右边的影响,然后还原(这个非常重要,因为你一定要按照原数组进行dp而不是排序以后的数组。举个例子,你求最大子序列和是直接扫还是排序再扫),最后递归右边。
原因是,我们普通的dp都是从左到右处理的。但是cdq分治是先处理小区间然后再处理大区间。如果我们按照普通的cdq写,那么先处理左边,再处理右边,统计左边对右边的贡献的时候,转移顺序就乱掉了。(毕竟我们正常dp也没有先左边再右边的吧)
处理方法也很简单,树状数组清空之后再按照第一维排一边序初始化整个数组,然后再递归右边就行了。
随便粘了个题的cdq部分。
void cdq(int l,int r){ if(l==r)return; int mid=(l+r)>>1;cdq(l,mid); sort(s+l,s+mid+1,cmp1);sort(s+mid+1,s+r+1,cmp2); int j=l; for(int i=mid+1;i<=r;i++){ while(s[j].val<=s[i].l&&j<=mid){ update(s[j].r,dp[s[j].t]); j++; } dp[s[i].t]=max(dp[s[i].t],query(s[i].val)+1); } for(int i=j-1;i>=l;i--)clear(s[i].r); sort(s+l,s+r+1);cdq(mid+1,r);//一定要先左再右 }
还有一点就是将动态问题(带修改)转化为静态问题。
举个例子,维护一个二维平面,然后支持在一个矩形区域内加一个数字,每次询问一个矩形区域的和。
如果不带修,我们有一个比较简洁的做法:扫描线。我们将每个矩形变成两条边,一条加,一条减回来。然后查询的时候我们维护一条一定长度的扫描线统计答案。
现在给你带修,我们可以仍然使用cdq分治。将修改拆成加减两个操作,询问差分成几个大减小。左对右的影响可以直接修改并且用扫描线处理,然后递归分治左右两边的关系。这样,我们就把动态问题转化为了静态问题。
另外的,如果各个修改之间独立(比如普通的加减,像上题一样),那么递归向下分治的顺序是无所谓的。然而,如果修改会对整个序列进行影响,而且后面的序列长什么样取决于前面的序列(比如上边的dp,修改会影响序列),那么我们就必须先分治左边,然后处理左边对右边影响,最后分治右边。原理和上面是差不多的。