cdq分治

[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 }
View Code

 

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 }
View Code

 

再来一个稍微难一点的——动态逆序对

对于序列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 }
View Code

 

又学会了一个新算法,~\(≧▽≦)/~

 

转载于:https://www.cnblogs.com/whc200305/p/7553792.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值