CDQ 分治 && 整体二分
CDQ 分治
主要用于解决偏序问题。在偏序问题中,以三维偏序居多。它是一种离线算法。
其实严格来说,它是一种思想而不是算法。它依赖于归并排序。
CDQ 分治也可以用于 1D/1D 动态规划的转移,不过目前暂不涉及。
偏序问题
什么是偏序?先从一维偏序说起。
一维偏序
给定
排个序就完了。
二维偏序
给定
把第一维排序,第二维用归并排序或树状数组解决。说到归并排序,其实 CDQ 分治就是利用归并排序的思想做的,后面会提到。
其实逆序对问题就是二维偏序,只不过第一维是位置,已经排好序罢了。
三维偏序
这才是 CDQ 分治的重点。照例第一维排序解决,第二维归并排序,第三维树状数组。
在拿到一个区间
CDQ 分治在关于如何排序
两种写法都给出来。
void cdq(int l,int r){
if(l==r) return;
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
sort(s2+l,s2+mid+1,cmp2);
sort(s2+mid+1,s2+r+1,cmp2);
int j=l;
for(int i=mid+1;i<=r;++i){
while(j<=mid&&s2[i].b>=s2[j].b){
B.add(s2[j].c,s2[j].cnt);
++j;
}
s2[i].ans+=B.sum(s2[i].c);
}
for(int i=l;i<j;++i) B.add(s2[i].c,-s2[i].cnt);
}
void cdq(int l,int r){
if(l==r) return;
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
int t1=l,t2=mid+1;
for(int i=l;i<=r;++i)
if((t1<=mid&&a[t1].b<=a[t2].b)||t2>r){
B.add(a[t1].c,a[t1].cnt);
b[i]=a[t1++];
}else{
a[t2].ans+=B.sum(a[t2].c);
b[i]=a[t2++];
}
for(int i=l;i<t1;++i) B.add(a[i].c,-a[i].cnt);
for(int i=l;i<=r;++i) a[i]=b[i];
}
例题
P3810 【模板】三维偏序(陌上花开)
板子。这题需要注意的是可能有多个点的
void cdq(int l,int r){
if(l==r) return;
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
int t1=l,t2=mid+1;
for(int i=l;i<=r;++i)
if((t1<=mid&&a[t1].b<=a[t2].b)||t2>r){
B.add(a[t1].c,a[t1].cnt);
b[i]=a[t1++];
}else{
a[t2].ans+=B.sum(a[t2].c);
b[i]=a[t2++];
}
for(int i=l;i<t1;++i) B.add(a[i].c,-a[i].cnt);
for(int i=l;i<=r;++i) a[i]=b[i];
}
int main(){
n=read(),K=read();
for(int i=1;i<=n;++i) s1[i]={read(),read(),read(),0,0};
sort(s1+1,s1+n+1);
for(int i=1,top=0;i<=n;++i){
++top;
if(s1[i].a^s1[i+1].a||s1[i].b^s1[i+1].b||s1[i].c^s1[i+1].c){
a[++m]={s1[i].a,s1[i].b,s1[i].c,top,0};
top=0;
}
}
cdq(1,m);
for(int i=1;i<=m;++i)
ans[a[i].ans+a[i].cnt-1]+=a[i].cnt;
for(int i=0;i<n;++i) write(ans[i]);
return fw,0;
}
P4169 [Violet] 天使玩偶/SJY摆棋子
好的,带修了。其实让离线算法带修并不难,仿照当初处理莫队的手法,给它加上一维时间维即可。
接着考虑怎么处理这个坐标问题。思路是巧妙的:首先有两点之间曼哈顿距离公式:
绝对值很麻烦,所以我们暂时只考虑在原点左下方的点,就可以把绝对值去掉了。去掉绝对值之后,
剩下的点,可以旋转坐标使其和上面的问题一样。坑点不少:
- 每次 CDQ 完时间维
会被打乱,与其重新 排序不如直接 复制过去。 - 如果某个点在坐标轴上,即
坐标有 ,此时需要给全局加一,不然树状数组就失败~了。 - 如果某一次翻转没有点在它的左下,树状数组会返回
,如果不做处理的话程序就会以为存在一个 为 的点,但实际上没有,也不可能有(已经没有 或 坐标为 的点)。所以如果树状数组返回的结果是 ,要改成 。
void cdq(int l,int r){
if(l==r) return;
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
int t1=l,t2=mid+1;
for(int i=l;i<=r;++i)
if((t1<=mid&&a[t1].x<=a[t2].x)||t2>r){
if(!a[t1].typ) B.add(a[t1].y,a[t1].x+a[t1].y);
b[i]=a[t1++];
}else{
if(a[t2].typ) ans[a[t2].tm]=min(ans[a[t2].tm],a[t2].x+a[t2].y-B.sum(a[t2].y));
b[i]=a[t2++];
}
for(int i=l;i<t1;++i) B.clear(a[i].y);
for(int i=l;i<=r;++i) a[i]=b[i];
}
void solve(bool t1,bool t2){
for(int i=1;i<=n+m;++i){
a[i]=t[i];
if(t1) a[i].x=N-a[i].x;
if(t2) a[i].y=N-a[i].y;
}
cdq(1,n+m);
}
P4390 [BalkanOI2007] Mokia 摩基亚
同样的三维偏序,只不过查询的时候改成了二维查询。把所有的询问一拆四,CDQ 分治的时候根据种类判断即可。
void cdq(int l,int r){
if(l==r) return;
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
int t1=l,t2=mid+1;
for(int i=l;i<=r;++i)
if((t1<=mid&&a[t1].x<=a[t2].x)||t2>r){
if(!a[t1].typ) B.add(a[t1].y,a[t1].val);
b[i]=a[t1++];
}else{
if(a[t2].typ==1) ans[a[t2].val]+=B.sum(a[t2].y);
else if(a[t2].typ==2) ans[a[t2].val]-=B.sum(a[t2].y);
b[i]=a[t2++];
}
for(int i=l;i<t1;++i) if(!a[i].typ) B.add(a[i].y,-a[i].val);
for(int i=l;i<=r;++i) a[i]=b[i];
}
int main(){
read(),n=read()+1;
int tm=0;
while(1){
int op=read();
if(op==3) break;
if(op==1) a[++tot]={read()+1,read()+1,read(),0};
else{
++tm;
int x1=read()+1,y1=read()+1,x2=read()+1,y2=read()+1;
a[++tot]={x2,y2,tm,1};
a[++tot]={x1-1,y1-1,tm,1};
a[++tot]={x1-1,y2,tm,2};
a[++tot]={x2,y1-1,tm,2};
}
}
cdq(1,tot);
for(int i=1;i<=tm;++i) write(ans[i]);
return fw,0;
}
P4093 [HEOI2016/TJOI2016] 序列
设
具体而言,将
注意用
void cdq(int l,int r){
if(l==r) return dp[l]=max(dp[l],1),void();
int mid=(l+r)>>1;
cdq(l,mid);
for(int i=l;i<=r;++i) p[i]=i;
sort(p+l,p+mid+1,[&](int x,int y){return a[x].mx<a[y].mx;});
sort(p+mid+1,p+r+1,[&](int x,int y){return a[x].x<a[y].x;});
int k=l;
for(int i=mid+1;i<=r;++i){
while(k<=mid&&a[p[k]].mx<=a[p[i]].x)
B.add(a[p[k]].x,dp[p[k]]),++k;
dp[p[i]]=max(dp[p[i]],B.sum(a[p[i]].mn)+1);
}
for(int i=l;i<=mid;++i) B.clear(a[i].x);
cdq(mid+1,r);
}
int main(){
n=read(),m=read();
for(int i=1,x;i<=n;++i) a[i]={x=read(),x,x};
for(int i=1,x,y;i<=m;++i){
x=read(),y=read();
a[x].mx=max(a[x].mx,y);
a[x].mn=min(a[x].mn,y);
}
cdq(1,n);
printf("%d\n",*max_element(dp+1,dp+n+1));
return 0;
}
意犹未尽的感觉。
整体二分
好一个二分!
起因是这样的,遇到有一部分能用二分解决的问题时,我们就可以用
整体二分同样是离线算法。所谓整体二分,其 “整体” 就体现在把询问和原序列一起二分。比如,我用
经典例题
静态区间 k 小值(P3834 【模板】可持久化线段树 2)
这题原本是主席树的板题,但也是整体二分的板题。定义
因为我们解决的是 “区间内小于
剩下的就是个板子。
void solve(int l,int r,int L,int R){
if(L>R) return;
if(l==r){
for(int i=L;i<=R;++i) ans[Q[i].id]=a[id[l]];
return;
}
int mid=(l+r)>>1,p=L-1,q=R+1;
for(int i=l;i<=mid;++i) B.add(id[i],1);
for(int i=L,sm;i<=R;++i){
sm=B.sum(Q[i].r)-B.sum(Q[i].l-1);
if(sm>=Q[i].k) tmp[++p]=Q[i];
else Q[i].k-=sm,tmp[--q]=Q[i];
}
for(int i=l;i<=mid;++i) B.add(id[i],-1);
for(int i=L;i<=p;++i) Q[i]=tmp[i];
for(int i=q;i<=R;++i) Q[R+q-i]=tmp[i];
solve(l,mid,L,p),solve(mid+1,r,q,R);
}
其中 其实主要还是板子。
动态区间 k 小值(P2617 Dynamic Rankings)
加了修改也是一样,不过这次把原序列和询问/修改都加到一个结构体里面,整体二分的时候一并处理。因为我们分的是值域,对询问的时间顺序是没有要求的。所以我们把询问按照时间轴排序之后,考虑如何把修改也放进去。而修改又可以拆成是在
void solve(int l,int r,int L,int R){
if(L>R) return;
if(l==r){
for(int i=L;i<=R;++i)
if(Q[i].op)
ans[Q[i].id]=b[l];
return;
}
int mid=(l+r)>>1,p=L-1,q=R+1;
for(int i=L,sm;i<=R;++i)
if(Q[i].op){
sm=B.sum(Q[i].r)-B.sum(Q[i].l-1);
if(sm>=Q[i].k) tmp[++p]=Q[i];
else Q[i].k-=sm,tmp[--q]=Q[i];
}else{
if(Q[i].k<=b[mid]){
B.add(Q[i].l,Q[i].id);
tmp[++p]=Q[i];
}else tmp[--q]=Q[i];
}
for(int i=L;i<=p;++i)
if(!tmp[i].op)
B.add(tmp[i].l,-tmp[i].id);
for(int i=L;i<=p;++i) Q[i]=tmp[i];
for(int i=q;i<=R;++i) Q[i]=tmp[R+q-i];
solve(l,mid,L,p),solve(mid+1,r,q,R);
}
int main(){
cin.tie(nullptr)->sync_with_stdio(0);
cin>>n>>m;
for(int i=1;i<=n;++i){
cin>>a[i];
b[++cnt]=a[i];
Q[++tot]={0,i,0,a[i],1};
}
for(int i=1,l,r,k;i<=m;++i){
string s;
cin>>s;
if(s=="C"){
cin>>l>>r;
b[++cnt]=r;
Q[++tot]={0,l,0,a[l],-1};
Q[++tot]={0,l,0,r,1};
a[l]=r;
}else{
cin>>l>>r>>k;
Q[++tot]={1,l,r,k,++q};
}
}
sort(b+1,b+cnt+1);
cnt=unique(b+1,b+cnt+1)-b-1;
solve(1,cnt,1,tot);
for(int i=1;i<=q;++i) cout<<ans[i]<<'\n';
return 0;
}
P1527 [国家集训队] 矩阵乘法
换成二维树状数组即可。
void solve(int l,int r,int L,int R){
if(L>R) return;
if(l==r){
for(int i=L;i<=R;++i) ans[Q[i].id]=a[l].c;
return;
}
int mid=(l+r)>>1,p=L-1,q=R+1;
for(int i=l;i<=mid;++i) B.add(a[i].x,a[i].y,1);
for(int i=L,sm;i<=R;++i){
sm=B.sum(Q[i].x1,Q[i].y1,Q[i].x2,Q[i].y2);
if(sm>=Q[i].k) tmp[++p]=Q[i];
else Q[i].k-=sm,tmp[--q]=Q[i];
}
for(int i=l;i<=mid;++i) B.add(a[i].x,a[i].y,-1);
for(int i=L;i<=p;++i) Q[i]=tmp[i];
for(int i=q;i<=R;++i) Q[i]=tmp[R+q-i];
solve(l,mid,L,p),solve(mid+1,r,q,R);
}
P7424 [THUPC2017] 天天爱射击
正难则反,考虑每块木板是被哪个子弹打碎的。题目显然就被转化成了一个区间
注意不能写 pos[read()]=i
,因为一个位置可能有多个数。所以读的时候就正常写 pos[i]=read()
,把
统计答案的时候有不同的统计方法,如果正常统计的话需要多一枚子弹,因为有的木板可能自始至终都没被打碎,多一枚子弹的目的是把多出来的这部分贡献统计到这枚虚弹而不是正常的
void solve(int l,int r,int L,int R){
if(L>R) return;
if(l==r){
for(int i=L;i<=R;++i)
if(Q[i].s==1&&Q[i].l<=pos[l]&&pos[l]<=Q[i].r)
++ans[l];
return;
}
int mid=(l+r)>>1,p=L-1,q=R+1;
for(int i=l;i<=mid;++i) B.add(pos[i],1);
for(int i=L,sm;i<=R;++i){
sm=B.sum(Q[i].r)-B.sum(Q[i].l-1);
if(sm>=Q[i].s) tmp[++p]=Q[i];
else Q[i].s-=sm,tmp[--q]=Q[i];
}
for(int i=l;i<=mid;++i) B.add(pos[i],-1);
for(int i=L;i<=p;++i) Q[i]=tmp[i];
for(int i=q;i<=R;++i) Q[R+q-i]=tmp[i];
solve(l,mid,L,p),solve(mid+1,r,q,R);
}
P4602 [CTSC2018] 混合果汁
很有意思的一道题,回味无穷。
考虑整体二分,在当前询问区间
考虑怎么维护果汁信息。为了实现 “优先考虑价格最低”,我们可以开两个树状数组,分别维护体积和总价,其中总价就是体积乘单价。这样做是为了维护方便维护前缀,充分利用了树状数组的特性。
然后是非常巧妙的一点:我们每次把美味度小于等于
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】