分治基础
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
<
a
i
,
b
j
<
b
i
a_j<a_i,b_j<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
<
a
i
,
b
j
<
b
i
,
c
j
<
c
i
a_j<a_i,\ b_j<b_i,\ c_j<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′〃)