CDQ分治
CDQ分治:用于解决离线或不强制在线问题中简化一层树结构的实用性分治算法
其实可以这样说,如果CDQ分治的题空间开大一点,基本树套树都可以搞定,但是树套树的空间复杂度是O(nlog~nlog^2n),而CDQ分治一般是O(n)级别的,因此,有些CDQ分治的题目并不能用树套树解决。并且,一般来说写一个CDQ分治都要比普通的树套树要容易的多,除了主席树+树状数组...
CDQ分治主要思想还是分治的思想,即递归处理小范围信息,之后将处理的信息合并上传。一般来说,都是先处理左区间,之后用左区间更新右区间,顺便更新答案,然后处理右区间,之后再将两个区间的信息合并。
通常来说,为了简化时间复杂度与常数,有几种方法可以参考
(1)在分治之前先按照某一关键字排序,之后在分治过程中,将信息按照时间分成前后两部分,这样避免了多次排序。
(2)在分治过程中,利用归并排序的方式将两个有序序列合并,将O(nlog)的排序变为O(n)的归并。
(3)在分治过程中,利用树状数组解决问题,除非必须用到别的东西。
(4)在分治过程中,利用有序的性质可以发现,逆序也是有序的,并且满足一些正好与正序相反,这样可以避免重复排序。
(5)在分治之前尽可能的简化不必要的信息,这样能减少整个代码的常数。
(6)另外,在更新右区间或者合并的时候,尽量选择常数与时间复杂度较小的算法,比如说能用单调队列就不要用斜率优化,能用斜率优化就不要用决策单调性。
其实本身CDQ分治的常数还是蛮小的,并不用太多的东西来优化,卡一卡rank1什么的可以优化一下...
注意事项:
分治结构一般的注意事项:不要用memset之类的东西!包括点分治在内的分治算法,时间复杂度的正确性关键在于快速处理出小部分的信息,而这么一个memset真的是自寻死路...(除非你能把握好sizeof里面的东西多大...)
注意出现某一信息相等的时候,记得将再判断时间的顺序,让时间小的放在前面。(其实没什么必要,但是能让你的代码好调一点)
注意一些比较恶心的边界条件...
扩充:
CDQ分治也是一种分治结构,可以和点分治结合(比如说:NOI2014购票)
CDQ分治可以套CDQ分治来解决四维偏序问题(当然,万一出题人毒瘤,出五维偏序怎么办?目测可以CDQ分治套CDQ分治套树套树)
例题时间:
BZOJ3262: 陌上花开
分析:比较裸的三维偏序,一维排序,一维分治,一维树状数组
具体实现是将先按x排序,之后按y分治,将z存入树状数组中,每次左区间修改,右区间查询即可。
附上代码:
#include <cstdio> #include <algorithm> #include <cmath> #include <cstring> #include <cstdlib> #include <queue> #include <iostream> using namespace std; #define N 100005 struct node { int x,y,z,cnt,num; friend bool operator!=(const node &a,const node &b) { return a.x!=b.x||a.y!=b.y||a.z!=b.z; } friend bool operator<(const node &a,const node &b) { return ((a.x==b.x)&(a.y==b.y)&(a.z<b.z))|((a.x==b.x)&(a.y<b.y))|(a.x<b.x); } }a[N],v[N],tmp[N]; int n,sum[N<<1],maxn,ans[N]; void fix(int x,int v){for(;x<=maxn;x+=x&-x)sum[x]+=v;} int find(int x){int ret=0;for(;x;x-=x&-x)ret+=sum[x];return ret;} void solve(int l,int r) { if(l==r)return ; int m=(l+r)>>1,i=l,j=m+1,p=l; solve(l,m);solve(m+1,r); while(i<=m&&j<=r) { if(v[i].y<=v[j].y)fix(v[i].z,v[i].cnt),tmp[p++]=v[i++]; else tmp[p]=v[j],tmp[p++].num+=find(v[j++].z); } while(i<=m)fix(v[i].z,v[i].cnt),tmp[p++]=v[i++]; while(j<=r)tmp[p]=v[j],tmp[p++].num+=find(v[j++].z); for(int i=l;i<=m;i++)fix(v[i].z,-v[i].cnt); for(int i=l;i<=r;i++)v[i]=tmp[i]; } int main() { scanf("%d%d",&n,&maxn); for(int i=1;i<=n;i++)scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z); sort(a+1,a+n+1);int tot=0;a[0].x=-1; for(int i=1;i<=n;i++){if(a[i]!=a[i-1])v[++tot]=a[i];v[tot].cnt++;} solve(1,tot); for(int i=1;i<=tot;i++)ans[v[i].num+v[i].cnt]+=v[i].cnt; for(int i=1;i<=n;i++)printf("%d ",ans[i]);return 0; }
BZOJ1492: [NOI2007]货币兑换Cash
分析:
暴力的DP方程:f[i]=max(f[j]+calc(i,j));另外,有一点很显然,要买就全买,要卖就全卖,贪心可证,不去证了...
先化简一下式子:
f[i]=f[j]/(a[j]*rat[j]+b[j])*(a[i]*rat[j]+b[i])
f[i]=a[i]*f[j]/(a[j]*rat[j]+b[j])*rat[j]+b[i]∗f[j]/(a[j]*rat[j]+b[j])
设g[j]=f[j]/(a[j]*rat[j]+b[j]);
f[i]=a[i]*rat[j]*g[j]+b[i]*g[j];
(f[i]/a[i])=rat[j]*g[j]+(b[i]/a[i])*g[j];
看起来可以斜率优化,但是完全不可做...因为完全没有单调性...
既然没有单调性做什么啊!(啪!不做题想死吗!)
好吧,既然没有单调性,那么就强行给他单调性!
所以其实单调性这种事情吧,就是想要有就可以有的,对不对?(对什么对!CDQ分治恶心死你)
那么其实本质上,我们需要将所有点按a[i]/b[i]排序,之后得到的序列满足a[i]/b[i]单调递增,每次再尽量不改变顺序的前提下,将区间分成两部分满足前半部分的时间都小于后半部分的时间,之后先递归处理左区间,再用左区间建立一个凸包(满足每个直线的斜率单调递增),之后用这个凸包更新右区间,再递归处理右区间,最后再按照g[i]的大小归并。
附上代码:
#include <cstdio> #include <algorithm> #include <cmath> #include <cstring> #include <cstdlib> #include <queue> #include <iostream> using namespace std; #define N 100005 #define K(x) (p[x].g) #define B(x) (p[x].g*p[x].rat) #define Y(x,y) (K(x)*p[y].b+B(x)*p[y].a) struct node { double a,b,f,g,k,rat,idx; friend bool operator<(const node &a,const node &b) { return a.k>b.k; } }p[N],pp[N]; int n,q[N],h,t; bool cmp(int i,int j,int k) { double t1=(B(i)-B(j))*(K(k)-K(i)); double t2=(B(k)-B(i))*(K(i)-K(j)); return t1<=t2; } void solve(int l,int r) { if(l==r) { p[l].f=max(p[l].f,p[l-1].f); p[l].g=p[l].f/(p[l].a*p[l].rat+p[l].b); return ; } int m=(l+r)>>1,h1=l,h2=m+1;double maxx=0; for(int i=l;i<=r;i++) { if(p[i].idx<=m)pp[h1++]=p[i]; else pp[h2++]=p[i]; } for(int i=l;i<=r;i++)p[i]=pp[i];solve(l,m); int h=1,t=0; for(int i=l;i<=m;i++) { while(h<t&&cmp(q[t],q[t-1],i))t--; q[++t]=i;maxx=max(maxx,p[i].f); } for(int i=m+1;i<=r;i++) { while(h<t&&Y(q[h],i)<=Y(q[h+1],i))h++; p[i].f=max(max(p[i].f,maxx),Y(q[h],i)); p[i].g=p[i].f/(p[i].a*p[i].rat+p[i].b); } solve(m+1,r);h1=l,h2=m+1; for(int i=l;i<=r;i++) { if(h1<=m&&(h2>r||K(h1)<K(h2)))pp[i]=p[h1++]; else pp[i]=p[h2++]; } for(int i=l;i<=r;i++)p[i]=pp[i]; } int main() { scanf("%d%lf",&n,&p[1].f); for(int i=1;i<=n;i++) { scanf("%lf%lf%lf",&p[i].a,&p[i].b,&p[i].rat); p[i].k=-p[i].b/p[i].a;p[i].idx=i; } sort(p+1,p+n+1); solve(1,n);double ans=0; for(int i=1;i<=n;i++)ans=max(ans,p[i].f); printf("%.3lf ",ans); }
BZOJ3295: [Cqoi2011]动态逆序对
当我第一次看到这道题的时候,“这不是主席树+树状数组裸题嘛!”之后树套树水过。
当我再次看到这道题的时候...“卧槽,这题还能用CDQ分治?!”之后CDQ分治水过。
类似陌上花开,删除就相当于加入,之后加入是逆序的
附上代码:
#include <cstdio> #include <cmath> #include <algorithm> #include <iostream> #include <queue> #include <cstdlib> #include <cstring> using namespace std; #define N 100005 struct node { int x,p,t,num; friend bool operator<(const node &a,const node &b) { if(a.t==b.t)return a.p<b.p; return a.t<b.t; } }p[N],pp[N]; int sum[N],n,Q,b[N];long long ans[N],tot; void fix(int x,int c){for(;x<=n;x+=x&-x)sum[x]+=c;} int find(int x){int ret=0;for(;x;x-=x&-x)ret+=sum[x];return ret;} void solve(int l,int r) { if(l==r)return ;int m=(l+r)>>1,h1=l,h2=m+1; /*for(int i=l;i<=r;i++){if(p[i].p<=m)pp[h1++]=p[i];else pp[h2++]=p[i];} for(int i=l;i<=r;i++)p[i]=pp[i];*/solve(l,m);solve(m+1,r);h1=l,h2=m+1; for(int i=l;i<=r;i++) { if(h1<=m&&(h2>r||p[h1].p<p[h2].p))pp[i]=p[h1],fix(p[h1++].x,1); else pp[i]=p[h2],ans[pp[i].t]+=find(n)-find(p[h2++].x); }for(int i=l;i<=m;i++)fix(p[i].x,-1);h1=m,h2=r; for(int i=l;i<=r;i++) { if(h1>=l&&(h2<m+1||p[h1].p>p[h2].p))fix(p[h1--].x,1); else ans[p[h2].t]+=find(p[h2].x-1),h2--; }for(int i=l;i<=m;i++)fix(p[i].x,-1);for(int i=l;i<=r;i++)p[i]=pp[i]; } int main() { scanf("%d%d",&n,&Q); for(int i=1;i<=n;i++)scanf("%d",&p[i].x),p[i].p=i,b[p[i].x]=i,p[i].t=1; for(int i=1,x;i<=Q;i++)scanf("%d",&x),p[b[x]].t=n-i+1;sort(p+1,p+n+1); solve(1,n);for(int i=1;i<=n;i++)tot+=ans[i]; for(int i=n;i>n-Q;i--)printf("%lld ",tot),tot-=ans[i]; //for(int i=1;i<=n;i++)printf("%lld ",ans[i]); }
树套树:
#include <cstdio> #include <cmath> #include <algorithm> #include <iostream> #include <queue> #include <cstdlib> #include <cstring> using namespace std; #define N 100005 struct node{int ls,rs,siz;}tr[N*100]; int nx,ny,rx[N],ry[N],cnt,Q,n,rot[N],a[N],p[N],q[N],vis[N];long long ans[N]; void insert(int v,int c,int l,int r,int &rt) { if(!rt)rt=++cnt;tr[rt].siz+=c;if(l==r)return ;int m=(l+r)>>1; if(m>=v)insert(v,c,l,m,tr[rt].ls);else insert(v,c,m+1,r,tr[rt].rs); } int query_back(int l,int r,int x) { if(l==r)return 0;int m=(l+r)>>1,sizls=0; for(int i=1;i<=nx;i++)sizls-=tr[tr[rx[i]].ls].siz; for(int i=1;i<=ny;i++)sizls+=tr[tr[ry[i]].ls].siz; if(m>=x) { for(int i=1;i<=nx;i++)rx[i]=tr[rx[i]].ls; for(int i=1;i<=ny;i++)ry[i]=tr[ry[i]].ls; return query_back(l,m,x); } for(int i=1;i<=nx;i++)rx[i]=tr[rx[i]].rs; for(int i=1;i<=ny;i++)ry[i]=tr[ry[i]].rs; return sizls+query_back(m+1,r,x); } int query_front(int l,int r,int x) { if(l==r)return 0;int m=(l+r)>>1,sizrs=0; for(int i=1;i<=nx;i++)sizrs-=tr[tr[rx[i]].rs].siz; for(int i=1;i<=ny;i++)sizrs+=tr[tr[ry[i]].rs].siz; if(m>=x) { for(int i=1;i<=nx;i++)rx[i]=tr[rx[i]].ls; for(int i=1;i<=ny;i++)ry[i]=tr[ry[i]].ls; return query_front(l,m,x)+sizrs; } for(int i=1;i<=nx;i++)rx[i]=tr[rx[i]].rs; for(int i=1;i<=ny;i++)ry[i]=tr[ry[i]].rs; return query_front(m+1,r,x); } void pre(int l,int r) { nx=ny=0; for(int i=l;i;i-=i&-i)rx[++nx]=rot[i]; for(int i=r;i;i-=i&-i)ry[++ny]=rot[i]; } int main() { scanf("%d%d",&n,&Q); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); p[a[i]]=i; } for(int i=1;i<=Q;i++) { scanf("%d",&q[i]); vis[q[i]]=1; } for(int i=n;i>=1;i--) { if(!vis[a[i]]) { pre(0,n);ans[Q+1]+=query_back(1,n,a[i]); for(int j=i;j<=n;j+=j&-j)insert(a[i],1,1,n,rot[j]); } } for(int i=Q;i;i--) { ans[i]=ans[i+1]; for(int j=p[q[i]];j<=n;j+=j&-j)insert(q[i],1,1,n,rot[j]); pre(p[q[i]],n);ans[i]+=query_back(1,n,q[i]); pre(0,p[q[i]]);ans[i]+=query_front(1,n,q[i]); } for(int i=1;i<=Q;i++) { printf("%lld ",ans[i]); } }
就是慢了点,没关系的...
BZOJ1176: [Balkan2007]Mokia&BZOJ2683: 简单题
说实话,这题写KD-Tree真的是好写...但是nsqrt(n)跑不过nloglog也是正常的对不对?
单点修改就直接相当于添加一个点,查询就相当于拆成四个。按照时间戳排序,按照x大小归并
附上代码:
#include <cstdio> #include <cmath> #include <algorithm> #include <iostream> #include <queue> #include <cstdlib> #include <cstring> using namespace std; #define N 200005 struct node { int x,y,op,c,idx; }p[N],pp[N]; int n,Q,maxn,num;long long sum[N*10],ans[N]; void fix(int x,int c){for(;x<=maxn;x+=x&-x)sum[x]+=c;} int find(int x){int ret=0;for(;x;x-=x&-x)ret+=sum[x];return ret;} void solve(int l,int r) { if(l==r)return ;int m=(l+r)>>1,h1=l,h2=m+1; solve(l,m);solve(m+1,r);h1=l,h2=m+1; for(int i=l;i<=r;i++) { if(h1<=m&&(h2>r||p[h1].x<=p[h2].x)){if(!p[h1].op)fix(p[h1].y,p[h1].c);pp[i]=p[h1++];} else{if(p[h2].op)ans[p[h2].idx]+=p[h2].c*find(p[h2].y);pp[i]=p[h2++];} } for(int i=l;i<=m;i++)if(!p[i].op)fix(p[i].y,-p[i].c);for(int i=l;i<=r;i++)p[i]=pp[i]; } int main() { scanf("%*d%d",&maxn); while(1) { int op,x,y,a,b; scanf("%d",&op);//%d%d%d",&op,&x,&y,&a); if(op==3)break; if(op==1) { scanf("%d%d%d",&x,&y,&a); p[++n]=(node){x,y,0,a,0}; }else { num++; scanf("%d%d%d%d",&x,&y,&a,&b);x--,y--; p[++n]=(node){a,b,1,1,num}; p[++n]=(node){a,y,1,-1,num}; p[++n]=(node){x,b,1,-1,num}; p[++n]=(node){x,y,1,1,num}; } } solve(1,n); for(int i=1;i<=num;i++)printf("%lld ",ans[i]); }
BZOJ2225: [Spoj 2371]Another Longest Increasing
分析:二维LIS,CDQ分治正常操作,按照第一维排序,每次取出左右区间,之后按照第一维归并。第二维树状数组即可。
附上代码:
#include <cstdio> #include <algorithm> #include <cmath> #include <cstring> #include <cstdlib> #include <queue> #include <iostream> using namespace std; #define N 200005 struct node { int a,b,idx; }p[N],pp[N]; int n,maxx[N],f[N],t[N]; void fix(int x,int c){for(;x<N;x+=x&-x)maxx[x]=max(maxx[x],c);} void clear(int x){for(;x<N;x+=x&-x)maxx[x]=0;} int find(int x){int ret=0;for(;x;x-=x&-x)ret=max(maxx[x],ret);return ret;} void solve(int l,int r) { if(l==r)return; int m=(l+r)>>1,h1=l,h2=m+1; for(int i=l;i<=r;i++) { if(p[i].idx<=m)pp[h1++]=p[i]; else pp[h2++]=p[i]; }for(int i=l;i<=r;i++)p[i]=pp[i]; solve(l,m);h1=l,h2=m+1; for(int i=m+1,j=l;i<=r;i++) { while(p[j].a<p[i].a&&j<=m)fix(p[j].b,f[p[j].idx]),j++; f[p[i].idx]=max(f[p[i].idx],find(p[i].b-1)+1); }for(int i=l;i<=m;i++)clear(p[i].b); solve(m+1,r);h1=l,h2=m+1; for(int i=l;i<=r;i++) { if(h1<=m&&(h2>r||p[h1].a<p[h2].a))pp[i]=p[h1++]; else pp[i]=p[h2++]; }for(int i=l;i<=r;i++)p[i]=pp[i]; } bool cmp(const node &a,const node &b){return a.a<b.a;} int main() { scanf("%d",&n);f[1]=1; for(int i=1;i<=n;i++) { scanf("%d%d",&p[i].a,&p[i].b);p[i].idx=i;t[i]=p[i].b; }sort(t+1,t+n+1); for(int i=1;i<=n;i++)p[i].b=lower_bound(t+1,t+n+1,p[i].b)-t; sort(p+1,p+n+1,cmp);solve(1,n);int ans=0; for(int i=1;i<=n;i++)ans=max(ans,f[i]);//printf("%d ",f[i]); printf("%d ",ans); }
BZOJ2716: [Violet 3]天使玩偶
CDQ分治可以做,别问我,反正CDQ分治还跑不过KD-Tree呢!
BZOJ3672: [Noi2014]购票
直接上链接就可以了...我写过这篇题解!https://www.cnblogs.com/Winniechen/p/9247939.html
那么CDQ分治就到这里了...