[cdq分治]
dalao们会发现这一篇没有带“专题”二字。哈哈哈那是因为本蒟蒻实在没有力气打专题了。。
还是太弱了。。我要逆袭!
嗯cdq分治的主要思想大概就是归并排序的基础思想——递归分治分治分治分治。。。
也就是说,我们早就学会了最基础的cdq分治——归并排序。
归并排序的思想无非就是分治,将无序的序列通过cdq分治的思想变为有序,归并求逆序对也就是这样一个思想。
cdq的主思想是分治,不过很多题目都可以套用cdq来搞事情(五花八门)。
比如三维偏序这个模板题——
有n个元素,一个元素有3个阈值,ai,bi,ci,求ai<=aj,bi<=bj,ci<=cj的对数(i!=j)。
显然,我们可以直接排序去除第一维的影响。
假设将a排好序了,那就剩下了b,c的影响。
那么,我们可以像归并排序那样将b排成有序的,那c呢?我们在排b的时候就要将关于b,c的逆序对算出来。这就是cdq分治。
那我们来关注一下怎么统计出逆序对。
就像归并一样,我们在cdq时有两个指针(u->L..M,v->M+1..R)。
如果当前y[u]<=y[v],说明是左边的那一块有一个元素加入到了新队列里面,我们通过树状数组bit来标记一下。
如果当前y[u]>y[v],说明是右边的那一块有一个元素加入到了新队列里面,我们通过树状数组进行统计有多少比它小的(相当于求出顺序对)。
code(luoguP3810):
1 %:pragma GCC optimize(2) 2 #include<bits/stdc++.h> 3 using namespace std; 4 const int N=100005,K=200005,inf=0x3f3f3f3f; 5 struct ele { 6 int x,y,z,t,s; 7 bool operator < (const ele &c) const { 8 return x==c.x?(y==c.y?z<c.z:y<c.y):x<c.x; 9 } 10 }a[N]; 11 int n,m,k,ans[N],cnt[K]; 12 inline int read() { 13 int x=0,f=1; char ch=getchar(); 14 while (ch<'0'||ch>'9') 15 f=(ch=='-')?-f:f,ch=getchar(); 16 while (ch>='0'&&ch<='9') 17 x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); 18 return x*f; 19 } 20 ele tmp[N]; 21 void U(int x,int v) {for (int i=x; i<=k; i+=i&(-i)) cnt[i]+=v;} 22 int Q(int x) {int re=0; for (int i=x; i; i-=i&(-i)) re+=cnt[i]; return re;} 23 void cdq(int L,int R) { 24 if (L==R) return; int M=(L+R)>>1; 25 cdq(L,M),cdq(M+1,R); 26 int u=L,v=M+1; 27 for (int i=L; i<=R; i++) 28 if (v>R||(a[u].y<=a[v].y&&u<=M)) 29 U(a[u].z,a[u].t),tmp[i]=a[u++]; 30 else a[v].s+=Q(a[v].z),tmp[i]=a[v++]; 31 for (int i=L; i<=M; i++) U(a[i].z,-a[i].t); 32 for (int i=L; i<=R; i++) a[i]=tmp[i]; 33 } 34 int main() { 35 n=read(),k=read(),m=0,a[0].x=a[0].y=a[0].z=-inf; 36 for (int i=1; i<=n; i++) 37 a[i].x=read(),a[i].y=read(),a[i].z=read(),a[i].t=1; 38 sort(a+1,a+1+n); 39 for (int i=1; i<=n; i++) 40 if (a[i].x==a[i-1].x&& 41 a[i].y==a[i-1].y&& 42 a[i].z==a[i-1].z) a[m].t++; 43 else a[++m]=a[i]; 44 cdq(1,m); 45 for (int i=1; i<=m; i++) ans[a[i].t+a[i].s-1]+=a[i].t; 46 for (int i=0; i<n; i++) printf("%d\n",ans[i]); 47 return 0; 48 }
cdq的作用可不仅限于此——它还可以用来刚某些修改+询问的问题。
比如这么一个问题:
已知一个数列,你需要进行下面两种操作:
1.将某一个数加上x
2.求出某区间每一个数的和
这是个典型的树状数组模板题。
但是如何用cdq做呢?
我们将操作类型看做一维,然后将位置看做一维,将修改的值看做一维。
也就是说,若是操作1,则第一维是1,第二维是修改的位置,第三维是修改值。
若是操作2,我们可以将它拆成2个操作。
一个操作,第一维是2,第二位维是查询的左端点-1,第三维是第几个操作(方便记录答案)。
另一个操作,第一维是3,第二位维是查询的右端点,第三维是第几个操作。
然后满足一个原则:左边的修改操作可以影响右边的查询操作。
code(luoguP3374):
1 %:pragma GCC optimize(2) 2 #include<bits/stdc++.h> 3 using namespace std; 4 const int N=2000005; 5 struct query { 6 int w,idx,ty; 7 bool operator < (const query &c) const { 8 return idx==c.idx?ty<c.ty:idx<c.idx; 9 } 10 }q[N]; 11 int n,m,Q,cask,ans[N]; 12 inline int read() { 13 int x=0,f=1; char ch=getchar(); 14 while (ch<'0'||ch>'9') 15 f=(ch=='-')?-f:f,ch=getchar(); 16 while (ch>='0'&&ch<='9') 17 x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); 18 return x*f; 19 } 20 query tmp[N]; 21 void cdq(int L,int R) { 22 if (L>=R) return; int M=(L+R)>>1; 23 cdq(L,M),cdq(M+1,R); 24 int sum=0,u=L,v=M+1,c=0; 25 for (; u<=M&&v<=R; ) { 26 if (q[u]<q[v]) { 27 if (q[u].ty==1) sum+=q[u].w; 28 tmp[++c]=q[u++]; 29 }else { 30 if (q[v].ty==2) ans[q[v].w]-=sum; else 31 if (q[v].ty==3) ans[q[v].w]+=sum; 32 tmp[++c]=q[v++]; 33 } 34 } 35 while (u<=M) tmp[++c]=q[u++]; 36 while (v<=R) { 37 if (q[v].ty==2) ans[q[v].w]-=sum; else 38 if (q[v].ty==3) ans[q[v].w]+=sum; 39 tmp[++c]=q[v++]; 40 } 41 for (int i=1; i<=c; i++) q[i+L-1]=tmp[i]; 42 } 43 int main() { 44 n=read(),m=read(),Q=cask=0; 45 for (int i=1; i<=n; i++) 46 Q++,q[Q].idx=i,q[Q].ty=1,q[Q].w=read(); 47 for (int i=1; i<=m; i++) { 48 Q++,q[Q].ty=read(); 49 if (q[Q].ty==1) q[Q].idx=read(),q[Q].w=read(); 50 else { 51 q[Q].idx=read()-1,q[Q].w=++cask,Q++; 52 q[Q].idx=read(),q[Q].ty=3,q[Q].w=cask; 53 } 54 } 55 cdq(1,Q); 56 for (int i=1; i<=cask; i++) printf("%d\n",ans[i]); 57 return 0; 58 }
再来一个稍微难一点的——动态逆序对
对于序列A,它的逆序对数定义为满足i<j,且Ai>Aj的数对(i,j)的个数。给1到n的一个排列,按照某种顺序依次删除m个元素,你的任务是在每次删除一个元素之前统计整个序列的逆序对数。
那么,我们怎么转化为偏序问题呢?我们肯定要将动态转为静态。怎么转移?
给每个数设置一个标记z,并规定:
被删除的数标号一定大于未被删除的数;
被删除早的数的标号一定大于被删除晚的数;
未被删除的数按照原顺序排序。
那么,我们按照这一维度排序,就能保证右边的编号总是大于左边的,也就是说,右边的修改是影响不到左边的,而左边的依然可以影响到右边的,这也符合实际。
然后就剩下两维(一维位置,一维数值)比较正常的了,用树状数组搞一下就行了。
但是要注意的是,这两种情况都是符合条件的:
ti<tj&&xi<xj&&yi>yj
ti<tj&&xi>xj&&yi<yj
所以要用两个数组分别维护。
code(luoguP3157):
1 %:pragma GCC optimize(2333) 2 #include<bits/stdc++.h> 3 #define LL long long 4 using namespace std; 5 const int N=100005,K=200005; 6 int n,m,tot,k,rel[N]; 7 LL cnt[N],ans[N]; 8 struct info { 9 int x,y,z; 10 bool operator < (const info &k) const { 11 return x==k.x?(y==k.y?z<k.z:y<k.y):x<k.x; 12 } 13 }a[N],b[N]; 14 inline int read() { 15 int x=0,f=1; char ch=getchar(); 16 while (ch<'0'||ch>'9') 17 f=(ch=='-')?-f:f,ch=getchar(); 18 while (ch>='0'&&ch<='9') 19 x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); 20 return x*f; 21 } 22 info tmp[N]; LL ret; int u,v; 23 void U(int x,LL v) {for (int i=x; i<=n; i+=i&(-i)) cnt[i]+=v;} 24 LL A(int x) {ret=0; for (int i=x; i; i-=i&(-i)) ret+=cnt[i]; return ret;} 25 void cdq(int L,int R) { 26 if (L==R) return; int M=(L+R)>>1; 27 cdq(L,M),cdq(M+1,R); 28 u=L,v=M+1; 29 for (int i=L; i<=R; i++) 30 if (v>R||(a[u].y>a[v].y&&u<=M)) U(a[u].z,1),tmp[i]=a[u++]; 31 else ans[a[v].x]+=A(a[v].z-1),tmp[i]=a[v++]; 32 for (int i=L; i<=M; i++) U(a[i].z,-1); 33 for (int i=L; i<=R; i++) a[i]=tmp[i]; 34 u=L,v=M+1; 35 for (int i=L; i<=R; i++) 36 if (v>R||(b[u].y<b[v].y&&u<=M)) U(b[u].z,1),tmp[i]=b[u++]; 37 else ans[b[v].x]+=A(n)-A(b[v].z),tmp[i]=b[v++]; 38 for (int i=L; i<=M; i++) U(b[i].z,-1); 39 for (int i=L; i<=R; i++) b[i]=tmp[i]; 40 } 41 int main() { 42 n=read(),m=read(),tot=0; 43 for (int i=1; i<=n; i++) a[i].x=-1,a[i].y=i,rel[a[i].z=read()]=i; 44 for (int i=1; i<=m; i++) a[rel[read()]].x=n-i+1; 45 for (int i=1; i<=n; i++) if (a[i].x==-1) a[i].x=++tot; 46 sort(a+1,a+1+n),memcpy(b,a,sizeof b); 47 cdq(1,n); 48 for (int i=1; i<=n; i++) ans[i]+=ans[i-1]; 49 for (int i=n; i>=n-m+1; i--) printf("%lld\n",ans[i]); 50 return 0; 51 }
又学会了一个新算法,~\(≧▽≦)/~