当前位置: 首页 > news >正文

神秘分治 学习笔记

前言

个人认为分治是一种神秘的算法,尤其是你在调代码的时候。

  • 什么是分治?

分治其实是一种思想,通过将一个大范围的问题分成许多小范围问题来求解。
对,就是「分以治之」。
这难免会想到一些与之相似的算法,像二分、动态规划,但这其实还是有区别的。
至于具体怎么个区别,本蒟蒻语文水平有限,还是自己体会吧。

cdq分治

介绍

cdq分治最早由IOI金牌得主陈丹琦在高中整理并总结,cdq分治也因此而得名。
cdq分治并不是一种具体的算法,而是一种思想,有十分强大的扩展性。
其主要应用于以下方面:

  • 多维偏序问题
  • 动态规划优化转移

其主要思想是将一些复杂问题转化为偏序问题进行求解,同时cdq分治是一种离线思想,处理不了强制在线的问题,如果出现了强制在线的偏序问题那本蒟蒻只能洗洗睡了。

二维偏序

  • 停停停,还没有说什么是偏序呢。

其实偏序的定义说了也看不懂

偏序关系

\(R\) 是集合 \(A\) 上的一个二元关系,若 \(R\) 满足:

  • 自反性:\(\forall x\in A,xRx\)
  • 反对称性:\(\forall x,y\in A\)\(xRy,yRx\)\(x=y\)
  • 传递性:\(\forall x,y,z\in A\)\(xRy,yRz\)\(xRz\)

则称 \(R\)\(A\) 上的偏序关系,通常记作 \(≼\)

其实可以把 $R$ 理解成 $\le$、$\ge$ 之类的东西。

下面就开始正式的二位偏序了。

  • 为什么不从一维开始?
  • 因为一维没什么东西

最著名的二位偏序问题就是逆序对个数问题,即求解一个排列中满足:

  • 下标 \(i < j\)
  • \(a_i>a_j\)

这样的数对的对数,这也就是二位偏序中的那「二维」。
想必已经不是第一次看这样的题了,想一想这道题可以用什么方法做?

  • 树状数组/线段树
  • 归并排序

其实这些就是cdq分治的思想。

  • 对于树状数组,统计前面所有点对当前的贡献,我们按下标顺序依次填入(在其所对应的值上添加 \(1\)),在填入一个值之前,查询比当前值小的有多少个,并累计在答案中。
  • 对于归并排序,统计左区间对右区间的贡献,我们将一个区间 \([l,r]\)\([l,mid]\)\([mid+1,r]\) 分成两个子区间向下递归,回溯时,左右两个区间内部已经各自拍好序,用双指针同时扫两个区间,因为左区间的下标一定小于右区间的下标,所以只需要统计对于右区间的每一个数,在左区间内有多少个比它小的,统计在答案中即可。

其实使用归并排序或树状数组的前提都是其中一位偏序——下标是有序的,也就是说归并排序和树状数组都只能干掉其中的一维偏序。

从二位到三维

陌上花开(三维偏序)\(^{luoguP3810}\)

题目中的三维偏序是很显然的,那要怎么做呢?
既然归并排序和树状数组都能干掉一维偏序,那么它俩结合起来不就能干两维了吗,那剩下的一维怎么整?
直接排序就行了。
也就是说,对于三维偏序问题,对于每一维我们分别使用:

  1. sort排序
  2. 归并排序(cdq分治)
  3. 树状数组

就可以轻松干掉这道题了。
唯一注意的一点在排序的cmp函数!?

#include<bits/stdc++.h>
using namespace std;
const int NUM=1e6+10;struct node{int s,c,m,id,op;
}q[NUM],tmp[NUM];
int ans[NUM];
int anss[NUM];
int n,k,tot;
int C[NUM];
void add(int x,int val){for(int i=x;i<=k;i+=(i&-i)) C[i]+=val;
}
int sum(int x){int ans=0;for(int i=x;i;i-=(i&-i)) ans+=C[i];return ans;
}void cdq(int l,int r){if(l==r) return;int mid=(l+r)>>1;cdq(l,mid),cdq(mid+1,r);int i=l,j=mid+1,k=i;while(i<=mid&&j<=r){if(q[i].c<=q[j].c){if(!q[i].op) add(q[i].m,1);tmp[k++]=q[i++];}else{if(q[j].op) ans[q[j].id]+=sum(q[j].m);tmp[k++]=q[j++];}}while(j<=r){if(q[j].op) ans[q[j].id]+=sum(q[j].m);tmp[k++]=q[j++];}for(int p=l;p<i;++p) if(!q[p].op) add(q[p].m,-1);while(i<=mid){tmp[k++]=q[i++];}for(int i=l;i<=r;++i) q[i]=tmp[i];
}int main(){cin>>n>>k;for(int i=1,x,y,z;i<=n;++i){cin>>x>>y>>z;q[++tot]={x,y,z,i,0};q[++tot]={x,y,z,i,1};}sort(q+1,q+1+tot,[](node x,node y){if(x.s!=y.s) return x.s<y.s;if(x.c!=y.c) return x.c<y.c;if(x.m!=y.m) return x.m<y.m;return x.op<y.op;});cdq(1,tot);for(int i=1;i<=tot;++i){if(q[i].op){anss[ans[q[i].id]-1]++;}}for(int i=0;i<n;++i){cout<<anss[i]<<'\n';}return 0;
}

从三维偏序到多维偏序

【模板】离线静态四维数点\(^{luoguP14957}\)

还在叠加。
上面说过,使用sort排序、归并排序、树状数组可以干掉三维,现在就只剩下了一维。
可不可以用线段树?
我不是把线段树和树状数组放一块了吗,它俩是一个东西。
不过貌似好像也许真的可以用树套树去做,不过本蒟蒻树套树是贺的。
既然树套树不行,可以cdq套cdq啊!
具体过程如下:

  1. 在主函数中案第一维排好序。
  2. 在cdq1中左区间打上标记 \(0\),右区间打上标记 \(1\),并按第二维排序,传入cdq2。
  3. 在cdq2中使用双指针扫描左右区间,当且仅当左区间的一个值的标记为 \(0\),才能对右区间做出贡献,将其添入树状数组,但且仅当右区间的一个值的标记为 \(1\) 才可以从树状数组中获取贡献并累计到答案中。

其实会了四维,五维、六维什么的也都不在话下了。

#include<bits/stdc++.h>
using namespace std;
const int NUM=10e5+10;
const int NUMM=100e5+10;int n,m;
struct node{int x,y,x2,y2,op,id,tag;
}A[NUM],tmp[NUM];
int tot,num;
int cpy[NUMM],cnt;
int C[NUM];
int ans[NUM];
void add(int x,int val){for(int i=x;i<=cnt+10;i+=(i&-i)) C[i]+=val;
}
int sum(int x){int res=0;for(int i=x;i;i-=(i&-i)) res+=C[i];return res;
}
int lb(int x){return lower_bound(cpy+1,cpy+1+cnt,x)-cpy;
}
void cdq2(int l,int r){if (l == r) return;int mid=(l+r)>>1;cdq2(l,mid),cdq2(mid+1,r); int i=l,j=mid+1,k=l;while(i<=mid&&j<=r){if(A[i].x2<=A[j].x2){if(!A[i].tag&&!A[i].op) add(A[i].y2,1);tmp[k++]=A[i++];}else{if(A[j].tag&&A[j].op) ans[A[j].id]+=sum(A[j].y2);tmp[k++]=A[j++];}}while(j<=r){if(A[j].tag&&A[j].op) ans[A[j].id]+=sum(A[j].y2);tmp[k++]=A[j++];}for(int p=l;p<i;++p) if(!A[p].tag&&!A[p].op) add(A[p].y2,-1);while(i<=mid) tmp[k++]=A[i++];for(int i=l;i<=r;++i) A[i]=tmp[i];
} void cdq1(int l,int r){if(l==r) return;int mid=(l+r)>>1;cdq1(l,mid),cdq1(mid+1,r);for(int i=l;i<=mid;++i) A[i].tag=0;for(int i=mid+1;i<=r;++i) A[i].tag=1;stable_sort(A+l,A+r+1,[](node x,node y){if(x.y!=y.y) return x.y<y.y;if(x.x2!=y.x2) return x.x2<y.x2;return x.y2<y.y2;});cdq2(l,r);
}signed main(){cin>>n>>m;for(int i=1,x,y,x2,y2;i<=n;++i){cin>>x>>y>>x2>>y2;cpy[++cnt]=-y2;A[++tot]={x,y,-x2,-y2,0,0,0};}for(int i=1,x,y,x2,y2;i<=m;++i){cin>>x>>y>>x2>>y2;cpy[++cnt]=-y2;A[++tot]={x,y,-x2,-y2,1,i,0};}sort(cpy+1,cpy+1+cnt);cnt=unique(cpy+1,cpy+1+cnt)-cpy;for(int i=1;i<=tot;++i){A[i].y2=lb(A[i].y2);}stable_sort(A+1,A+1+tot,[](node x,node y){if(x.x!=x.y) return x.x<y.x;if(x.y!=y.y) return x.y<y.y;if(x.x2!=y.x2) return x.x2<y.x2;return x.y2<y.y2;});cdq1(1,tot);for(int i=1;i<=m;++i){cout<<ans[i]<<'\n';}return 0;
}

整体二分

介绍

二分答案自然是一种很好的算法,虽然这会使你的算法复杂度多加一个 \(log\),但在大多数情况都无伤大雅,但偏偏就有有些时候多加一个 \(log\) 就会死,这就不得不请出整体二分了。
如果说二分答案会让你的程序多加一个 \(log\),那么整体二分则是将那个 \(n\) 换成 \(log\),听起来非常厉害,在实现上其实是将全部寻问一次查完。

实现

其在是线上类似于cdq分治,将一个大区间分成两个小区间,并将寻问分到对应的区间中去。

【模板】可持久化线段树 2\(^{luoguP3834}\)

等等,这不是主席树的题吗?
寻问区间第 \(k\) 大整体二分也可以干呢,而且无论时间还是空间都比主席树更优,唯一的缺点可能就是整体二分是离线算法,而主席树则是在线算法。
这可能就是离线算法的特点了吧。
好了回归正题。
具体做法就是扫描属于当前值域的全部操作(注意在整体二分的时候,将原数列当成添加操作),先扫描添加操作,在扫描查询操作,并用树状数组维护区间元素个数。
设当前值域区间为 \([l,r]\),重点为 \(mid\)
如果当前操作时添加一个数:

  • 若当前元素在 \([l,mid]\) 之间,则将其填入树状数组,并放入左区间的临时数组。
  • 若当前元素在 \([mid+1,r]\) 之间,则直接将其放入右区间的临时数组。

如果当前操作时查询:
首先在树状数组中查询当前操作所要查询的区间的元素个数(当然,时小于 \(mid\) 的元素个数),记为 \(cnt\)

  • \(cnt\ge k\) 说明答案在左区间,将其放入左区间的临时数组。
  • \(cnt < k\) 说明答案在右区间,讲 \(k\) 减去 \(cnt\) ,并将其放入右区间临时数组(相当于在右区间查询 \(k-cnt\),根权值线段树查排名的值一个思路)。

最后别往了:

  • 将临时数组中的数放到查询数组中。
  • 清空树状数组。

一个好理解但空间会爆炸的代码(vector存储):

#include<bits/stdc++.h>
using namespace std;
const int NUM=5e6+10;struct oper{int t;int l,r,k,id;
}; int n,m;
int a[NUM],ans[NUM];
int C[NUM];
void add(int x,int val){for(int i=x;i<=n;i+=(i&-i)) C[i]+=val;
}
int sum(int x){int ans=0;for(int i=x;i;i-=(i&-i)) ans+=C[i];return ans;
}
int query(int l,int r){return sum(r)-sum(l-1);
}
void sol(int l,int r,vector<oper>& ops){if(ops.empty()) return;if(l==r){for(auto& op:ops){if(op.t==1){ans[op.id]=l;}}return;}int mid=(l+r)>>1;vector<oper> left,right;for(oper& op:ops){if(op.t==0){if(op.k<=mid){add(op.l,1);left.push_back(op);}else{right.push_back(op);}}}for(oper& op:ops){if(op.t==1){int cnt=query(op.l,op.r);if(cnt>=op.k){left.push_back(op);}else{op.k-=cnt;right.push_back(op);}}}for(oper& op:ops){if(op.t==0&&op.k<=mid) add(op.l,-1);}sol(l,mid,left);left.clear();sol(mid+1,r,right);right.clear();
}int main(){cin>>n>>m;int minn=INT_MAX,maxx=INT_MIN;for(int i=1;i<=n;++i){cin>>a[i];minn=min(minn,a[i]);maxx=max(maxx,a[i]);}vector<oper> ops;for(int i=1;i<=n;++i){ops.push_back({0,i,0,a[i],0});}for(int i=1;i<=m;++i){int l,r,k;cin>>l>>r>>k;ops.push_back({1,l,r,k,i});}sol(minn,maxx,ops);for(int i=1;i<=n;++i){cout<<ans[i]<<'\n';}return 0;
}

整体二分的竞赛风写法(数组存储):

#include<bits/stdc++.h>
using namespace std;const int NUM=1e6+10;
const int inf=0x3f3f3f3f;struct node{int l,r,v,id;
}q[NUM],tp[NUM];
int n,m,a[NUM],len;
int C[NUM],ans[NUM];
inline void add(int x,int val){for(int i=x;i<=n;i+=(i&-i)) C[i]+=val;
}
inline int sum(int x){int ans=0;for(int i=x;i;i-=(i&-i)) ans+=C[i];return ans;
}
void clear(int x){for(int i=x;i<=n;i+=(i&-i)) C[i]=0; 
}
inline void sol(int ql,int qr,int l,int r){if(l==r){while(ql<=qr){if(q[ql].id) ans[q[ql].id]=l;++ql;}return;}int mid=(l+r)>>1,il=ql,ir=qr;for(int i=ql;i<=qr;++i){if(!q[i].id){if(q[i].v<=mid) add(q[i].l,1),tp[il++]=q[i];else tp[ir--]=q[i];}else{int cnt=sum(q[i].r)-sum(q[i].l-1);if(cnt>=q[i].v) tp[il++]=q[i];else q[i].v-=cnt,tp[ir--]=q[i];}}for(int i=ql;i<il;++i){q[i]=tp[i];if(!q[i].id&&q[i].v<=mid) clear(q[i].l);}for(int i=il;i<=qr;++i){q[i]=tp[qr-i+il];}if(il!=ql) sol(ql,ir,l,mid);if(ir!=qr) sol(il,qr,mid+1,r);
}int main(){cin>>n>>m;for(int i=1;i<=n;++i){cin>>a[i];q[i]={i,i,a[i],0};}sort(a+1,a+1+n);len=unique(a+1,a+1+n)-a-1;for(int i=1;i<=n;++i){q[i].v=lower_bound(a+1,a+1+len,q[i].v)-a;}for(int i=1;i<=m;++i){cin>>q[i+n].l>>q[i+n].r>>q[i+n].v;q[i+n].id=i;}sol(1,n+m,1,len);for(int i=1;i<=m;++i){cout<<a[ans[i]]<<'\n';}return 0;
}

提示
双倍经验:可怜的狗狗\(^{louguP1533}\)
你甚至连改都不用改

猫树分治

在学猫树分治前,应先学习猫树。

介绍

其实猫树分治没多少东西,类似于前面的分治算法,在跑猫树的时候计算答案即可。

好吃的题目\(^{luoguP6240}\)

一道猫树套DP的题。
数据范围过大,预处理数组开不开?
于是我们考虑在跑猫树的时候计算答案。
对于区间背包的合并,可以这样做:

\[ans=\max_{j=0}^{t_i}\{f[l_i][j]+f[r_i][t_i-j]\} \]

#include <bits/stdc++.h>
using namespace std;
const int NUM1=6e4+10;
const int NUM2=2e2+10;
const int NUM3=2e5+10;
struct node{int l,r,t;
}q[NUM3];
int n,m;
int h[NUM3],w[NUM3];
int f[NUM1][NUM2];
int p[NUM3],s[NUM3],tot;
int ans[NUM3];void sol(int l,int r,int tl,int tr){if(tl>tr) return;int mid=(l+r)>>1,tmid=tl-1;for(int i=0;i<=200;++i) f[mid][i]=0;for(int i=mid+1;i<=r;++i){for(int j=0;j<h[i];++j) f[i][j]=f[i-1][j];for(int j=h[i];j<=200;++j){f[i][j]=max(f[i-1][j],f[i-1][j-h[i]]+w[i]);}}for(int i=h[mid];i<=200;++i) f[mid][i]=w[mid];for(int i=mid-1;i>=l;--i){for(int j=0;j<h[i];++j) f[i][j]=f[i+1][j];for(int j=h[i];j<=200;++j){f[i][j]=max(f[i+1][j],f[i+1][j-h[i]]+w[i]);}}int tt=0;for(int i=tl;i<=tr;++i){int x=p[i];if(q[x].r<=mid) p[++tmid]=x;else if(q[x].l>mid) s[++tt]=x;else{int res=0;for(int j=0;j<=q[x].t;++j){res=max(res,f[q[x].l][j]+f[q[x].r][q[x].t-j]);}ans[x]=res;}}for(int i=1;i<=tt;++i) p[tmid+i]=s[i];tr=tmid+tt;sol(l,mid,tl,tmid);sol(mid+1,r,tmid+1,tr); 
}signed main() {ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);cin>>n>>m;for(int i=1;i<=n;++i){cin>>h[i];}for(int i=1;i<=n;++i){cin>>w[i];}for(int i=1;i<=m;++i){cin>>q[i].l>>q[i].r>>q[i].t;if(q[i].l==q[i].r){if(q[i].t>=h[q[i].l]) ans[i]=w[q[i].l];}else p[++tot]=i;}sol(1,n,1,tot);for(int i=1;i<=m;++i){cout<<ans[i]<<'\n';}
}

后记

感觉写博客也是件很累的事。
猫树分治那里一是没学太明白,而是累了写不动,过几天继续干。
累了,睡了。

http://www.jsqmd.com/news/1250445/

相关文章:

  • ATS: Adaptive Token Sampling for Efficient Vision Transformers 解读
  • 天道电视剧观后感2
  • Windows/macOS 通用 OpenClaw 2.7.9,本地存储 AI 智能体实操步骤
  • 2026北京亲子教育服务企业做GEO服务商怎么选?五家机构深度测评与靠谱选型指南 - 科技快讯
  • 北京汽车后市场服务企业做GEO服务商怎么选?2026本地靠谱选型指南与深度测评 - 科技快讯
  • 2026 图片水印怎么去掉免费 视频去水印在线工具免费盘点 - 免费软件工具方法教程
  • 白帽SEO服务商机构排名:白帽技术流派解析与2026白帽机构TOP推荐 - GEORANK
  • 招聘网站之外,留学生还能去哪找岗位?|蒸汽求职分享
  • 北京财税服务企业做GEO服务商怎么选?2026年深度测评与靠谱选型指南 - 科技快讯
  • 自动化专业毕业能干什么?制造业、控制、软件、数据怎么选
  • 2026在线去水印免费工具有哪些 用了三年的方法教程 - 免费软件工具方法教程
  • AI 时代,普通人如何用「提问」逼近天才的认知密度——从杨植麟本科演讲说起
  • 7.23小记
  • 前端该如何选择图片的格式?
  • 同城便民,大连跨省长途返乡救护车出租,卧床老人转运注意事项汇总 - 资讯快报
  • 【题解】P2791 幼儿园篮球题
  • iconfont是什么?有什么优缺点?
  • Recruiter初筛到底在判断什么?|蒸汽求职分享
  • 骨架屏库:内容加载前的占位图组件(245)
  • 从手工填报到实时决策:AI报表自动化实施路线图(含POC验证清单+ROI测算表)
  • 椰林海鲜码头企业新愿景是什么?:崇实永守初心 - 18102756859
  • HarmonyOS7 手势综合展示:六种手势类型一网打尽 + Grid 数据看板
  • vscode C++ qmake 配置问题
  • 什么是双向鉴权?从一个随机数说起
  • 青岛专业工程律师于臻:政府及国企拖欠工程款应如何依法处理 - 新思维商业观察
  • 白帽SEO服务商平台推荐:白帽技术趋势2026与合规优化机构盘点 - GEORANK
  • zynq高频小数据量PS闭环的三个实验-第3课:Linux驱动测试
  • 2026食品塑料容器行业格局调研|京津冀优质厂商综合实力分析分析,密封塑料桶/大批量注塑加工,食品塑料容器源头厂家推荐 - 品牌推荐师
  • 51单片机从零到实战(13)——毕设常用ADC模块
  • Python模块管理的底层逻辑:从路径查询到版本溯源