CDQ 分治学习笔记

CDQ 分治,据说是由陈丹琦最先引入国内 OI 圈的。它与 dp 类似,是一种思想,而不是一种具体的算法。

CDQ 分治一般用来解决点对的问题,例如P3810 【模板】三维偏序(陌上花开)这道题:

给定 n 个元素,每个元素有权值 a_i,b_i,c_i。

规定 f(i) 为满足 a_j<=a_i,b_j<=b_i,c_j<=c_i 且 j!=i 的 j 的个数。

求所有 0<=x<n 的 ans(x) 表示有多少个满足 f(i)=x 的 i。

CDQ 分治的步骤大体如下:

  • 二分一个 midmid

  • 递归处理所有 i,jmidi,j\le mid 的点对和所有 i,jmid+1i,j\ge mid+1 的点对

  • 设法处理所有 imid,jmid+1i\le mid,j\ge mid+1 的点对

没错,归并排序求逆序对也是类似的思路,所以可以说归并排序求逆序对也是一种 CDQ 分治(

回到例题,首先考虑一个弱智版问题:

每个元素只有一个属性 a_i

很明显排一次序就行了。

再来看一个难一点的

每个元素有两个属性 a_i 和 b_i

可以先按 aia_i 排序,然后再用树状数组计算贡献。

sort(a+1,a+n+1,cmpa); // 按 a_i 排序
for(int i=1;i<=n;i++)
{
    // 下文 add 函数和 que 函数分别是树状数组的单点修改和前缀和查询
    a[i].ans=que(a[i].b); // 查询
    add(a[i].b,1); // 插入树状数组
}

回到例题,首先对 aia_i 排序,这样我们就消去了一个关键字。由于有重复的元素,所以我们可以做一下去重(其实不去重也没什么关系)。

接下来,我们可以使用 CDQ 分治来再去一个关键字

具体的做法是,递归分治完两边后,需要计算 x[l,mid]x\in[l,mid]y[mid+1,r]y\in[mid+1,r] 的贡献,那么由于 axyxa_x\le y_x,所以可以先对两个区间的 bib_i 排序,然后用树状数组统计答案。

排序部分可以用归并排序,省去一些时间。

代码如下:

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>

using namespace std;

struct node
{
	int a,b,c,cnt,ans;
}a[100005],tmp[100005];

int n,k;
int c[200005],ans[100005];

inline bool cmpa(node x,node y)
{
	if(x.a==y.a)
	{
		if(x.b==y.b)
		{
			return x.c<y.c;
		}
		return x.b<y.b;
	}
	return x.a<y.a;
}

inline bool cmpb(node x,node y)
{
	if(x.b==y.b)
	{
		return x.c<y.c;
	}
	return x.b<y.b;
}

inline void add(int pos,int val)
{
	for(int i=pos;i<=k;i+=i&-i)
	{
		c[i]+=val; 
	}
}

inline int que(int pos)
{
	int res=0;
	for(int i=pos;i>=1;i-=i&-i)
	{
		res+=c[i];
	}
	return res;
}

void slove(int l,int r)
{
	// “分” 
	if(l==r)
	{
		return;
	}
	int mid=l+r>>1;
	slove(l,mid);
	slove(mid+1,r);
	// “治”
	int j=l,tot=0;
	for(int i=mid+1;i<=r;i++) // 归并排序,计算贡献 
	{
		while(j<=mid&&a[j].b<=a[i].b) 
		{
			add(a[j].c,a[j].cnt); 
			tmp[++tot]=a[j++];
		}
		a[i].ans+=que(a[i].c);
		tmp[++tot]=a[i];
	}
	while(j<=mid)
	{
		add(a[j].c,a[j].cnt);
		tmp[++tot]=a[j++];
	}
	for(int i=l;i<=mid;i++) // 清空树状数组,这样写比 memset 快一点( 
	{
		add(a[i].c,-a[i].cnt);
	}
	for(int i=l;i<=r;i++)
	{
		a[i]=tmp[i-l+1];
	}
}

int main()
{
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d%d",&a[i].a,&a[i].b,&a[i].c);
	}
	sort(a+1,a+n+1,cmpa);
	int tot=0;
	for(int i=1,sum=0;i<=n;i++) // 去重 
	{
		sum++;
		if(a[i].a!=a[i+1].a||a[i].b!=a[i+1].b||a[i].c!=a[i+1].c) 
		{
			a[++tot]=a[i];
			a[tot].cnt=sum;
			sum=0;
		}
	}
	slove(1,tot);
	for(int i=1;i<=tot;i++)
	{
		ans[a[i].ans+a[i].cnt-1]+=a[i].cnt; // 计算答案,<= a_i 的有 a_i.ans 个,再算上 = a_i 的,最后减去重复计算的 a_i 
	}
	for(int i=0;i<n;i++)
	{
		printf("%d\n",ans[i]);
	}
	return 0;
}

做完这道题,可以发现 CDQ 分治处理偏序问题的本质是消关键字(

所以四维偏序可以使用 CDQ 分治套 CDQ 分治来做(((

练习题目

P3157 [CQOI2011]动态逆序对

UVA11990 ``Dynamic'' Inversion