CDQ分治简介(洛谷P3810、BZOJ3262)

292 篇文章 1 订阅
281 篇文章 1 订阅

%%%陈丹琦巨佬

算法用途

当碰到一些动态的题目时,常常需要用到高级数据结构来维护,代码又臭又长。而在某些情况下,CDQ分治可以代替这些高级数据结构,转动态为静态来处理,使代码复杂度大大降低。

算法实现

基本步骤

看到这个名称,就知道肯定是用分治的思想来解决了。

普通的分治中,左半个区间是不影响右半个区间的,做完当前区间直接递归。而CDQ分治一般会经过以下三个步骤:
1.将当前区间分成两半(废话)

2.考虑并计算左区间的所有操作对右区间的影响。

3.递归求解。

经典问题

CDQ分治中最经典的问题就是三维偏序问题:

给出许多个有三个属性 (x,y,z) ( x , y , z ) 的元素,对于每个元素,求≤它的元素个数。( ab:a a ≤ b : a 的三个属性值分别小于 b b

当只有两个属性时,把两个属性按照第一、二关键字从小到大排序,其实就是逆序对问题,可以用树状数组求解。

而有三个属性时,如果仍然用数据结构维护,同样把三个属性按照第一、二、三关键字排序,用树状数组维护第二个关键字,平衡树维护第三个关键字。即树状数组套平衡树。代码又臭又长。

我们考虑CDQ分治。把每个点看成一个数,所有点看成一个序列。先按三个关键字排完序后,对于一个分治的区间[l,r],再以 y y 为第一关键字,编号为第二关键字进行排序。这时y不产生影响,直接用树状数组维护 z z <script id="MathJax-Element-7" type="math/tex">z</script>即可。把左区间的影响加上并计算答案,再撤销左区间的影响。最后递归求解。

模板

洛谷P3810BZOJ3262)为例:

#include<cctype>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 100005
#define F inline
using namespace std;
struct px{
    int x,y,z,id;
    bool operator == (const px &a) const{
        return a.x==x&&a.y==y&&a.z==z;
    }
}q[N],tem[N];
int n,k,t[N<<1],ans[N<<1],ti[N<<1];
F char readc(){
    static char buf[100000],*l=buf,*r=buf;
    if (l==r) r=(l=buf)+fread(buf,1,100000,stdin);
    if (l==r) return EOF; return *l++;
}
F int _read(){
    int x=0,f=1; char ch=readc();
    while (!isdigit(ch)) { if (ch=='-') f=-1; ch=readc(); }
    while (isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48),ch=readc();
    return x*f;
}
F void writec(int x){ if (x>9) writec(x/10); putchar(x%10+48); }
F void _write(int x){ writec(x),putchar('\n'); }
//以上为IO优化
#define lbt(x) (x&(-x))
F void nsrt(int x,int p){ while (x<=k) t[x]+=p,x+=lbt(x); }
F int srch(int l,int r){
    int x,sum=0; l--;
    x=l; while (x) sum-=t[x],x-=lbt(x);
    x=r; while (x) sum+=t[x],x-=lbt(x);
    return sum;
}
//以上为树状数组单点修改,区间查询
F bool cmp1(px a,px b){ return a.y<b.y||(a.y==b.y&&a.id<b.id); }
F bool cmp2(px a,px b){
    bool x=(a.x==b.x),y=(a.y==b.y),z=(a.z==b.z);
    return a.x<b.x||(x&a.y<b.y)||(x&y&a.z<b.z)||(x&y&z&a.id<b.id);
}
F void cdq(int l,int r){
    if (l==r) return; int mid=l+r>>1;
    for (int i=l;i<=mid;i++) tem[i]=q[i],tem[i].id=0;
    //把左区间的id赋成0,这样就可以保证左区间的影响
    for (int i=mid+1;i<=r;i++) tem[i]=q[i];
    sort(tem+l,tem+r+1,cmp1);
    for (int i=l;i<=r;i++)
        if (!tem[i].id) nsrt(tem[i].z,1);//加上影响
        else ans[tem[i].id]+=srch(1,tem[i].z);//计算
    for (int i=l;i<=r;i++) if (!tem[i].id) nsrt(tem[i].z,-1);//消去影响
    cdq(l,mid),cdq(mid+1,r);//递归
}
//以上为CDQ分治
int main(){
    n=_read(),k=_read();
    for (int i=1;i<=n;i++)
        q[i].x=_read(),q[i].y=_read(),q[i].z=_read(),q[i].id=i;
    sort(q+1,q+n+1,cmp2);
    for (int i=n-1,sum=0;i;i--)
        ans[q[i].id]+=(q[i]==q[i+1]?++sum:sum=0);
    cdq(1,n); for (int i=1;i<=n;i++) t[ans[i]]++;
    for (int i=0;i<n;i++) _write(t[i]);
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值