CDQ分治 x 偏序问题

分治基础

1.划分
将原问题划分成若干子问题,子问题间互相独立且与原问题形式相同
2.解决 子问题
递归解决子问题,递归划分到子问题规模足够小,子问题的解可用常数时间解决
3.合并
将各子问题的解合并得到原问题的解


CDQ分治

CDQ分治的思路大体也和上述相同
特别的地方在于,对于一个大问题划分成的两个子问题
普通分治每个子问题只对自己产生贡献
CDQ分治中需要用前一个子问题对后一个子问题产生贡献


从二维偏序谈起

n n n个元素,第 i i i个元素有 a i , b i a_i,b_i ai,bi两个属性
f ( i ) f(i) f(i)表示 a j &lt; a i , b j &lt; b i a_j&lt;a_i,b_j&lt;b_i aj<ai,bj<bi j j j的数量 ,求所有 f ( i ) f(i) f(i)

这个问题是不是看的很眼熟?
没错,经典的逆序对问题就是他的简化版

逆序对问题相当于第一维可以被忽略(因为已经有序)的二维偏序问题
求解逆序对可以使用一个经典算法—归并排序

void merge(int ll,int rr)
{
    if(ll==rr) return;
    int mid=ll+rr>>1;
    merge(ll,mid); merge(mid+1,rr);
    
    int t1=ll,t2=mid+1,p=ll;
    while(t1<=mid&&t2<=rr)
    if(a[t1]>a[t2]){ b[p++]=a[t2++]; ans+=mid-t1+1;}
    else b[p++]=a[t1++];
    
    while(t1<=mid) b[p++]=a[t1++];
    while(t2<=rr) b[p++]=a[t2++];
    for(int i=ll;i<=rr;++i) a[i]=b[i];
}

int main()
{
    n=read();
    for(int i=1;i<=n;++i) a[i]=read();
	merge(1,n); 
    printf("%lld",ans);
    return 0;
}

这里为什么要提到归并排序
因为这里有很明显的CDQ思想体现
每次把问题区间 [ l l , r r ] [ll,rr] [ll,rr]划分成子区间 [ l l , m i d ] , [ m i d + 1 , r r ] [ll,mid],[mid+1,rr] [ll,mid],[mid+1,rr]
计算左子区间对右子区间的贡献

对于普通二维偏序问题 (不像求逆序对这样第一维已排序)
在上面基础上扩展出CDQ的解法
就是第一维直接排序第二维利用CDQ求解


CDQ分治解决三维偏序

n n n个元素,第 i i i个元素有 a i , b i , c i a_i,b_i,c_i ai,bi,ci两个属性
f ( i ) f(i) f(i)表示 a j &lt; a i ,   b j &lt; b i ,   c j &lt; c i a_j&lt;a_i,\ b_j&lt;b_i,\ c_j&lt;c_i aj<ai, bj<bi, cj<ci j j j的数量 ,求所有 f ( i ) f(i) f(i)

三维偏序是CDQ分治的一个很经典的应用
可以直接从上面二位偏序的解法扩展出来

第一维直接排序第二维利用CDQ求解
第三维呢,一个普遍的解法是权值树状数组

在处理第二维时发现左子区间的 i i i能对右子区间的 j j j产生贡献
那么我们 i i i的第三维属性 c i c_i ci插入树状数组
对于一个右子区间的 j j j就可以在树状数组中统计区间 1 1 1~ c j c_j cj的和计算来自左子区间的贡献

此处为三维偏序裸题陌上花开代码

#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
#include<cstring>
#include<cstdio>
using namespace std;
typedef long long lt;
#define lowbit(x) ((x)&(-x)) 

int read()
{
    int f=1,x=0;
    char ss=getchar();
    while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
    while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
    return x*f;
}

const int maxn=200010;
int N,n,k;
struct node{int x,y,z,cnt,ans;}a[maxn],b[maxn]; 
int tree[maxn],ans[maxn];

bool cmp(node a,node b){
    if(a.x!=b.x) return a.x<b.x;
    else if(a.y!=b.y) return a.y<b.y;
    else return a.z<b.z;
}

void add(int x,int v){ for(int i=x;i<=k;i+=lowbit(i))tree[i]+=v;}
int qsum(int x){ int res=0; for(int i=x;i>0;i-=lowbit(i))res+=tree[i]; return res;}

void CDQ(int ll,int rr)
{
    if(ll==rr) return;
    int mid=ll+rr>>1;
    CDQ(ll,mid); CDQ(mid+1,rr);

    int t1=ll,t2=mid+1,p=ll;
    while(t2<=rr)
    {
        while(a[t1].y<=a[t2].y&&t1<=mid) //若左子区间的t1对右子区间产生了贡献
        add(a[t1].z,a[t1].cnt),b[p++]=a[t1++];//就插入权值树状数组
        a[t2].ans+=qsum(a[t2].z); b[p++]=a[t2++];//更新左子区间对右子区间的t2产生的贡献
        //b数组是利用归并同时排序了第二维属性yi
    }
    for(int i=ll;i<t1;++i)//清空树状数组
    add(a[i].z,-a[i].cnt);

    while(t1<=mid) b[p++]=a[t1++];
    while(t2<=rr) b[p++]=a[t2++];
    for(int i=ll;i<=rr;++i) a[i]=b[i];
}

int main()
{
    N=read();k=read();
    for(int i=1;i<=N;++i)
    b[i].x=read(),b[i].y=read(),b[i].z=read();

    sort(b+1,b+1+N,cmp); //第一维xi直接排序从而忽略影响
    int num=0;
    for(int i=1;i<=N;++i)//统计完全相同的元素
    {
        num++;
        if(b[i].x!=b[i+1].x||b[i].y!=b[i+1].y||b[i].z!=b[i+1].z)
        a[++n]=b[i],a[n].cnt=num,a[n].ans=0,num=0;
        //cnt属性表示有多少个相同的,ans属性记录对i满足三维偏序条件的j数量
    }
    CDQ(1,n);

    for(int i=1;i<=n;++i)
    ans[a[i].ans+a[i].cnt-1]+=a[i].cnt;
    for(int i=0;i<N;++i)
    printf("%d\n",ans[i]);
    return 0;
}

【拓展】CDQ分治 x K维偏序

上述第三维偏序问题中对第三维的处理运用了树状数组
这是一般比较普遍且方便的写法

但事实上我们必须了解的是
CDQ是可以一直嵌套下去的

也就是说我们可以
第一维直接排序第二维CDQ第三维CDQ第K维还是CDQ
那天心血来潮了就补一下陌上花开的CDQ嵌套写法吧

当然K维偏序也可以利用K-D Tree
然而像我这种蒟蒻怎么可能会(〃` 3′〃)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值