Dyd's Blog

He who has a strong enough why can bear almost any how.

CDQ分治

cdq,又一个大佬

CDQ分治

CDQ分治是一种运用广泛的分治思想,主要用于解决偏序问题,尤其是三维偏序,其主要思想是将区间分成两段,段内的信息递归处理,跨段的信息在左右两段都处理好后处理。

先看模板题吧:

三维偏序

这道题就是一个三维偏序的裸题,不过,在三维前,我们先想想一、二维偏序怎么做。

  • 一维偏序
    非常简单,排序即可,时间复杂度为 $O(n\log{n})$
  • 二维偏序
    先双关键字排序,完成后就可以不管第一关键字了(只需保证 $i>j$ 就可以使 $a_i \ge a_j$),而对于第二关键字,将其离散化到值域为 $[1,n]$ 建立一棵线段树来维护值 $x$ 的数量,则 $ask(1,1,b_i)$ 就储存了小于 $b_i$ 的值的个数,更新完 $i$ 的答案后让 $b_i$ 这个点 $+1$ 然后继续向后扫描即可。时间复杂度为 $O(n\log{n})$

如上,我们可以在 $O(n\log{n})$ 的时间内求解一、二维偏序,那么,我们现在来康康三维。

首先,类似一、二维偏序,三维偏序的第一维可以通过排序解决,第三维在前两维有序(即 $\forall i>j,a_i \ge a_j,b_i \ge b_j$ )的情况下可以用线段树解决,问题在于:如何在保证前两维都有序?

很明显,在对第一维排序后(三关键字排序),若再更改顺序来满足第二维,第一维的单调会被破坏,而CDQ分治解决了这个问题,其过程如下:

  1. 对于区间 $[l,r]$ (此时第一维仍然单调),把区间中满足三维偏序的数对 $(i,j)(i>j)$ 以 $mid$ 为界分为三种: $i,j \in [l,mid]$ 、 $i,j \in [mid+1,r]$ 、 $i \in [mid+1,r],j \in [l,mid]$
  2. 对于前两种( $i,j$ 在同一段的)递归处理
  3. 处理完毕后,只剩下 $i,j$ 分别在 $mid$ 左右的情况,此时,由于 $mid$ 左边的数第一维一定小于右边,我们可以对左右分别以第二维排序,然后双指针枚举 $i,j$ 用线段树求解第三维

以上就是CDQ分治的主要思路,不难发现,由于每次取 $mid$ ,递归层数不会超过 $\log{n}$ 层,每一层中对左右的分别排序可以用归并在递归中顺带完成,真正消耗时间的是双指针和线段树,第 $x$ 层中 $[1,n]$ 被分为了 $2^{x-1}$ 段,每段时间复杂度为 $O(2^{x-1} \times \frac{n}{2^{x-1}} \times \log{ \frac{n}{2^{x-1}}}) = O(n\log{n})$ ,乘上层数,总时间复杂度为 $O(n \log^2_{}{n})$

一个要注意的问题是,当出现三维都相等的数时,会导致排在前面的数少算,故应先去重。

代码实现(注意代码中的 $m$ 是题目中的 $k$ ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=2e5+5;
int n,m;
int ans[N];
struct Node{
int a,b,c,num,res; //a,b,c:三维,num:重复的个数,res:三维都比它小的数的个数
bool operator < (const Node &x) const{ //重载三关键字排序
if(a!=x.a) return a<x.a;
if(b!=x.b) return b<x.b;
return c<x.c;
}
bool operator == (const Node &x) const{ //重载等于用于判重
return a==x.a&&b==x.b&&c==x.c;
}
}q[N],w[N]; //w用于归并排序
struct LineTree{
int l,r,dat;
}tr[M*4];
/*--------------------------LineTree---------------------------*/
void push_up(int u){
tr[u].dat=tr[u<<1].dat+tr[u<<1|1].dat;
}
void build(int u,int l,int r){
tr[u].l=l;
tr[u].r=r;
tr[u].dat=0;
if(l==r) return ;
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
}
void change(int u,int pos,int d){
if(tr[u].l==tr[u].r){
tr[u].dat+=d;
return ;
}
int mid=tr[u].l+tr[u].r>>1;
if(pos<=mid) change(u<<1,pos,d);
else change(u<<1|1,pos,d);
push_up(u);
}
int ask(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].dat;
int res=0;
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) res+=ask(u<<1,l,r);
if(r>mid) res+=ask(u<<1|1,l,r);
return res;
}
/*----------------------------CDQ------------------------------*/
void merge_sort(int l,int r){
if(l>=r) return ;
int mid=l+r>>1;
merge_sort(l,mid);
merge_sort(mid+1,r);
int i=l,j=mid+1,k=1;
while(i<=mid&&j<=r)
if(q[i].b<=q[j].b) change(1,q[i].c,q[i].num),w[k++]=q[i++];
else q[j].res+=ask(1,1,q[j].c),w[k++]=q[j++];
while(i<=mid) change(1,q[i].c,q[i].num),w[k++]=q[i++];
while(j<=r) q[j].res+=ask(1,1,q[j].c),w[k++]=q[j++];
for(i=l;i<=mid;++i) change(1,q[i].c,-q[i].num); //还原线段树以备下次使用
for(i=l,j=1;j<k;++i,++j) q[i]=w[j];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i) scanf("%d%d%d",&q[i].a,&q[i].b,&q[i].c),q[i].num=1;
sort(q+1,q+1+n);
int k=2;
for(int i=2;i<=n;++i)
if(q[i]==q[k-1]) ++q[k-1].num;
else q[k++]=q[i];
build(1,1,m); //这里m足够小不必离散化
merge_sort(1,k-1);
for(int i=1;i<k;++i)
ans[q[i].num+q[i].res-1]+=q[i].num; //f(i)=q[i].num+q[i].res-1(去掉自己)
for(int i=0;i<n;++i) printf("%d\n",ans[i]);
return 0;
}

应用

老C的任务

提示:考虑二维前缀和,对于每个询问 $(x1,y1,x2,y2),ans=s(x2,y2)-s(x2,y1-1)-s(x1-1,y2)+s(x1-1,y1-1)$ ,不妨以坐标作为第一、二维,是否是询问为第三维(是为1,不是为0),把询问拆成四个点(注意保留符号),和不是询问的点一起,对于每个点,求其三维偏序(求出的点就是会对其二维前缀和有贡献的点),最后计算即可,本题第三维只有0、1两中情况,故可以省略线段树,直接用一个 $sum$ 记录为0的数的权值和。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m;
struct Node{
int x,y,z,p,i,sign;
long long sum;
bool operator < (const Node &t) const{
if(x!=t.x) return x<t.x;
if(y!=t.y) return y<t.y;
return z<t.z;
}
void inint(int _x,int _y,int _z,int _p,int _i,int _s){
x=_x;
y=_y;
z=_z;
p=_p;
i=_i;
sign=_s;
sum=0;
}
}q[N*5],w[N*5];
long long ans[N];
void merge_sort(int l,int r){
if(l>=r) return ;
int mid=l+r>>1;
merge_sort(l,mid);
merge_sort(mid+1,r);
int i=l,j=mid+1,k=1;
long long sum=0;
while(i<=mid&&j<=r)
if(q[i].y<=q[j].y) sum+=!q[i].z*q[i].p,w[k++]=q[i++];
else q[j].sum+=sum,w[k++]=q[j++];
while(i<=mid) sum+=!q[i].z*q[i].p,w[k++]=q[i++];
while(j<=r) q[j].sum+=sum,w[k++]=q[j++];
for(i=l,j=1;j<k;++i,++j) q[i]=w[j];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i) scanf("%d%d%d",&q[i].x,&q[i].y,&q[i].p),q[i].z=0;
int k=n+1;
for(int i=1;i<=m;++i){
int x_1,y_1,x_2,y_2;
scanf("%d%d%d%d",&x_1,&y_1,&x_2,&y_2);
q[k++].inint(x_2,y_2,1,0,i,1);
q[k++].inint(x_1-1,y_2,1,0,i,-1);
q[k++].inint(x_2,y_1-1,1,0,i,-1);
q[k++].inint(x_1-1,y_1-1,1,0,i,1);
}
sort(q+1,q+k);
merge_sort(1,k-1);
for(int i=1;i<k;++i)
if(q[i].z) ans[q[i].i]+=q[i].sum*q[i].sign;
for(int i=1;i<=m;++i) printf("%lld\n",ans[i]);
return 0;
}

动态逆序对

提示:以时间为第三维,对于每个删除,求出该删除会导致此时仍存在的数列减少多少个逆序对即可。注意第三维越大越早删除,注意逆序应分类讨论

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e5+5;
int n,m;
struct Node{
int a,t,res; //位置关系已经是有序且互不相等的,不必记录和排序
}q[N],w[N];
struct LineTree{
int l,r,dat;
}tr[N*4];
int pos[N];
LL ans[N];
/*---------------------------------LineTree--------------------------------------*/
void push_up(int u){
tr[u].dat=tr[u<<1].dat+tr[u<<1|1].dat;
}
void build(int u,int l,int r){
tr[u].l=l;
tr[u].r=r;
tr[u].dat=0;
if(l==r) return ;
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
}
void change(int u,int pos,int d){
if(tr[u].l==tr[u].r){
tr[u].dat+=d;
return ;
}
int mid=tr[u].l+tr[u].r>>1;
if(pos<=mid) change(u<<1,pos,d);
if(pos > mid) change(u<<1|1,pos,d);
push_up(u);
}
int ask(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].dat;
int mid=tr[u].l+tr[u].r>>1,res=0;
if(l<=mid) res+=ask(u<<1,l,r);
if(r>mid) res+=ask(u<<1|1,l,r);
return res;
}
/*----------------------------------CDQ---------------------------------------*/
void merge_sort(int l,int r){
if(l>=r) return ;
int mid=l+r>>1;
merge_sort(l,mid);
merge_sort(mid+1,r);
int i=mid,j=r,k;
//j的位置在i后时
while(i>=l&&j>mid)
if(q[i].a>q[j].a) change(1,q[i].t,1),--i;
else q[j].res+=ask(1,1,q[j].t-1),--j;
while(j>mid) q[j].res+=ask(1,1,q[j].t-1),--j;
for(k=i+1;k<=mid;++k) change(1,q[k].t,-1);
//j的位置在i前时
j=l,i=mid+1;
while(j<=mid&&i<=r)
if(q[i].a<q[j].a) change(1,q[i].t,1),++i;
else q[j].res+=ask(1,1,q[j].t-1),++j;
while(j<=mid) q[j].res+=ask(1,1,q[j].t-1),++j;
for(k=i-1;k>mid;--k) change(1,q[k].t,-1);
//归并排序可以用sort代替
i=l,j=mid+1;
k=1;
while(i<=mid&&j<=r)
if(q[i].a<=q[j].a) w[k++]=q[i++];
else w[k++]=q[j++];
while(i<=mid) w[k++]=q[i++];
while(j<=r) w[k++]=q[j++];
for(i=l,j=1;j<k;++i,++j) q[i]=w[j];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
scanf("%d",&q[i].a);
pos[q[i].a]=i;
}
for(int i=1,j=n;i<=m;++i){
int a;
scanf("%d",&a);
q[pos[a]].t=j--;
pos[a]=-1;
}
for(int i=1,j=n-m;i<=n;++i){
if(pos[i]!=-1) q[pos[i]].t=j--;
}
build(1,1,n);
merge_sort(1,n);
for(int i=1;i<=n;++i) ans[q[i].t]=q[i].res;
for(int i=2;i<=n;++i) ans[i]+=ans[i-1];
for(int i=1,j=n;i<=m;++i,--j) printf("%lld\n",ans[j]);
return 0;
}