CDQ分治与整体二分学习笔记
CDQ分治部分
CDQ分治是用分治的方法解决一系列类似偏序问题的分治方法,一般可以用KD-tree、树套树或权值线段树代替。
三维偏序,是一种类似LIS的东西,但是LIS的关键字只有两个,数组下标和权值,三维偏序问题的权值有两个,且必须A[I]<A[J]且B[I]<B[j]。
把这个问题放到平面上,就是一个点在另一个点的左下方。
那么如何求?
CDQ分治的主要过程是二分整个区间,把左区间看成产生贡献的区间,于是我们在左区间进行操作,在右区间统计答案,用归并排序的方法求解。
对于这道题,我们二分整个区间,对A做归并排序,归并时对B维护一个树状数组,在左区间更改,右区间查询就可以了。
例题1
陌上花开(bzoj3262)裸的三维偏序
我们按照a为第一关键字,b为第二关键字,c为第三关键字排序,其实相当于把a去掉了,换成了数组下标,因为本质相同的元素答案是相同的,所以手动去重后直接CDQ分治求解。
Code
#include<iostream> #include<cstdio> #include<algorithm> #define low (x&(-x)) #define N 200009 #define M 100009 using namespace std; typedef long long ll; int k,n,ans2[M],be[M]; ll f[M],tr[N],ans[M]; struct zzh { int a,b,c,id,size; bool operator < (const zzh &u)const { if(b!=u.b)return b<u.b; if(c!=u.c)return c<u.c; return id<u.id; } }ji[M],a[M],t[M]; bool cmp(zzh a,zzh b) { if(a.a!=b.a)return a.a<b.a; if(a.b!=b.b)return a.b<b.b; if(a.c!=b.c)return a.c<b.c; return a.id<b.id; } void mem(int x){while(x<=k)tr[x]=0,x+=low;} void gai(int x,int y){while(x<=k)tr[x]+=y,x+=low;} int query(int x) { int ans=0; while(x){ans+=tr[x];x-=low;} 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 p=l,q=mid+1,o=l-1; while(p<=mid&&q<=r) { if(t[p]<t[q]) gai(t[p].c,t[p].size),ji[++o]=t[p++]; else ans[t[q].id]+=query(t[q].c),ji[++o]=t[q++]; } while(p<=mid)ji[++o]=t[p++]; while(q<=r)ans[t[q].id]+=query(t[q].c),ji[++o]=t[q++]; for(int i=l;i<=r;++i) mem(ji[i].c),t[i]=ji[i]; } bool pd(int x,int now){ if(!now)return 0; return ((a[x].a==a[now].a)&&(a[x].b==a[now].b)&&(a[x].c==a[now].c)); } int main() { // freopen("233.out","w",stdout); scanf("%d%d",&n,&k); for(int i=1;i<=n;++i) scanf("%d%d%d",&a[i].a,&a[i].b,&a[i].c),a[i].id=i,a[i].size=1; sort(a+1,a+n+1,cmp);int now=0; for(int i=1;i<=n;++i) { if(!pd(i,i-1)) t[++now]=a[i],t[now].id=now; else t[now].size++; be[i]=now; } cdq(1,now); for(int i=1;i<=now;++i)ans[t[i].id]+=t[i].size-1; for(int i=1;i<=n;++i)f[i]=ans[be[i]]; for(int i=1;i<=n;++i)ans2[f[i]]++; for(int i=0;i<n;++i)printf("%d\n",ans2[i]); return 0; }
bzoj4553 [Tjoi2016&Heoi2016]序列
也是一个类LIS问题,只不过条件有些特殊。
观察题目可知,i和j都在LIS中需满足a[i]<=a[j]&&a[i]<=l[j]&&r[i]<=a[j],其中l[i]为a[i]的最小值,r[i]为其最大值,第一个条件可省略,再加上下标,正好是三个。
我们先CDQ左区间,再把左区间按照a排序,右区间按照l排序,归并时将左区间的r加入树状数组,右区间用a查询,做完后在CDQ右区间就行了。
f数组记得取max。
Code
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 100009 #define shi 100000 #define lowbit(x) (x&(-x)) using namespace std; int tr[N],n,m,f[N]; inline int rd() { int x=0; char c=getchar(); while(!isdigit(c))c=getchar(); while(isdigit(c)) { x=(x<<1)+(x<<3)+(c^48); c=getchar(); } return x; } struct zzh { int l,r,a,id; }ji[N]; void add(int x,int y){while(x<=shi){tr[x]=max(tr[x],y);x+=lowbit(x);}} void clear(int x){while(x<=shi){tr[x]=0;x+=lowbit(x);}} int query(int x){ int ans=0; while(x){ ans=max(tr[x],ans); x-=lowbit(x); } return ans; } bool cmp1(zzh a,zzh b){return a.a<b.a;} bool cmp2(zzh a,zzh b){return a.l<b.l;} bool cmp3(zzh a,zzh b){return a.id<b.id;} void cdq(int l,int r) { if(l==r)return; int mid=(l+r)>>1; cdq(l,mid); sort(ji+l,ji+mid+1,cmp1);sort(ji+mid+1,ji+r+1,cmp2); int p=l,q=mid+1,o=l-1; while(p<=mid&&q<=r) { if(ji[p].a<=ji[q].l)add(ji[p].r,f[ji[p].id]),p++; else f[ji[q].id]=max(query(ji[q].a)+1,f[ji[q].id]),q++; } while(q<=r)f[ji[q].id]=max(query(ji[q].a)+1,f[ji[q].id]),q++; for(int i=l;i<=mid;++i)clear(ji[i].r); sort(ji+l,ji+r+1,cmp3); cdq(mid+1,r); } int main() { n=rd();m=rd(); for(int i=1;i<=n;++i) ji[i].l=ji[i].r=ji[i].a=rd(),ji[i].id=i; int x,y; for(int j=1;j<=m;++j) { x=rd();y=rd(); ji[x].l=min(ji[x].l,y); ji[x].r=max(ji[x].r,y); } for(int i=1;i<=n;++i)f[i]=1; cdq(1,n);int ans=0; for(int i=1;i<=n;++i)ans=max(ans,f[i]); cout<<ans; return 0; }
整体二分部分
整体二分本质和CDQ差不多,都是基于分治的算法,实现起来比较简单(只要能够深入理解)。
例题(就写过一道)
[ZJOI2013]K大数查询
有N个位置,M个操作。操作有两种,每次操作如果是1 a b c的形式表示在第a个位置到第b个位置,每个位置加入一个数c如果是2 a b c形式,表示询问从第a个位置到第b个位置,第C大的数是多少。
这题什么神奇分块树套树的都能过,整体二分也是一个很经典的解法。
我们现在有一个操作和询问混合的序列,我们需要通过一些操作把它分成两个序列,在这两个序列在时间维度上是单调递增的(在原序列中的相对位置不变),但答案的值域分别小了一半,虽然序列并没有严格二分,但答案值域减小了一半,保证了我们分治的复杂度。
考虑我们有一个序列,里面有在l-r内每个位置插入一个c,还有询问一个区间的第K大,目前的值域为-inf---inf.
我们按照时间的顺序把这个序列扫一遍,遇到操作大于等于mid就把这个l---r+1,当我们遇到一个询问时,我们就查l---r有多少数,实际就是查l---r有多少数是大于等于mid,如果已经够K个了,说明我的第K大一定是>=mid的(显然),所以我们把它放到右序列,否则,它就该在左边,同时我们要把K减去我们查询的结果,意思就是说我们已经数了这么多个了。
同理,对于每个操作,如果>=mid就放到右序列,否则就放到左序列。
然后我们完成了一项使命,把答案值域缩小了一半,接下来继续分治就好了。
边界l==r,此时我的所有询问的答案都是这个值。
时间复杂度,分治nlogn,加上线段树维护操作一个log,总复杂度nlog2n。
Code
#include<iostream> #include<cstdio> #define N 50009 using namespace std; long long tr[N<<2]; int la[N<<2],n,m,ans[N]; struct ef { int l,r,tag,id; long long v; }a[N],q1[N],q2[N]; inline void pushdown(int cnt,int l1,int l2) { tr[cnt<<1]+=la[cnt]*l1;tr[cnt<<1|1]+=la[cnt]*l2; la[cnt<<1]+=la[cnt];la[cnt<<1|1]+=la[cnt];la[cnt]=0; } long long query(int cnt,int l,int r,int L,int R) { if(l>=L&&r<=R)return tr[cnt]; int mid=(l+r)>>1; if(la[cnt])pushdown(cnt,mid-l+1,r-mid); long long ans=0; if(mid>=L)ans+=query(cnt<<1,l,mid,L,R); if(mid<R)ans+=query(cnt<<1|1,mid+1,r,L,R); return ans; } void add(int cnt,int l,int r,int L,int R,int tag) { if(l>=L&&r<=R) { tr[cnt]+=(r-l+1)*tag; la[cnt]+=tag; return; } int mid=(l+r)>>1; if(la[cnt])pushdown(cnt,mid-l+1,r-mid); if(mid>=L)add(cnt<<1,l,mid,L,R,tag); if(mid<R)add(cnt<<1|1,mid+1,r,L,R,tag); tr[cnt]=tr[cnt<<1]+tr[cnt<<1|1]; } void solve(int st,int en,int l,int r) { if(l==r) { for(int i=st;i<=en;++i) if(a[i].tag==2)ans[a[i].id]=l; return; } int mid=(l+r)>>1,ll=0,rr=0; for(int i=st;i<=en;++i) if(a[i].tag==1) { if(a[i].v<=mid)q1[++ll]=a[i]; else { add(1,1,n,a[i].l,a[i].r,1); q2[++rr]=a[i]; } } else { long long val=query(1,1,n,a[i].l,a[i].r); if(val>=a[i].v) { q2[++rr]=a[i]; } else a[i].v-=val,q1[++ll]=a[i]; } for(int i=st;i<=en;++i) if(a[i].tag==1&&a[i].v>mid)add(1,1,n,a[i].l,a[i].r,-1); for(int i=1;i<=ll;++i)a[st+i-1]=q1[i]; for(int i=1;i<=rr;++i)a[st+ll+i-1]=q2[i]; solve(st,st+ll-1,l,mid);solve(st+ll,en,mid+1,r); } int main() { scanf("%d%d",&n,&m);int tot=0; for(int i=1;i<=m;++i) { scanf("%d%d%d%lld",&a[i].tag,&a[i].l,&a[i].r,&a[i].v); if(a[i].tag==2)a[i].id=++tot; } solve(1,m,-n,n); for(int i=1;i<=tot;++i)printf("%d\n",ans[i]); return 0; }
Dynamic Rankings
给定一个含有n个数的序列a[1],a[2],a[3]……a[n],程序必须回答这样的询问:对于给定的i,j,k,在a[i],a[i+1],a[i+2]……a[j]中第k小的数是多少(1≤k≤j-i+1),并且,你可以改变一些a[i]的值,改变后,程序还能针对改变后的a继续回答上面的问题。你需要编一个这样的程序,从输入文件中读入序列a,然后读入一系列的指令,包括询问指令和修改指令。
对于每一个询问指令,你必须输出正确的回答。
Solution
区间待修第k大,经典整体二分题。
Code
#include<iostream> #include<cstdio> #include<algorithm> #define N 100002 using namespace std; int tr[N<<1],ji[N<<1],top,tot,x,y,ans[N],n,m,aa,num[N]; char c; inline void add(int x,int y){while(x<=top)tr[x]+=y,x+=x&-x;} inline int query(int x){int ans=0;while(x)ans+=tr[x],x-=x&-x;return ans;} struct sd{ int x,y,tag,l,r,k; }q[N*3],a[N*3],b[N*3]; void solve(int l,int r,int L,int R){ if(L==R) { for(int i=l;i<=r;++i) if(q[i].l)ans[q[i].tag]=L; return; } int mid=(L+R)>>1,l1=0,l2=0; for(int i=l;i<=r;++i){ if(q[i].x!=0){ if(q[i].y<=mid)add(q[i].x,q[i].tag),a[++l1]=q[i]; else b[++l2]=q[i]; } else{ int tmp=query(q[i].r)-query(q[i].l-1); if(tmp>=q[i].k)a[++l1]=q[i]; else q[i].k-=tmp,b[++l2]=q[i]; } } for(int i=1;i<=l1;++i)if(a[i].x)add(a[i].x,-a[i].tag); for(int i=l;i<=l+l1-1;++i)q[i]=a[i-l+1]; for(int i=l+l1;i<=r;++i)q[i]=b[i-l-l1+1]; solve(l,l+l1-1,L,mid);solve(l+l1,r,mid+1,R); } char ge(){ char c=getchar(); while(c!='Q'&&c!='C')c=getchar(); return c; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;++i){ scanf("%d",&x);ji[++top]=x; q[++tot].x=i;q[tot].y=x;q[tot].tag=1; num[i]=x; } for(int i=1;i<=m;++i){ c=ge(); if(c=='Q'){ ++tot; scanf("%d%d%d",&q[tot].l,&q[tot].r,&q[tot].k); q[tot].tag=++aa; } else{ scanf("%d%d",&x,&y); q[++tot].x=x;q[tot].y=num[x];q[tot].tag=-1; q[++tot].x=x;q[tot].y=y;q[tot].tag=1; num[x]=y;//care!!!!! ji[++top]=y; } } sort(ji+1,ji+top+1); top=unique(ji+1,ji+top+1)-ji-1; for(int i=1;i<=tot;++i) if(q[i].y)q[i].y=lower_bound(ji+1,ji+top+1,q[i].y)-ji; solve(1,tot,1,top); for(int i=1;i<=aa;++i)printf("%d\n",ji[ans[i]]); return 0; }