首页 > 其他分享 >cdq分治

cdq分治

时间:2022-09-03 19:24:52浏览次数:88  
标签:偏序 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
From: https://www.cnblogs.com/gtm1514/p/16653357.html

相关文章

  • Problem P04. [算法课分治] 找到 k 个最小数
    先sort排序,在输出最小的k个数。#include<iostream>#include<bits/stdc++.h>#include<cstdio>usingnamespacestd;intn,k;intarr[10005];intmain(){......
  • Problem P05. [算法课分治] 寻找第 k 个最大元素
    先sort进行排序,然后输出第k大的元素即可#include<iostream>#include<bits/stdc++.h>#include<cstdio>usingnamespacestd;intn,k;intarr[10005];intmain......
  • Problem P01. [算法课分治] 最大二叉树
    需要注意的:scanf()的返回值是EOF,输入结束通过指针指向左右子树的二叉树构建#include<iostream>#include<bits/stdc++.h>#include<cstdio>usingnamespacestd;......
  • 树的难题 BJOI2017 点分治 单调队列
    P3714[BJOI2017]树的难题没时间码先口胡。明显有一个n^2的暴力。可以拿到20分。链的情况也非常容易一个简单的单调队列就可以解决当然可以暴力的采用线段树。这样......
  • [洛谷P5787] 线段树时间分治
    题目大意给\(n\)个点\(m\)条边,在\(k\)时间内,第\(i\)条边只在\([l_i+1,r_i]\)的时间范围内存在。对于每个\(i\leqk\),输出\(i\)时刻这个图是否是二分图。题......
  • 分治算法(汉诺塔)
    1.分治算法介绍1)分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最......
  • CDQ分治总结
    分治是什么分治(DivideandConquer),是一种把大规模数据分为更小规模数据单独处理然后合并的思想。如果连分治都不会的话建议看看LuoguP1177:快速排序,然后尝试用快排......
  • 【树套树与分治】
    降维方法可持久化条件:静态问题,且一般都是在线,因为可以离线的话,通常会用各方面更优的离线(分治)算法。减少一个维度后,时间复杂度去掉一个log对于二维问题,可持久化一维,如:......
  • 3.最大子段和之分治递归法(分治)
    题目描述:给定n(1<=n<=50000)个整数(可能为负数)组成的序列a[1],a[2],a[3],…,a[n],求该序列如a[i]+a[i+1]+…+a[j]的子段和的最大值。当所给的整数均为负数时定义子段和为0,......