CDQ分治基础

引入

偏序问题: 这种类型的题目通常会告诉我们n个数组,其中每个元素有不同属性,询问通常是回答有多少元素A满足元素B的属性条件。

如一个元素有三种属性:a,b,c,对于元素B,求满足条件的元素A(A.a<B.a,A.b>B.b,A.c<B.c)的个数,这就是一道典型的三维偏序问题。

而解决偏序问题通常有以下方法:排序,数据结构(树状数组,线段树,平衡树),cdq分治。

在处理这类问题时我们通常会通过排序来处理其中一个属性(最简易),那剩下几个属性呢?容易想到,每一个都可以用一层数据结构维护。那么只有一个属性时我们用一颗树维护;两个属性,树套树?;三个属性,树套树套树?四个属性......当大量数据结构堆在一起,代码量就十分爆炸,而且bug将无处不在。这时我们就要用到CDQ分治,它是我们处理多维偏序问题的重要武器。它的优势在于可以顶替复杂的高级数据结构,而且常数比较小;缺点在于必须离线操作。

基本思想

CDQ分治的基本思想十分简单。如下:

  1. 我们要解决一系列问题,这些问题一般包含修改和查询操作,可以把这些问题排成一个序列,用一个区间[L,R]表示。
  2. 分。递归处理左边区间[L,M]和右边区间[M+1,R]的问题。
  3. 治。合并两个子问题,同时考虑到[L,M]内的修改对[M+1,R]内的查询产生的影响。即,用左边的子问题帮助解决右边的子问题。

  这就是CDQ分治的基本思想。和普通分治不同的地方在于,普通分治在合并两个子问题的过程中,[L,M]内的问题不会对[M+1,R]内的问题产生影响。(转载__stdcall的博客)

二维偏序问题

给定N个有序对(a,b),求对于每个(a,b),满足a2<a且b2<b的有序对(a2,b2)有多少个(条件随题目而变)。

模板题:普通逆序对

这道题大家应该非常熟悉,是归并排序的应用(不会的请先学习),但这道题也是CDQ分治最基础的题。所以说,我们其实很早就已经学会了最简单的CDQ分治,只是我们不理解这个概念。

重新分析这道题,为什么说求逆序对是偏序问题。对于一个数A,和它组成逆序对的数B满足:B小于A,B在A后面。我们发现,这两句话不就是描述了两个属性条件吗。属性一:数值大小。属性二:在整个序列里的位置(下标)。第二个属性十分隐蔽,因此初学者很难发现它。

回忆一下归并排序求逆序对的过程,我们在合并两个子区间的时候,要考虑到左边区间的对右边区间的影响。即,我们每次从右边区间的有序序列中取出一个元素的时候,要把“以这个元素结尾的逆序对的个数”加上“左边区间有多少个元素比他大”。这是一个典型的CDQ分治的过程。那么对于二维偏序问题,我们在拿到所有有序对(a,b)的时候,先把a元素从小到大排序。这时候问题就变成了“求b元素的顺/逆序对”(具体求什么看题目),因为a元素已经有序,可以忽略a元素带来的影响,和“求逆序对”的问题是一样的。

代码:

#include<cstdio>
int s[100005],t[100005],n;
long long tot;
void merge(int l,int m,int r)
{
	int p=l,q=m+1,k=0;
	while(p<=m&&q<=r)
	{
		if(s[p]<=s[q])
			t[k++]=s[p++];
		else
		{
			t[k++]=s[q++];//因为左右区间分别有序,并且。p永远大于q
			tot+=m-p+1;//所以当s[p]>s[q]时,s[p]后面所有左区间的数>s[q],都为逆序对
		}
	}
	while(p<=m)
		t[k++]=s[p++];
	while(q<=r)
		t[k++]=s[q++];
	for(int i=l;i<=r;i++)		
		s[i]=t[i-l];
}
void msort(int l,int r)
{
	int mid;
	if(l<r)
	{ 
		mid=(l+r)/2;
		msort(l,mid);//处理左区间
		msort(mid+1,r);//处理右区间
		merge(l,mid,r);
	}
}
int main() 
{
	scanf("%d",&n);
	for(int i=0;i<n;i++)
		scanf("%d",&s[i]);
	msort(0,n-1);
	printf("%lld",tot);
}

 

三维偏序问题(重点)

给定N个有序三元组(a,b,c),求对于每个三元组(a,b,c),有多少个三元组(a2,b2,c2)满足a2<a且b2<b且c2<c(条件随题目而变)。

类似二维偏序问题,先按照a元素从小到大排序,忽略a元素的影响。然后CDQ分治,按照b元素从小到大的顺序进行归并操作。但是这时候没办法像 求逆序对 一样简单地统计个数了,c元素如何处理呢?

这时候比较好的方案就是借助树状数组维护c元素(一层数据结构可以接受)。每次从右边的序列中取出三元组(a,b,c)时,对树状数组查询c值小于(a,b,c)的三元组有多少个;每次从左边序列取出三元组(a,b,c)的时候,根据c值在树状数组中进行修改。

模板题:动态逆序对

题意:给1到n的一个排列,按照某种顺序依次删除m个元素,你的任务是在每次删除一个元素之前统计整个序列的逆序对数。

同样的题,怎么就三维偏序了呢?由于序列是动态的,所以每个数就多了一个加入时间(题目是删除,我们就反过来离线处理),这就是它的第三维元素。于是对于每一个数A有三个元素(下标x,数值y,加入时间z)。这道题就变成了:对于每一个数A,求满足(B.x>A.x,B.y<A.y,B.z>A.z)的数B的个数。这是一个典型的三维偏序问题。

代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 3000005
struct mzls
{
	int x,y,z;//下标,数值,加入时间。
}d[N];
int t[N],n,k,tot,num[N],a[N],a1[N],a2[N];
long long ans[N];
bool f1[N];
inline bool cmp(mzls a,mzls b)
{
	return a.z<b.z;
}
inline bool cmp1(mzls a,mzls b)
{
	return a.y>b.y;
}
inline bool cmp2(mzls a,mzls b)
{
	return a.y<b.y;
}
inline void modify(int x,int v)
{
	for(int i=x;i<=n;i+=i&(-i))
		t[i]+=v;
}
inline int query(int x)
{
	int sum=0;
	for(int i=x;i>0;i-=i&(-i))
		sum+=t[i];
	return sum;
}
inline void cdq(int l,int r)
{
	if(l==r) 
	 	return;
	int mid=(l+r)>>1;
	cdq(l,mid); 
	cdq(mid+1,r);
	sort(d+l,d+mid+1,cmp1);//手动排序
	sort(d+mid+1,d+r+1,cmp1);
	int j=l;
	for(int i=mid+1;i<=r;i++)//记录数值大的,下标小的
	{
	 	while(j<=mid&&d[j].y>d[i].y)
		   modify(d[j].x,1),j++;
	    ans[d[i].z]+=query(d[i].x);
	}
	for(int i=l;i<j;i++)//树状数组恢复
		modify(d[i].x,-1);
	sort(d+l,d+mid+1,cmp2);//注意排序的不同
	sort(d+mid+1,d+r+1,cmp2);
	j=l;
	for(int i=mid+1;i<=r;i++)//记录数值小的,下标大的
	{
	 	while(j<=mid&&d[j].y<d[i].y)
		   modify(d[j].x,1),j++; 
	    ans[d[i].z]+=query(n)-query(d[i].x-1);
	}
	for(int i=l;i<j;i++) 
		modify(d[i].x,-1);
}
int main()//主函数注意离线反向操作
{
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		f1[a[i]]=1;//是否被删除
		a1[a[i]]=i;//记录下标
	}
	for(int i=1;i<=k;i++)
	{
		scanf("%d",&a2[i]);
		f1[a2[i]]=0;
	}
	for(int i=1;i<=n;i++)
		if(f1[a[i]])//没有被删除的优先加入
		{
			tot++;
			d[tot].x=a1[a[i]];
			d[tot].y=a[i];
			d[tot].z=tot;
		}
	for(int i=k;i>=1;i--)//删除=反过来加入
	{
		tot++;
		d[tot].x=a1[a2[i]];
		d[tot].y=a2[i];
		d[tot].z=tot;
	}
	sort(d+1,d+n+1,cmp);
	cdq(1,tot);
	sort(d+1,d+n+1,cmp);
	for(int i=2;i<=tot;i++)//之前的答案需要累加
		ans[i]+=ans[i-1];
	for(int i=n;i>=n-k+1;i--)//注意不能到1,因为可能没有删除完
		printf("%lld\n",ans[i]);
}

简单说几句:

  1. 以上方法的效率基本是每上一维多一个log,但是空间基本都是nlog(n)的数据大的话到五维可能就超时了。
  2. 第二句我想想。
  3. 关于不同方案间的优劣的话,排序是必须的,直接调用c++的快排即可,数据结构之间树状数组是最值得使用的,常数小而且代码短,cdq分治在时间方面要慢于树状数组,但是数据结构通常只能维护一维(大佬绕道),所以通常考虑使用排序+cdq分治+树状数组的方案,简单而且高效。
内容概要:本文档主要展示了C语言中关于字符串处理、指针操作以及动态内存分配的相关代码示例。首先介绍了如何实现键值对(“key=value”)字符串的解析,包括去除多余空格和根据键获取对应值的功能,并提供了相应的测试用例。接着演示了从给定字符串中分离出奇偶位置字符的方法,并将结果分别存储到两个不同的缓冲区中。此外,还探讨了常量(const)修饰符在变量和指针中的应用规则,解释了不同类型指针的区别及其使用场景。最后,详细讲解了如何动态分配二维字符数组,并实现了对这类数组的排序与释放操作。 适合人群:具有C语言基础的程序员或计算机科学相关专业的学生,尤其是那些希望深入理解字符串处理、指针操作以及动态内存管理机制的学习者。 使用场景及目标:①掌握如何高效地解析键值对字符串并去除其中的空白字符;②学会编写能够正确处理奇偶索引字符的函数;③理解const修饰符的作用范围及其对程序逻辑的影响;④熟悉动态分配二维字符数组的技术,并能对其进行有效的排序和清理。 阅读建议:由于本资源涉及较多底层概念和技术细节,建议读者先复习C语言基础知识,特别是指针和内存管理部分。在学习过程中,可以尝试动手编写类似的代码片段,以便更好地理解和掌握文中所介绍的各种技巧。同时,注意观察代码注释,它们对于理解复杂逻辑非常有帮助。
### 关于CDQ分治基础练习题目 #### 三维偏序问题入门示例 考虑一个经典的三维偏序问题,给定n个三元组(a_i,b_i,c_i),求对于每一个i(1≤ i ≤ n),有多少个j满足a_j ≤ a_i, b_j ≤ b_i 和 c_j ≤ c_i (j ≠ i)[^1]。 这个问题可以通过CDQ分治来高效解决。核心思路在于将原问题分解成更小规模的子问题,并通过处理这些子问题之间的关系得到最终解。具体来说,在每次划分过程中,先按照某一维度排序(比如a),再利用已排序的结果递归地解决问题的一半对另一半的影响。 ```cpp #include <algorithm> using namespace std; struct node { int x,y,z,id; }p[200005],tmp[200005]; int bit[200005],ans[200005]; bool cmp_x(const node& a,const node& b){ return a.x<b.x||(a.x==b.x&&a.y<b.y)||(a.x==b.x&&a.y==b.y&&a.z<b.z); } void add(int pos,int val){ while(pos<=200000){ bit[pos]+=val; pos+=pos&(-pos); } } int query(int pos){ int ret=0; while(pos>0){ ret+=bit[pos]; pos-=pos&(-pos); } return ret; } void cdq(int l,int r){ if(l>=r)return ; int mid=(l+r)>>1,i=l,j=mid+1,k=l; cdq(l,mid);cdq(mid+1,r); sort(p+l,p+mid+1,cmp_y); sort(p+mid+1,p+r+1,cmp_y); for(;k<=r;k++){ tmp[k]=p[j]; j++; }else{ ans[p[i].id]+=query(p[i].z); add(p[i].z,1); tmp[k++]=p[i++]; } for(i=l;i<j;i++)if(tmp[i].id<=mid)add(tmp[i].z,-1); copy(tmp+l,tmp+r+1,p+l); } ``` 上述代码片段展示了如何应用CDQ分治方法去计算三维偏序中的逆序对数量。这里`cdq()`函数实现了主要逻辑,它不仅解决了当前区间内的部分问题,还负责统计左区间的元素对右区间产生的影响。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值