学习博客:
https://www.cnblogs.com/LMCC1108/category/1444281.html 来自未来“图灵奖”获得者潘武灵
https://www.cnblogs.com/mlystdcall/p/6219421.html 来自一看就知道是大佬的__stdcall
拙见:首先,在此膜一发算法发明者---陈丹琦小姐姐,%%%%%%%%。然后,开始口胡拙见。分治,顾名思义分而治之。普通的分治就是先把区间分为左右两个区间(分),分别处理后(治),再将两个区间合并,再去处理更大的区间。普通的分治要求左右区间没有影响,如果左右区间有影响,怎么办?这就是CDQ分治能解决的啦。CDQ分治总是考虑左边区间对右边区间的影响,也就是每次计算左边区间对右边区间的贡献。其实,本质上解决的是多维偏序问题,只会二维、三维。
通常情况下,做题套路如下:
1、首先我们把问题转化为多维偏序问题(即含有若干元素的有序元组的大小问题)。
2、然后,将第一维排序(多数问题默认第一维为时间,这样我们就可以不排序。)。
3、接着,将第二维归并排序的同时,将左边区间的贡献加给右边区间。
4、最后,将排好序的区间保存并重新赋值给原数组,以便操作更大的区间。
注意点:
1、一定要注意只算左边区间对右边区间的影响
2、处理完区间后,一定要删除之前统计的影响。
3、注意先更新,后查询之类的优先级问题。
缺点:只能离线查询,出题人要想强制在线,CDQ分治就没法搞了。
例题1、OpenJ_Bailian - 2299 Ultra-QuickSort
思路:经典的求逆序对个数问题。用树状数组再简单不过。现在用CDQ分治做。按套路来:
1、首先,(pos,val)表示为以pos为下标的数的值为val。现有左区间(pos1,val1)、右区间(pos2,val2),问题转化为求pos1<pos2,val1>val2的逆序对有多少。
2、按第一维pos从小到大排序,题目给的顺序便是,不用排。
3、将第二维val进行归并排序,现在考虑左边区间对右边区间的影响。毫无疑问,整个区间的数已按pos从小到大排序,又因为已经对左边区间内部,右边区间内部的数按val从小到大排序。假设当前处理的区间为[l,r],当左边区间的某个val(假设下标为ll)大于右边区间的某个val(rr),那么对右边的影响也就是逆序对数增加m-ll+1。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e6+10;
int a[N],n,b[N],ll,rr;
long long ans;
void solve(int l,int r)
{
if(l==r) return ;
int m=(l+r)>>1;
solve(l,m),solve(m+1,r);
ll=l,rr=m+1;
int pos=l;
while(ll<=m&&rr<=r)
if(a[ll]<=a[rr])
b[pos++]=a[ll++];
else
b[pos++]=a[rr++],ans+=(m-ll+1);
while(ll<=m)
b[pos++]=a[ll++];
while(rr<=r)
b[pos++]=a[rr++];
for(int i=l;i<=r;i++)
a[i]=b[i];
return ;
}
int main(void)
{
while(~scanf("%d",&n)&&n)
{
ans=0;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
solve(1,n);
printf("%lld\n",ans);
}
return 0;
}
例题2、HDU - 1166 敌兵布阵
题意:n个营地,初始时都有若干兵。有3中操作,第一种往某个营地加x个兵,第二种从某个营地减少x个兵,第三种,询问当前x到y营地总共有多少个兵。
思路:线段树做起来so easy。考虑CDQ分治,首先,查询可以拆为两个前缀和查询,之后便是套路。CDQ套路:
1、首先,(pos,op,val)表示为op种类的操作。
op==1:将pos上的值加上val。
op==2:表示第val个查询要减去pos的前缀和。
op==3:表示第val个查询要加上pos的前缀和。
现有左区间(pos1,op1,val1)、右区间(pos2,op2,val2),问题转化为当pos1<pos2,op1==1,op2>=2时,求pos1的前缀和的二维偏序问题。
2、将第一维时间从小到大排序,输入时默认有序,不用排。
3、将第二维pos进行归并排序,因为是前缀和,肯定是考虑左边区间对右边区间的影响,也就是前面的前缀和对后面前缀和的影响。现在考虑左边区间对右边区间的影响。归并排序的时候,如果左边区间的pos小于右边区间的pos,那么就更新左边区间前缀和;否则就更新右边区间的查询。这里注意如果位置相等,先更新,后查询。
PS:至于初始时有的兵,将他们转化为op为1的操作。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e6+10;
const int M = 1e6+10;
struct node
{
int op,pos;
ll val;
node(){}
node(int op,int pos,ll val):op(op),pos(pos),val(val){}
friend bool operator <(node a,node b)
{
if(a.pos==b.pos) return a.op<b.op;
else return a.pos<b.pos;
}
}q[N],b[N];
int n,id,num;
ll ans[N],x,y;
char s[10];
void cdq(int l,int r)
{
if(l==r)
return ;
int m;ll sum=0;
m=(l+r)>>1;
cdq(l,m),cdq(m+1,r);
int L=l,R=m+1,i=l;
while(L<=m&&R<=r)
{
if(q[L]<q[R])
{
if(q[L].op==1)
sum+=q[L].val;
b[i++]=q[L++];
}
else
{
if(q[R].op==2)
ans[q[R].val]-=sum;
else if(q[R].op==3)
ans[q[R].val]+=sum;
b[i++]=q[R++];
}
}
while(L<=m) b[i++]=q[L++];
while(R<=r)
{
if(q[R].op==2)
ans[q[R].val]-=sum;
else if(q[R].op==3)
ans[q[R].val]+=sum;
b[i++]=q[R++];
}
for(int i=l;i<=r;i++)
q[i]=b[i];
}
int main(void)
{
int t,tt=0;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
id=n;
for(int i=1;i<=n;i++)
scanf("%lld",&q[i].val),q[i].op=1,q[i].pos=i,ans[i]=0;
num=0;
while(~scanf("%s",s+1)&&s[1]!='E')
{
scanf("%lld%lld",&x,&y);
if(s[1]=='Q')
{
++num;
q[++id]=node(2,x-1,num);
q[++id]=node(3,y,num);
}
else if(s[1]=='A')
q[++id]=node(1,x,y);
else
q[++id]=node(1,x,-y);
}
for(int i=1;i<=num;i++)
ans[i]=0;
cdq(1,id);
printf("Case %d:\n",++tt);
for(int i=1;i<=num;i++)
printf("%lld\n",ans[i]);
}
return 0;
}
例3、HYSBZ - 1935 Tree 园丁的烦恼
题意:二维平面上,有n棵树。m个询问,每个询问给出平面上一个矩形的左下角和右上角,问这个矩形内有多少棵树?
思路:首先,很容易就能想到二维前缀和,假设给定矩形为(x1,y1)、(x2,y2),那么根据容斥原理答案显然为
mp[x2][y2]-mp[x1-1][y2]-mp[x2][y1-1]+mp[x1-1][y1-1],我们cdq分治的时候将x归并排序,由于是前缀和始终考虑较小的x对较大的x的影响,然后用树状数组维护y的前缀和,合起来就好了。接下来我们按套路走:
1、首先,(x,y,op,val)表示为op种类的操作。
op==1:将(x,y)上的值加上1。
op==2:表示第val个查询要减去/加上(x,y)的前缀和(这里加或减用f表示)。
现有左区间(x1,y1,op1,val1)、右区间(x2,y2,op2,val2),问题转化为当x1<x2,op1==1,op2==2时的二维前缀和的二维偏序问题。
2、按第一维时间从小到大排序,输入时默认有序,不用排。
3、将第二维进行归并排序,因为是前缀和,肯定是考虑左边区间对右边区间的影响,也就是前面的前缀和对后面前缀和的影响。现在考虑左边区间对右边区间的影响。归并排序的时候,如果左边区间的x小于右边区间的x,那么就更新包含左边区间y的前缀和;否则就更新右边区间的查询。这里注意如果位置相等,先更新,后查询。
PS:注意每次处理完一个区间后,要清空树状数组。
#include <cstdio>
#include <iostream>
#define ll long long
using namespace std;
const int N = 5e5+10;
const int M = 3e6+10;
const int maxn = 1e7+10;
struct node
{
int x,y,op,val,f;
node(){}
node(int x,int y,int op,int val,int f):x(x),y(y),op(op),val(val),f(f){}
friend bool operator <(const node& a,const node& b)
{
if(a.x==b.x) return a.op<b.op;
else return a.x<b.x;
}
}q[M],b[M];
int sum[maxn],ans[N];
int n,m,maxy;
int lowbit(int x){ return (x&(-x)); }
void add(int x)
{
while(x<=maxy)
{
sum[x]++;
x+=lowbit(x);
}
}
void sub(int x)
{
while(x<=maxy)
{
sum[x]=0;
x+=lowbit(x);
}
}
int getsum(int x)
{
int res=0;
while(x)
{
res+=sum[x];
x-=lowbit(x);
}
//一定要返回!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
return res;
}
void cdq(int l,int r)
{
if(l==r) return ;
int m=(l+r)>>1;
cdq(l,m),cdq(m+1,r);
int L=l,R=m+1,i=l;
while(L<=m&&R<=r)
{
if(q[L]<q[R])
{
if(q[L].op==1)
add(q[L].y);
b[i++]=q[L++];
}
else
{
if(q[R].op==2)
ans[q[R].val]+=q[R].f*getsum(q[R].y);
b[i++]=q[R++];
}
}
while(L<=m) b[i++]=q[L++];
while(R<=r)
{
if(q[R].op==2)
ans[q[R].val]+=q[R].f*getsum(q[R].y);
b[i++]=q[R++];
}
for(i=l;i<=r;i++)
{
if(b[i].op==1)
sub(b[i].y);
q[i]=b[i];
}
}
int main(void)
{
scanf("%d%d",&n,&m);
int x1,y1,x2,y2,id;
maxy=0;
for(int i=1;i<=n;i++)
{
scanf("%d%d",&x1,&y1);
x1++,y1++;
q[i]=node(x1,y1,1,1,0);
if(y1>maxy) maxy=y1;
}
id=n;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
x1++,x2++,y1++,y2++;
q[++id]=node(x2,y2,2,i,1);
q[++id]=node(x2,y1-1,2,i,-1);
q[++id]=node(x1-1,y2,2,i,-1);
q[++id]=node(x1-1,y1-1,2,i,1);
if(y1>maxy) maxy=y1;
if(y2>maxy) maxy=y2;
}
cdq(1,id);
for(int i=1;i<=m;i++)
printf("%d\n",ans[i]);
return 0;
}
例4、2019南京网络赛 A. The beautiful values of the palace
题意:和上一个题几乎一模一样,就是矩阵上的值求起来麻烦些。
思路:请参考上一题。
PS:不用long long会错,我的实现有问题。
#include <bits/stdc++.h>
#define ll long long
#define int long long
using namespace std;
const int N = 1e6+10;
struct node
{
int x,y,op,val,f;
node(){}
node(int x,int y,int op,int val,int f):x(x),y(y),op(op),val(val),f(f){}
friend bool operator <(const node& a,const node& b)
{
if(a.x==b.x) return a.op<b.op;
else return a.x<b.x;
}
}q[N],b[N];
int n,m,p,maxy,id;
ll ans[N],sum[N];
ll getval(int x,int y)
{
int xx=x,yy=y;
ll ans=0,res=0;
ll layer=min(min(x,y),n-max(x,y)+1);
if((layer<<1)>n) ans=1LL*n*n;
else
{
ll st=n*n-(n-(layer<<1)+2)*(n-(layer<<1)+2)+1;
if (x>=y) ans=st+((n-layer)<<1)+2-x-y;
else ans=st+((n-(layer<<1)+1)<<1)+x+y-(layer<<1);
}
while(ans)
{
res+=(ans%10);
ans/=10;
}
return res;
}
int lowbit(int x){ return (x&(-x)); }
void add(int x,int val)
{
while(x<=maxy)
{
sum[x]+=val;
x+=lowbit(x);
}
}
void sub(int x)
{
while(x<=maxy)
{
sum[x]=0;
x+=lowbit(x);
}
}
ll getsum(int x)
{
int xx=x;
ll res=0;
while(x)
{
res+=sum[x];
x-=lowbit(x);
}
//一定要返回!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
return res;
}
void cdq(int l,int r)
{
if(l==r) return ;
int m=(l+r)>>1;
cdq(l,m),cdq(m+1,r);
int L=l,R=m+1,i=l;
while(L<=m&&R<=r)
{
if(q[L]<q[R])
{
if(q[L].op==1)
add(q[L].y,q[L].val);
b[i++]=q[L++];
}
else
{
if(q[R].op==2)
ans[q[R].val]+=getsum(q[R].y)*q[R].f;
b[i++]=q[R++];
}
}
while(L<=m) b[i++]=q[L++];
while(R<=r)
{
if(q[R].op==2)
ans[q[R].val]+=getsum(q[R].y)*q[R].f;
b[i++]=q[R++];
}
for(i=l;i<=r;i++)
{
if(b[i].op==1)
sub(b[i].y);
q[i]=b[i];
}
}
signed main(void)
{
int t,x1,y1,x2,y2;
scanf("%lld",&t);
while(t--)
{
scanf("%lld%lld%lld",&n,&m,&p);
id=m,maxy=0;
for(int i=1;i<=m;i++)
{
scanf("%lld%lld",&x1,&y1);
q[i]=node(x1,y1,1,getval(x1,y1),1);
if(y1>maxy) maxy=y1;
}
for(int i=1;i<=p;i++)
{
ans[i]=0;
scanf("%lld%lld%lld%lld",&x1,&y1,&x2,&y2);
q[++id]=node(x2,y2,2,i,1);
q[++id]=node(x1-1,y2,2,i,-1);
q[++id]=node(x2,y1-1,2,i,-1);
q[++id]=node(x1-1,y1-1,2,i,1);
if(y1>maxy) maxy=y1;
if(y2>maxy) maxy=y2;
}
cdq(1,id);
for(int i=1;i<=p;i++)
printf("%lld\n",ans[i]);
}
return 0;
}
例5、HYSBZ - 3262 陌上花开
题意:有n朵花,每朵花有3种属性(s,c,m),给出属性的最大值k。现在给n朵花划分等级,一朵花的等级为所有花中s>=s1,c>=c1,m>=m1的个数。这很明显的三维偏序问题,要注意的是,s、c、m都相等的花,等级相同,那么我们要去重把们归为一类,剩下的就按套路走。
1、首先,(s,c,m,id,val)表示为属性为(s,c,m)的花为低id种,有val个。现有左区间(s1,c1,m1,id1,val1)、右区间(s2,c2,m2,id2,val2),问题转化为当s1<s2,c1<c2时,求m1前缀和的二维偏序问题。
2、按第一维s从小到大排序。
3、将第二维c进行归并排序,因为是前缀和,肯定是考虑左边区间对右边区间的影响,也就是前面的前缀和对后面前缀和的影响。现在考虑左边区间对右边区间的影响。归并排序的时候,如果左边区间的c小于等于右边区间的c,那么就更新包含左边区间m的前缀和;否则就更新右边区间的查询。
PS:注意每次处理完一个区间后,要清空树状数组。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5+10;
struct node
{
int s,c,m,id,val;
}a[N],b[N];
int n,k;
int le[N],ans[N],sum[N<<1];
bool cmp(const node& a,const node& b)
{
if(a.s==b.s)
{
if(a.c==b.c)
return a.m<b.m;
else return a.c<b.c;
}
return a.s<b.s;
}
int lowbit(int x){ return (x&(-x));}
void add(int x,int val)
{
while(x<=k)
{
sum[x]+=val;
x+=lowbit(x);
}
}
void sub(int x)
{
while(x<=k)
{
sum[x]=0;
x+=lowbit(x);
}
}
int getsum(int x)
{
int res=0;
while(x)
{
res+=sum[x];
x-=lowbit(x);
}
//记得返回!!!!!!!!!!!!!!!!!!!!!!
return res;
}
void cdq(int l,int r)
{
if(l==r)
{
le[a[l].id]+=a[l].val-1;
return ;
}
int m=(l+r)>>1;
cdq(l,m),cdq(m+1,r);
int L=l,R=m+1,i=l;
while(L<=m&&R<=r)
{
if(a[L].c<=a[R].c)
{
add(a[L].m,a[L].val);
b[i++]=a[L++];
}
else
{
le[a[R].id]+=getsum(a[R].m);
b[i++]=a[R++];
}
}
while(L<=m) b[i++]=a[L++];
while(R<=r)
{
le[a[R].id]+=getsum(a[R].m);
b[i++]=a[R++];
}
for(i=l;i<=r;i++)
{
sub(a[i].m);
a[i]=b[i];
}
}
bool equal(node a,node b){ return a.s==b.s&&a.c==b.c&&a.m==b.m; }
int main(void)
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d%d%d",&a[i].s,&a[i].c,&a[i].m),a[i].val=1;
sort(a+1,a+1+n,cmp);
int id=1;
a[1].id=1;
for(int i=2;i<=n;i++)
if(equal(a[i],a[i-1]))
a[id].val++;
else
a[++id]=a[i],a[id].id=id;
cdq(1,id);
for(int i=1;i<=id;i++)
ans[le[a[i].id]]+=a[i].val;
for(int i=0;i<n;i++)
printf("%d\n",ans[i]);
return 0;
}
总结:大体上,我觉得我总结的套路够用,再难的恐怕也没有能力去解决了。注意树状数组一定要返回,错了好几次了。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。