ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

cdq分治

2022-09-03 19:32:36  阅读:158  来源: 互联网

标签:偏序 int 分治 mid cdq dp


cdq分治,一种广为人知的离线分治算法。大体的思想是:

  1. 将左右两边区间分开递归处理。
  2. 统计左边区间修改对右边区间查询的影响。

第一步很简单,写两个递归就行了。关键在第二步。我们搞个cdq的经典问题——三维偏序来具体解释这个东西。

P3810 【模板】三维偏序(陌上花开)

三维偏序,顾名思义,要求三维都满足某个条件。具体地,对于每个元素有一个三元组\((a_i,b_i,c_i)\),我们要求对每个元素\(i\)满足偏序关系\(a_j\le a_i,b_j\le b_i,c_j\le c_i\)的元素个数。

回忆我们解决二维偏序的方法,按照第一维为第一关键字,第二维为第二关键字的顺序排序。然后顺序扫描每个元素,将其第二维插入树状数组并统计答案。显然,第一维已经通过排序保证有序,树状数组又可以保证第二维只查到比当前数小的数,因此算法正确。

现在我们将问题拓展到了三维,我们可以用cdq分治解决问题。首先我们还是按照第一维为第一关键字,第二维为第二关键字,第三维为第三关键字排序,这样我们的第一维就是有序的。

然后我们开始分治。首先我们左右区间内部的贡献可以递归处理,那么我们就只需要考虑区间\([l,mid]\)的元素对区间\([mid+1,r]\)内答案的贡献。此时,由于我们第一维已经排好序(显然,这样二分是不会有区间重复的,参考线段树),那么左区间\([l,mid]\)的第一维元素就会小于等于右区间\([mid+1,r]\)的元素,也就是不用考虑第一维。这样我们就转化成了普通的二维偏序。

关于二维偏序,我们可以将左右两边分别按第二维为第一关键字,第三维为第二关键字进行排序。这样我们左右两边区间的第一维和第二维就都是有序的了。我们可以用两个指针扫描左右区间,用树状数组统计第三维,更新答案。

然后上个代码。注意这个题因为有相同元素,本来相互间有贡献,但是二分的时候会分开,没法相互统计,所以直接去个重。

struct node{
	int a,b,c,ans;
}s[1000010];
bool cmp1(node a,node b){
	return a.a==b.a?(a.b==b.b?a.c<b.c:a.b<b.b):a.a<b.a;
}
bool cmp2(node a,node b){
	return a.b==b.b?a.c<b.c:a.b<b.b;
}
void cdq(int l,int r){
	if(l==r)return;//边界为只有一个元素 
	int mid=(l+r)>>1;
	cdq(l,mid);cdq(mid+1,r);//第一步 递归处理两边 
	sort(s+l,s+mid+1,cmp2);sort(s+mid+1,s+r+1,cmp2);
	int j=l;//左区间指针 
	for(int i=mid+1;i<=r;i++){
		while(s[j].b<s[i].b&&j<=mid){
			update(s[j].c,s[j].cnt);j++;
		}
		s[i].ans+=query(s[i].c);
	}
	for(int i=l;i<j;i++)update(s[i].c,-s[i].cnt);//每次统计之后清空树状数组 
}
int main(){
	//输入与去重 
	sort(s+1,s+n+1,cmp1);
	cdq(1,n);
}

然后是另一个东西:cdq分治优化dp。

具体地说,有的题的dp方程长得像\(dp[i]=\max_{j=1}^{i-1}dp[j]([a_j<a_i][b_j<b_i])+k\)这种。发现这个东西,\(a\)是一维,\(b\)是一维,位置也是一维,于是就成了个三维偏序,可以用cdq来处理。

但是关于这个的cdq有一点要注意,就是一定要先递归左边,然后处理左边对右边的影响,然后还原(这个非常重要,因为你一定要按照原数组进行dp而不是排序以后的数组。举个例子,你求最大子序列和是直接扫还是排序再扫),最后递归右边。

原因是,我们普通的dp都是从左到右处理的。但是cdq分治是先处理小区间然后再处理大区间。如果我们按照普通的cdq写,那么先处理左边,再处理右边,统计左边对右边的贡献的时候,转移顺序就乱掉了。(毕竟我们正常dp也没有先左边再右边的吧)

处理方法也很简单,树状数组清空之后再按照第一维排一边序初始化整个数组,然后再递归右边就行了。

随便粘了个题的cdq部分。

void cdq(int l,int r){
	if(l==r)return; 
	int mid=(l+r)>>1;cdq(l,mid);
	sort(s+l,s+mid+1,cmp1);sort(s+mid+1,s+r+1,cmp2);
	int j=l;
	for(int i=mid+1;i<=r;i++){
		while(s[j].val<=s[i].l&&j<=mid){
			update(s[j].r,dp[s[j].t]);
			j++;
		}
		dp[s[i].t]=max(dp[s[i].t],query(s[i].val)+1);
	}
	for(int i=j-1;i>=l;i--)clear(s[i].r);
	sort(s+l,s+r+1);cdq(mid+1,r);//一定要先左再右
}

还有一点就是将动态问题(带修改)转化为静态问题。

举个例子,维护一个二维平面,然后支持在一个矩形区域内加一个数字,每次询问一个矩形区域的和。

如果不带修,我们有一个比较简洁的做法:扫描线。我们将每个矩形变成两条边,一条加,一条减回来。然后查询的时候我们维护一条一定长度的扫描线统计答案。

现在给你带修,我们可以仍然使用cdq分治。将修改拆成加减两个操作,询问差分成几个大减小。左对右的影响可以直接修改并且用扫描线处理,然后递归分治左右两边的关系。这样,我们就把动态问题转化为了静态问题。

另外的,如果各个修改之间独立(比如普通的加减,像上题一样),那么递归向下分治的顺序是无所谓的。然而,如果修改会对整个序列进行影响,而且后面的序列长什么样取决于前面的序列(比如上边的dp,修改会影响序列),那么我们就必须先分治左边,然后处理左边对右边影响,最后分治右边。原理和上面是差不多的。

标签:偏序,int,分治,mid,cdq,dp
来源: https://www.cnblogs.com/gtm1514/p/16653357.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有