cdq分治
cdq分治,一种广为人知的离线分治算法。大体的思想是:
- 将左右两边区间分开递归处理。
- 统计左边区间修改对右边区间查询的影响。
第一步很简单,写两个递归就行了。关键在第二步。我们搞个cdq的经典问题——三维偏序来具体解释这个东西。
三维偏序,顾名思义,要求三维都满足某个条件。具体地,对于每个元素有一个三元组
回忆我们解决二维偏序的方法,按照第一维为第一关键字,第二维为第二关键字的顺序排序。然后顺序扫描每个元素,将其第二维插入树状数组并统计答案。显然,第一维已经通过排序保证有序,树状数组又可以保证第二维只查到比当前数小的数,因此算法正确。
现在我们将问题拓展到了三维,我们可以用cdq分治解决问题。首先我们还是按照第一维为第一关键字,第二维为第二关键字,第三维为第三关键字排序,这样我们的第一维就是有序的。
然后我们开始分治。首先我们左右区间内部的贡献可以递归处理,那么我们就只需要考虑区间
关于二维偏序,我们可以将左右两边分别按第二维为第一关键字,第三维为第二关键字进行排序。这样我们左右两边区间的第一维和第二维就都是有序的了。我们可以用两个指针扫描左右区间,用树状数组统计第三维,更新答案。
然后上个代码。注意这个题因为有相同元素,本来相互间有贡献,但是二分的时候会分开,没法相互统计,所以直接去个重。
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方程长得像
但是关于这个的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,修改会影响序列),那么我们就必须先分治左边,然后处理左边对右边影响,最后分治右边。原理和上面是差不多的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现