【学习笔记】cdq 分治
一、定义
cdq 分治是一种离线分治算法,一般有三种用途:
-
处理点对之间的问题
-
优化 1D/1D 动态规划
-
将动态问题转为静态问题
对于分治区间 \([l,r]\),确定一个中点 \(mid\),对于左右区间分别递归分治,然后再处理左右区间之间的贡献。啊显然归并排序就是 cdq 分治。
二、例题
1.【模板】树状数组 1
将以此询问拆成两个单点的前缀和,而修改操作就是单点修改。考虑对于一个修改 \((x,y)\) 和询问 \(p\),如果修改发生在询问前并且 \(x\le p\),那么修改就会对询问产生贡献。对于操作序列中的分治区间 \([l,r]\),我们按照类似归并排序的方式去遍历,那么左区间的修改操作就会影响到右区间的查询操作。排序后的序列是按操作的位置升序的,于是我们再记录一个 \(sum\) 表示当前增加了多少,对应地更新到答案中即可。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=5e5+5;
int n,m,ans[maxn];
struct node{
int typ,pos,val;
node(int typ=0,int pos=0,int val=0)
:typ(typ),pos(pos),val(val){}
il bool operator<(const node &x)const{
if(pos==x.pos){
return typ<x.typ;
}
else{
return pos<x.pos;
}
}
}a[maxn*3],b[maxn*3];
il void cdq(int l,int r){
if(l==r){
return ;
}
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
int p1=l,p2=mid+1,p3=l,sum=0;
for(int i=l;i<=r;i++){
if(p1<=mid&&a[p1]<a[p2]||p2>r){
if(a[p1].typ==1){
sum+=a[p1].val;
}
b[p3++]=a[p1++];
}
else{
if(a[p2].typ==2){
ans[a[p2].val]-=sum;
}
else if(a[p2].typ==3){
ans[a[p2].val]+=sum;
}
b[p3++]=a[p2++];
}
}
for(int i=l;i<=r;i++){
a[i]=b[i];
}
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
int cnt=0,tot=0;
for(int i=1,x;i<=n;i++){
cin>>x;
a[++cnt]=node(1,i,x);
}
while(m--){
int opt,x,y;
cin>>opt>>x>>y;
if(opt==1){
a[++cnt]=node(1,x,y);
}
else{
a[++cnt]=node(2,x-1,++tot);
a[++cnt]=node(3,y,tot);
}
}
cdq(1,cnt);
for(int i=1;i<=tot;i++){
cout<<ans[i]<<"\n";
}
return 0;
}
}
int main(){return asbt::main();}
2.[bzoj3262]陌上花开
三维偏序。第一维可以直接排序,第二维用 cdq 维护,第三维存入树状数组。这里不需要像归并排序那样写,只需要按照 \(b\) 排序后,对于右区间的每个位置去找左区间有哪些满足性质即可。注意去重。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=2e5+5;
int n,m,ans[maxn];
struct node{
int a,b,c,cnt,ans;
node(int a=0,int b=0,int c=0,int cnt=0,int ans=0)
:a(a),b(b),c(c),cnt(cnt),ans(ans){}
il bool operator<(const node &x)const{
if(a==x.a){
if(b==x.b){
return c<x.c;
}
return b<x.b;
}
return a<x.a;
}
}p[maxn],P[maxn];
il bool cmp(const node &x,const node &y){
if(x.b==y.b){
return x.c<y.c;
}
return x.b<y.b;
}
int tr[maxn];
il int lowbit(int x){
return x&-x;
}
il void add(int p,int v){
for(;p<=m;p+=lowbit(p)){
tr[p]+=v;
}
}
il int query(int p){
int res=0;
for(;p;p-=lowbit(p)){
res+=tr[p];
}
return res;
}
il void cdq(int l,int r){
if(l==r){
return ;
}
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
sort(P+l,P+mid+1,cmp);
sort(P+mid+1,P+r+1,cmp);
int pp=l;
for(int i=mid+1;i<=r;i++){
while(pp<=mid&&P[i].b>=P[pp].b){
add(P[pp].c,P[pp].cnt);
pp++;
}
P[i].ans+=query(P[i].c);
}
for(int i=l;i<pp;i++){
add(P[i].c,-P[i].cnt);
}
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
for(int i=1,a,b,c;i<=n;i++){
cin>>a>>b>>c;
p[i]=node(a,b,c);
}
sort(p+1,p+n+1);
int tot=0;
for(int i=1,num=0;i<=n;i++){
num++;
if(p[i].a!=p[i+1].a||p[i].b!=p[i+1].b||p[i].c!=p[i+1].c){
P[++tot]=node(p[i].a,p[i].b,p[i].c,num);
num=0;
}
}
cdq(1,tot);
for(int i=1;i<=tot;i++){
ans[P[i].cnt+P[i].ans-1]+=P[i].cnt;
}
for(int i=0;i<n;i++){
cout<<ans[i]<<"\n";
}
return 0;
}
}
int main(){return asbt::main();}
3.[bzoj1176][Balkan2007]Mokia
类似例 1,将查询差分后并入操作序列中。那么时间是一维,横竖各是一维,一共就有三维。那么就需要 cdq 套树状数组了。
Code
#include<bits/stdc++.h>
#define int long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1.6e5+5,maxm=2e6+5;
int n,m,ans[maxn];
struct node{
int typ,xp,yp,val;
node(int typ=0,int xp=0,int yp=0,int val=0)
:typ(typ),xp(xp),yp(yp),val(val){}
il bool operator<(const node &x)const{
if(xp==x.xp){
return typ<x.typ;
}
return xp<x.xp;
}
}a[maxn<<2],b[maxn<<2];
int tr[maxm];
il int lowbit(int x){
return x&-x;
}
il void add(int p,int v){
for(;p<=n;p+=lowbit(p)){
tr[p]+=v;
}
}
il int query(int p){
int res=0;
for(;p;p-=lowbit(p)){
res+=tr[p];
}
return res;
}
il void cdq(int l,int r){
if(l==r){
return ;
}
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
int p1=l,p2=mid+1,p3=l;
for(int i=l;i<=r;i++){
if(p1<=mid&&a[p1]<a[p2]||p2>r){
if(a[p1].typ==1){
add(a[p1].yp,a[p1].val);
}
b[p3++]=a[p1++];
}
else{
if(a[p2].typ==2){
ans[a[p2].val]+=query(a[p2].yp);
}
else if(a[p2].typ==3){
ans[a[p2].val]-=query(a[p2].yp);
}
b[p3++]=a[p2++];
}
}
for(int i=l;i<=mid;i++){
if(a[i].typ==1){
add(a[i].yp,-a[i].val);
}
}
// for(int i=0;i<=n;i++){
// cout<<query(i)<<" ";
// }
// puts("");
for(int i=l;i<=r;i++){
a[i]=b[i];
}
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>m>>n;
int opt,tot=0,cnt=0;
while(cin>>opt){
if(opt==1){
int x,y,v;
cin>>x>>y>>v;
a[++tot]=node(1,x,y,v);
}
else if(opt==2){
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
ans[++cnt]=m*(x2-x1+1)*(y2-y1+1);
a[++tot]=node(2,x1-1,y1-1,cnt);
a[++tot]=node(2,x2,y2,cnt);
a[++tot]=node(3,x1-1,y2,cnt);
a[++tot]=node(3,x2,y1-1,cnt);
}
}
cdq(1,tot);
for(int i=1;i<=cnt;i++){
cout<<ans[i]<<"\n";
}
return 0;
}
}
signed main(){return asbt::main();}
4.[Violet 3]天使玩偶
两点 \((x_i,y_i),(x_j,y_j)\) 间的距离为 \(|x_i-x_j|+|y_i-y_j|\),当 \(x_i\ge x_j\land y_i\ge y_j\) 时,就变成了 \((x_i+y_i)-(x_j+y_j)\)。于是我们可以对左上,左下,右上,右下各跑一遍 cdq 套树状数组。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e6+5,lim=1e6+1;
int n,m,tot,cnt,ans[maxn];
struct node{
int typ,xx,yy;
node(int typ=0,int xx=0,int yy=0)
:typ(typ),xx(xx),yy(yy){}
il bool operator<(const node &x)const{
if(xx==x.xx){
return typ<x.typ;
}
return xx<x.xx;
}
}hp[maxn],a[maxn],b[maxn];
struct{
int tr[maxn];
il int lowbit(int x){
return x&-x;
}
il void init(){
memset(tr,-0x3f,sizeof tr);
}
il void upd(int p,int x){
for(;p<=lim;p+=lowbit(p)){
tr[p]=max(tr[p],x);
}
}
il int query(int p){
int res=-1e9;
for(;p;p-=lowbit(p)){
res=max(res,tr[p]);
}
return res;
}
il void clear(int p){
for(;p<=lim;p+=lowbit(p)){
tr[p]=-1e9;
}
}
}F;
il void cdq(int l,int r){
if(l==r){
return ;
}
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
int p1=l,p2=mid+1,p3=l;
for(int i=l;i<=r;i++){
if(p1<=mid&&a[p1]<a[p2]||p2>r){
if(!a[p1].typ){
F.upd(a[p1].yy,a[p1].xx+a[p1].yy);
}
b[p3++]=a[p1++];
}
else{
if(a[p2].typ){
ans[a[p2].typ]=min(ans[a[p2].typ],a[p2].xx+a[p2].yy-F.query(a[p2].yy));
}
b[p3++]=a[p2++];
}
}
for(int i=l;i<=mid;i++){
if(!a[i].typ){
F.clear(a[i].yy);
}
}
for(int i=l;i<=r;i++){
a[i]=b[i];
}
}
il void work(bool flax,bool flay){
for(int i=1;i<=tot;i++){
a[i].typ=hp[i].typ;
if(flax){
a[i].xx=hp[i].xx;
}
else{
a[i].xx=lim-hp[i].xx+1;
}
if(flay){
a[i].yy=hp[i].yy;
}
else{
a[i].yy=lim-hp[i].yy+1;
}
}
cdq(1,tot);
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
for(int i=1,x,y;i<=n;i++){
cin>>x>>y;
hp[++tot]=node(0,x+1,y+1);
}
while(m--){
int opt,x,y;
cin>>opt>>x>>y;
if(opt==1){
hp[++tot]=node(0,x+1,y+1);
}
else{
hp[++tot]=node(++cnt,x+1,y+1);
}
}
memset(ans,0x3f,sizeof ans);
F.init();
work(0,0),work(0,1),work(1,0),work(1,1);
for(int i=1;i<=cnt;i++){
cout<<ans[i]<<"\n";
}
return 0;
}
}
int main(){return asbt::main();}
5.[Cqoi2011]动态逆序对
考虑对答案进行差分,求出每一次删除到下一次删除的逆序对减少数,再做后缀和。
对于两个元素 \(i\) 和 \(j\),如果它们的值为 \(val\),下标为 \(pos\),被删除的时间为 \(time\),那么如果满足 \((val_i<val_j\land pos_i>pos_j)\lor(val_i>val_j\land pos_i<pos_j)\),就可以产生一个逆序对。钦定 \(time_i<time_j\),这就对 \(time_i\) 的答案产生了 \(1\) 的贡献。这是一个三维偏序问题,cdq 即可。
值得注意的是 \(m\) 次操作不一定会把数组删完,而对于最后剩下的那部分元素在 cdq 里是不容易直接计算的(因为 \(time\) 相同)。所以对于剩下的那部分不妨直接用树状数组处理掉。
Code
#include<bits/stdc++.h>
#define int long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int n,m,ans[maxn];
struct node{
int tim,pos,val;
}a[maxn],b[maxn];
struct{
int n,tr[maxn];
il void init(){
memset(tr,0,sizeof tr);
}
il int lowbit(int x){
return x&-x;
}
il void add(int p,int v){
for(;p<=1e5;p+=lowbit(p)){
tr[p]+=v;
}
}
il int query(int p){
int res=0;
for(;p;p-=lowbit(p)){
res+=tr[p];
}
return res;
}
}F;
template<typename T>il void cdq(int l,int r,T cmp){
if(l==r){
return ;
}
int mid=(l+r)>>1;
cdq(l,mid,cmp),cdq(mid+1,r,cmp);
int p1=l,p2=mid+1,p3=l;
for(int i=l;i<=r;i++){
if(p1<=mid&&cmp(a[p1],a[p2])||p2>r){
F.add(a[p1].tim,1);
b[p3++]=a[p1++];
}
else{
ans[a[p2].tim]+=F.query(1e5)-F.query(a[p2].tim);
b[p3++]=a[p2++];
}
}
for(int i=l;i<=mid;i++){
F.add(a[i].tim,-1);
}
for(int i=l;i<=r;i++){
a[i]=b[i];
}
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
for(int i=1,x;i<=n;i++){
cin>>x;
a[x].val=x,a[x].pos=i;
}
for(int i=1,x;i<=m;i++){
cin>>x;
a[x].tim=i;
}
for(int i=1;i<=n;i++){
if(!a[i].tim){
a[i].tim=m+1;
ans[m+1]+=F.query(1e5)-F.query(a[i].pos);
F.add(a[i].pos,1);
}
}
F.init();
sort(a+1,a+n+1,[](const node &x,const node &y){return x.pos<y.pos;});
cdq(1,n,[](const node &x,const node &y){return x.val>y.val;});
sort(a+1,a+n+1,[](const node &x,const node &y){return x.pos>y.pos;});
cdq(1,n,[](const node &x,const node &y){return x.val<y.val;});
for(int i=m;i;i--){
ans[i]+=ans[i+1];
}
for(int i=1;i<=m;i++){
cout<<ans[i]<<"\n";
}
return 0;
}
}
signed main(){return asbt::main();}
6.[Tjoi2016&Heoi2016]序列
记每个位置 \(i\) 可能变成的值(不包括原数组)中最大的为 \(mx_i\),最小的为 \(mn_i\),设 \(dp_i\) 表示以 \(i\) 结尾的最长合法子序列的长度,那么有转移方程:
看起来是个四维偏序,不是很好优化(总不能 cdq 套树套树吧?)。考虑将 \(mx_i\) 和 \(mn_i\) 分别与 \(a_i\) 取最大、最小值,那么第二个条件就可以省掉了。也就是说只需满足三维偏序:
那么就可以用 cdq 优化了。每次都需要将自己和左右区间重新排序。注意由于 dp 的转移顺序是固定的,因此一定要先递归左区间,再处理左右区间之间的转移,再递归右区间。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int n,m,dp[maxn];
struct node{
int val,mx,mn,pos;
}a[maxn];
struct fenwick{
int tr[maxn];
fenwick(){
memset(tr,-0x3f,sizeof tr);
}
il int lowbit(int x){
return x&-x;
}
il void upd(int p,int v){
for(;p<=1e5;p+=lowbit(p)){
tr[p]=max(tr[p],v);
}
}
il void clear(int p){
for(;p<=1e5;p+=lowbit(p)){
tr[p]=-1e9;
}
}
il int query(int p){
int res=-1e9;
for(;p;p-=lowbit(p)){
res=max(res,tr[p]);
}
return res;
}
}F;
il void cdq(int l,int r){
sort(a+l,a+r+1,[](const node &x,const node &y){return x.pos<y.pos;});
if(l==r){
// cout<<a[l].pos<<" "<<dp[a[l].pos]<<"\n";
dp[a[l].pos]=max(dp[a[l].pos],1);
return ;
}
int mid=(l+r)>>1;
cdq(l,mid);
sort(a+l,a+mid+1,[](const node &x,const node &y){return x.mx<y.mx;});
sort(a+mid+1,a+r+1,[](const node &x,const node &y){return x.val<y.val;});
int p1=l,p2=mid+1;
for(int i=l;i<=r;i++){
if(p1<=mid&&a[p1].mx<=a[p2].val||p2>r){
F.upd(a[p1].val,dp[a[p1].pos]);
p1++;
}
else{
dp[a[p2].pos]=max(dp[a[p2].pos],F.query(a[p2].mn)+1);
p2++;
}
}
for(int i=l;i<=mid;i++){
F.clear(a[i].val);
}
cdq(mid+1,r);
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i].val;
a[i].mx=a[i].mn=a[i].val;
a[i].pos=i;
}
for(int i=1,p,v;i<=m;i++){
cin>>p>>v;
a[p].mx=max(a[p].mx,v);
a[p].mn=min(a[p].mn,v);
}
cdq(1,n);
int ans=0;
for(int i=1;i<=n;i++){
// cout<<dp[i]<<" ";
ans=max(ans,dp[i]);
}
// cout<<"\n";
cout<<ans;
return 0;
}
}
int main(){return asbt::main();}

浙公网安备 33010602011771号