ICode9

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

CDQ 分治

2022-02-16 14:00:51  阅读:205  来源: 互联网

标签:int 分治 mid leq CDQ quad


算法简介

CDQ 分治是一种基于分治思想,因原理与写法的不同,大概可以分为三类:

  1. 解决有关点对的问题
  2. 1D 动态规划的优化与转移
  3. 将动态问题转成静态问题

解决有关点对的问题

\(\quad\)这类问题多数类似于:给定一个长度为 n 的序列,统计有一些特性的点对 的数量/找到一对点 使得一些函数的值最大。而用 CDQ 分治解决这类问题的算法流程大致如下:

  1. 找到这个序列的中点 mid
  2. 将所有点对 \((i,j)\) 分成 3 类:

\[1\leq i\leq mid,1\leq j\leq mid \]

\[1\leq i\leq mid,mid+1\leq j\leq n \]

\[mid+1\leq i \leq n,mid+1\leq j \leq n, \]

  1. 将 \((1,n)\) 这个序列拆成两个序列 \((1,mid)\) 和 \((mid+1,n)\)。此时第一类点对和第三类点对都在这两个序列中
  2. 递归处理这两类点对
  3. 设法处理第二类点对

可以看到 CDQ 分治的思想就是不断地把点对通过递归的方式分给左右两个区间,

例题

三维偏序

\(\quad\)这是 CDQ 分治的一个很经典的应用,对于一个区间,先处理左区间和右区间的贡献,然后想办法处理第二类情况。首先按 c 排序,我们会发现 \(c_i<c_j\) 这个限制已经没有用了。然后我们对于每一个右区间的 j ,求出有多少个左区间的 i 满足条件。这里我们可以先将两个区间的 a 按照从小到大的顺序排个序,然后用两个指针,按顺序对于每一个 j 将 a 比自己小的 i 的 b 插入树状数组中,然后查询就直接在树状数组里面查询即可。

时间复杂度:\(O(nlogn^2)\)

code:

//code by SPzos
#include<bits/stdc++.h>
#define ll long long
#define double
#define fo(i,j,k) for(register int i=j;i<=k;++i)
#define fd(i,j,k) for(register int i=j;i>=k;--i)
#define ff(i,x) for(register int i=head[x];i;i=e[i].nxt)
#define fv(i,x) for(register int i=0;i<v[x].size();++i)
using namespace std;
const int N=100010,K=200010;
inline int read(){
	int ret=0,f=0;
	char ch=getchar();
	while(ch<'0' || ch>'9'){if(ch=='-') f=1;ch=getchar();}
	while(ch>='0' && ch<='9') {ret=(ret<<1)+(ret<<3)+ch-(1<<4)-(1<<5);ch=getchar();} 
	return f?-ret:ret;
}
struct node{
	int x,y,z,ans,w;
}a[N],b[N];
bool cmp1(node p1,node p2){
	if(p1.x==p2.x){
		if(p1.y==p2.y) return p1.z<p2.z;
		return p1.y<p2.y;
	}
	return p1.x<p2.x;
}
bool cmp2(node p1,node p2){
	if(p1.y==p2.y) return p1.z<p2.z;
	return p1.y<p2.y;
}
struct tree{
	int kk,c[K];
	inline int lowbit(int x){
		return x&(-x);
	}
	inline void add(int x,int y){
		for(int i=x;i<=kk;i+=lowbit(i)) c[i]+=y;
	} 
	inline int query(int x){
		int sum=0;
		for(int i=x;i>0;i-=lowbit(i)) sum+=c[i]; 
		return sum;
	}
}t;
inline void cdq(int l,int r){
	if(l==r) return ;
	int m=l+r>>1;
	cdq(l,m);cdq(m+1,r);
	sort(a+l,a+m+1,cmp2);
	sort(a+m+1,a+r+1,cmp2);
	int t1=l,t2=m+1;
	while(t2<=r){
		while(a[t1].y<=a[t2].y && t1<=m){
			t.add(a[t1].z,a[t1].w);
			++t1;
		}
		a[t2].ans+=t.query(a[t2].z);
		++t2;
	}
	fo(i,l,t1-1) t.add(a[i].z,-a[i].w);
}
int ct,cnt[N],n,k;
int main(){
	n=read();k=read();
	t.kk=k;
	fo(i,1,n){
		b[i].x=read();b[i].y=read();b[i].z=read();
	}
	sort(b+1,b+1+n,cmp1);
	int c=0;
	fo(i,1,n){
		c++;
		if(b[i].x!=b[i+1].x || b[i].y!=b[i+1].y || b[i].z!=b[i+1].z){
			a[++ct]=b[i];
			a[ct].w=c;c=0;
		}
	} 
	cdq(1,ct);
	fo(i,1,ct) cnt[a[i].ans+a[i].w-1]+=a[i].w;
	fo(i,0,n-1) printf("%dn",cnt[i]);
	return 0;
} 

\(\quad\)扩展:既然说到了三位偏序,那就顺便提一下 四维偏序

\(\quad\)四维偏序就是 CDQ分治 套 CDQ分治。

\(\quad\)假如四维偏序的元素属性是 a,b,c,d,那么先按 a 排序,然后处理一个区间的时候,还是递归处理左右区间,至于左区间对右区间的贡献,就需要再套一个 CDQ 分治来操作。具体就是对于左区间的数字打上一个标记 L ,右区间打上一个 标记 R ,然后新的 CDQ2 分治的基础是 b 已经排好序了,先忽略 a 这一维,那么就是一个三维偏序,至于 a 的限制就是当 j 的标记是 R,且 i 的标记是 L 的时候,才能累计贡献。

动态逆序对

\(\quad\)我们可以换一个角度,求出每一次删除后会影响多少个逆序对。首先还是要按时间排序,这样每个点就只用在自己前面找就可以了,可以保证不重不漏。但是有些已经删除的就不能算在贡献里面,那我们就可以给它打一个负标记,这样就会和之前的正标记抵消了。然后计算答案的话,就可以对每一个点存一下它们消除的时间,最后求一个前缀和就行了。

//code by SPzos
#include<bits/stdc++.h>
#define ll long long
#define double
#define fo(i,j,k) for(register int i=j;i<=k;++i)
#define fd(i,j,k) for(register int i=j;i>=k;--i)
#define ff(i,x) for(register int i=head[x];i;i=e[i].nxt)
#define fv(i,x) for(register int i=0;i<v[x].size();++i)
using namespace std;
const int N=100010;
inline int read(){
	int ret=0,f=0;
	char ch=getchar();
	while(ch<'0' || ch>'9'){if(ch=='-') f=1;ch=getchar();}
	while(ch>='0' && ch<='9') {ret=(ret<<1)+(ret<<3)+ch-(1<<4)-(1<<5);ch=getchar();} 
	return f?-ret:ret;
}
int tot,n,m,a[N];
ll ans[N];
int pos[N],c[N];
struct AC{
    int m,v,d,id,t;//正负,数值,位置,t->time
}e[N<<1];
bool cmp1(AC x,AC y){return x.d<y.d;}
void add(int x,int k){while(x<=n) c[x]+=k,x+=(x&(-x));}
int query(int x){int sum=0;while(x) sum+=c[x],x-=(x&(-x));return sum;}
void cdq(int l,int r){
    if(l==r) return ;
    int mid=l+r>>1;
    int j=l;
    cdq(l,mid);cdq(mid+1,r);
    sort(e+l,e+mid+1,cmp1);
    sort(e+mid+1,e+r+1,cmp1);
    for(int i=mid+1;i<=r;++i){
        while(j<=mid && e[j].d<=e[i].d) add(e[j].v,e[j].m),++j;
        ans[e[i].id]+=e[i].m*(query(n)-query(e[i].v));
    }
    for(int i=l;i<j;++i) add(e[i].v,-e[i].m);
    j=mid;
    for(int i=r;i>mid;--i){
        while(j>=l && e[j].d>=e[i].d) add(e[j].v,e[j].m),--j;
        ans[e[i].id]+=e[i].m*query(e[i].v-1);
    }
    for(int i=mid;i>j;--i) add(e[i].v,-e[i].m);
}
int main(){
    n=read();m=read();
    fo(i,1,n) a[i]=read(),pos[a[i]]=i,e[++tot]=(AC){1,a[i],i,0,tot};
    for(int i=1,x;i<=m;++i) x=read(),e[++tot]=(AC){-1,x,pos[x],i,tot};
    cdq(1,tot);
    fo(i,1,m) ans[i]+=ans[i-1];
    for(int i=0;i<m;++i) printf("%lldn",ans[i]);
	return 0;
} 

CDQ 分治优化 1D/1D 动态规划的转移

$\quad$1D/1D 动态规划指的是一类特定的 DP 问题,该类题目的特征是 DP 数组是一维的,转移是 \(O(n^2)\) 的,而我们希望能用 CDQ 分治的思想将其优化成 \(O(nlogn^2)\)。

\(\quad\)例如给定一个序列,每一个元素有两个属性 a,b 。我们希望计算一个式子的值,转移方程如下:

\[dp_i=1+max_{j=1}^{i-1}dp[j][a_j<a_i][b_j<b_i] \]

\(\quad\)这是一个二维最长上升子序列的 DP 方程。

\(\quad\)其实优化步骤也是差不多的:

  1. 处理左区间
  2. 处理左区间对右区间的影响
  3. 处理右区间

\(\quad\)这里的第 2 步在之前的问题中什么时候都可以操作,但是在此必须”夹在中间“,这是显然的。

例题

P2487 [SDOI2011]拦截导弹

\(\quad\)很明显是个二维最长上升子序列问题,所以可以考虑用 CDQ 分治优化。

\(\quad\)我们需要记录四个数组

f1[i]//以i为结尾的最长长度
f2[i]//以i为开头的最长长度
g1[i]//以i为结尾的最长长度的数量
g2[i]//以i为开头的最长长度的数量

\(\quad\)这个就可以用 CDQ 分治直接求,第二个步骤的数据结构既要支持单点修改,还要支持查询区间最大值和对应的数量,个人认为线段树是一个很好的选择。然后考虑每一个点能在几个这种序列上。不可能去记录路径的,但是我们可以发现一个性质就是当一个的点的 \(f1[i]+f2[i]-1=len_{max}\) 时,它出现在最长序列的次数就是 \(g1[i]\times g2[i]\) ,这是显然的。而总的数量可以算出来,假设是 sum ,那么概率就是 \(\frac{g1[i]\times g2[i]}{sum}\) ,此题终。

\(\quad\)注意:这个题开 long long 都不够,要开 double ,听说不会爆,原理未知......

将动态问题转化为静态问题

\(\quad\)前两种问题是将序列折半后递归处理点对间的关系,不过在这一节中折叠的是时间序列。

\(\quad\)它适用于一些$\lceil $ 需要支持做...修改然后询问...\(\rceil\) 的数据结构题。该类题目有两个特点:

  1. 如果将操作离线,所有操作会按照时间自然的排成一个序列。
  2. 每一个修改均与之后的询问操作息息相关。

\(\quad\)我们可以使用 CDQ 分治对这个操作序列进行分治,先递归处理 \((l,mid)\) 的修改-询问关系,再处理所有 \(l\leq i \leq mid,mid+1\leq j\leq r\) 的修改-询问关系,其中 i 是一个修改,j 是一个询问,最后再处理 \((mid+1,r)\) 的修改-询问关系。注意,如果修改之间是独立的,那么就不需要管这个处理的顺序了。

\(\quad\)想一个很经典的例子:动态二维数点。

\(\quad\)只用考虑在自己之前的操作对自己的贡献,这就可以将这个问题转化为静态问题,用 CDQ 去解决。

\(\quad\)因为这一点,CDQ 分治被称为 \(\lceil\) 动态问题转化为静态问题的工具\(\rceil\)。

例题

Luogu 4690 [Ynoi2016] 镜中的昆虫

Luogu 3206 [HNOI2010]城市建设

注意

\(\quad\)在我之前的程序中都是直接在递归中 sort ,事实上这样跑的复杂度假了,但是也能跑过去,所以如果打代码的时候有空闲时间其实可以以归并排序这种思想来处理左右区间的同时排序,这样的复杂度就比较正确。但是有点复杂,sort 确实比较直观。

标签:int,分治,mid,leq,CDQ,quad
来源: https://www.cnblogs.com/SPzos017/p/15900152.html

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

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

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

ICode9版权所有