首页 > 其他分享 >$CDQ$ 分治总结

$CDQ$ 分治总结

时间:2024-02-03 09:13:11浏览次数:25  
标签:总结 树状 int 分治 mid lt CDQ 排序

\(CDQ\) 分治是一种特殊的分治方法,基本思想就是前一半的结果辅助后一半答案解答。

一、归并排序

提到 \(CDQ\) 分治,就不得不提到归并排序。
作为一种 似乎只有在瑞士轮里才有用的算法,归并排序有着优秀的时间复杂度,短小精悍的代码,十分的可爱。
首先,我们将问题转换成这样(\(l,r\) 代表目前处理的区间,\(a\) 为原序列):

  1. 假如 \(l=r\),直接返回;
  2. 将序列分成前后两部分,分别递归处理;
  3. 维护两个指针 \(lt=l,rt=mid+1\);
  4. 若 \(a_{lt}<a_{rt}\),先放 \(a_{lt}\) 且 \(lt++\),否则放 \(a_{rt}\) 且 \(rt++\)。

正确性显然,考虑时间复杂度:

  1. 第 \(i\) 层序列长度为 \(n\times 2^{1-i}\),最多分成 \(\log_2n\) 层;
  2. 每层每个量只过一遍,相当于一层时间复杂度 \(O(n)\)。

所以时间复杂度 \(O(n\log_2n)\)。
代码有注释,可供参考:

#include<bits/stdc++.h>
using namespace std;
int n,a[100005],b[100005];
void gb_sort(int l,int r){
	if(l==r) return;//l,r相同,直接退出
	int mid=(l+r)/2,lt=l,rt=mid+1;
	gb_sort(l,mid);gb_sort(mid+1,r);//递归处理
	for(int i=l;i<=r;i++){
		if(a[lt]<a[rt]&&lt<=mid||rt>r)
			b[i]=a[lt++];else b[i]=a[rt++];
	}//排序,[l,r]的有序序列保存在b[]
	for(int i=l;i<=r;i++) a[i]=b[i];//复制过来
}signed main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	gb_sort(1,n);
	for(int i=1;i<=n;i++)
		printf("%d ",a[i]);
	return 0;
}

这是 \(CDQ\) 分治的一个基本运用。

二、二维偏序(逆序对)

我们考虑这个问题:

  • 给出长度为 \(n\) 的序列 \(a\),问有多少个数对 \((i,j)\),使得 \(i<j\) 且 \(a_i>a_j\),\(n\le 5\times 10^5\)。

很明显,此题也可以用树状数组,并且很方便(记住这个叫树状数组的物体,很快我们会再次遇到他,并且了解他的弱项)。
不过,我们可不可以通过改动归并排序代码,解决这个问题呢?
当然可以!我们发现,可以将原序列分为三个部分统计答案:前半、后半、整体。
前半和后半可以递归处理,主要考虑 通过前半部分的数据统计整体答案!!!
设 \(k\) 在 \([mid+1,r]\) 中,则 \(k\) 可以和在归并排序中后于他进入的位置 \(l\) 形成逆序对,其中 \(l\) 在 \([l,mid]\) 中。
那么我们只需做出以下修改:

#include<bits/stdc++.h>
using namespace std;
int n,a[500005],b[500005];
long long ans;
void gb_sort(int l,int r){
	if(l==r) return;
	int mid=(l+r)/2,lt=l,rt=mid+1;
	gb_sort(l,mid);gb_sort(mid+1,r);
	for(int i=l;i<=r;i++){
		if((a[lt]>a[rt]&&rt<=r)||lt>mid)
		    b[i]=a[rt++],ans+=mid+1-lt;
		else b[i]=a[lt++];
		//还有mid+1-lt个属于[l,mid]中的l没有加入
	}for(int i=l;i<=r;i++) a[i]=b[i];
}signed main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	gb_sort(1,n);printf("%lld",ans);
	return 0;
}

那么,假如将问题改一下呢?

  • 给出长度为 \(n\) 的两个序列 \(a,b\),问有多少个数对 \((i,j)\),使得 \(a_i<a_j\) 且 \(b_i<b_j\),\(n\le 5\times 10^5\)。

看起来来者不善,但假如我们按照 \(a\) 序列先行排序呢?
我们就会发现,本题需要考虑的就只剩下 \(b\) 序列了,相当于求顺序对。
代码就不写了,思路也十分简单。

三、陌上花开 可缓缓归矣——三维偏序

看如下这个问题(本题名为:陌上花开):

  • 给出长度为 \(n\) 的三个序列 \(a,b,c\),问有多少个数对 \((i,j)\),使得 \(a_i<a_j\) 且 \(b_i<b_j\) 且 \(c_i<c_j\),\(n\le 10^5\)。

我相信大家的第一反应一定不是直接排序第一维后跑二维树状数组……
面对这种题,我们该怎么办呢?
\(CDQ\) 分治就可以很好的解决这个问题。
考虑先按照第一维排序,然后考虑第二维。
假如要把问题分成前半、后半、整体的话,前半和后半当然还是递归。
整体,主要考虑前半对后半的贡献。
发现,当前半部分的 \(b,c\) 都小于后半部分时,能供提供一个答案。
那么,我们只需要按照 \(b\) 值前后分别进行排序,用树状数组维护已经进队的前半部分 \(c\) 值,后半部分每次查询树状数组即可。
具体可以看代码:

#include<bits/stdc++.h>
#define ll long long
#define N 100005
using namespace std;
inline int read(){
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }while(ch>='0'&&ch<='9')
        x=x*10+ch-'0',ch=getchar();
    return x*f;
}int n,m,k,re[N],c[2*N];struct laz{int a,b,c,sz,ans;}lyh[N],zjy[N];
//判个相等,没啥别的意思
int ch(laz x,laz y){return (x.a==y.a&&x.b==y.b&&x.c==y.c);}
//树状数组
void add(int x,int cc){for(;x<=k;x+=x&-x) c[x]+=cc;}
int sum(int x){int re=0;for(;x;x-=x&-x) re+=c[x];return re;}
//按第一维排序
int cmp(laz x,laz y){
    return x.a!=y.a?x.a<y.a:(x.b!=y.b?x.b<y.b:x.c<y.c);
}//cdq主要部分!!!
void cdq(int l,int r){
    if(l==r) return;int mid=(l+r)/2;cdq(l,mid);cdq(mid+1,r);//递归处理
    int lt=l,rt=mid+1;for(int i=1;i<=k;i++) c[i]=0;//清空树状数组
    for(int i=l;i<=r;i++){
        if((zjy[lt].b<=zjy[rt].b&&lt<=mid)||rt>r)
            add(zjy[lt].c,zjy[lt].sz),lyh[i]=zjy[lt++];//前半部分,贡献加入树状数组
        else lyh[i]=zjy[rt++],lyh[i].ans+=sum(lyh[i].c);//后半部分,查询树状数组,修改答案
    }for(int i=l;i<=r;i++) zjy[i]=lyh[i];
    //实际上,这里并不一定需要在cdq中用归并方法排序,也可以在前面直接排序
}signed main(){
    n=read();k=read();for(int i=1;i<=n;i++)
        lyh[i].a=read(),lyh[i].b=read(),lyh[i].c=read();
    sort(lyh+1,lyh+n+1,cmp);
    for(int i=1;i<=n;){
        zjy[++m]=lyh[i];
        while(ch(zjy[m],lyh[i]))
            zjy[m].sz++,i++;
    }//注意去重
    cdq(1,m);for(int i=1;i<=m;i++)
        re[zjy[i].ans+zjy[i].sz-1]+=zjy[i].sz;
    for(int i=0;i<n;i++) printf("%d\n",re[i]);
    return 0;
}

分析时间复杂度:

  1. 仍然 \(\log n\) 层,每层处理 \(n\) 个值;
  2. 但是,由于有树状数组,所以时间复杂度还要多加一个 \(\log n\)。

所以总时间复杂度为 \(O(n\log^2n)\),十分的优秀(当然不排除神犇使用 \(KD-Tree\) 等其他算法的可能)。

四、时间维度

那么,假如问题如此修改呢?

  • 有一个序列 \(a\),我们会对它进行 \(q\) 次操作,有两种操作:
  1. 修改一个值
  2. 询问区间和

很明显,这是一个树状数组模板题,可以用它轻松解决。
但是,我们能不能用 \(CDQ\) 分治来解决这个问题呢?
假如我们将时间也看作一维,那么这道题就仍可以看作类似二维偏序问题,就转化为了 \(CDQ\) 分治。
再看下面这个问题(\([bzoj1176][Balkan2007]Mokia\) / \([bzoj2683]\) 简单题):

  • 维护一个 \(W\times W\) 的矩阵,每次操作可以增加某格子的权值,或询问某子矩阵的总权值。修改操作数 \(M<=160000\) ,询问数 \(Q<=10000,W<=2000000\)。

同样,将时间维理解成第一维,而且已经排序好了。
详细见代码:

#include<bits/stdc++.h>
#define N 2000005
#define M 800005
#define K 200005
using namespace std;
int n,tot,cnt,c[N],ans[K];
struct yh{int x,y,v,p,id,o;}a[M],tp[M];//o为问题种类
int cmp(yh a,yh b){
    return a.x!=b.x?a.x<b.x:a.y!=b.y?a.y<b.y:a.o<b.o;
}void ad(int x,int y,int z,int w){
    tot++;a[tot]={x,y,z,0,tot,w};
    if(w) a[tot].p=cnt;
}void add(int x,int k){for(;x<=n;x+=x&-x) c[x]+=k;}
int sum(int x){int re=0;for(;x;x-=x&-x) re+=c[x];return re;}
void cdq(int l,int r){
    if(l==r) return;int mid=(l+r)/2,lt=l,rt=mid+1;
    //处理答案
    for(int i=l;i<=r;i++){
        if(a[i].id<=mid&&!a[i].o) add(a[i].y,a[i].v);
        if(a[i].id>mid&&a[i].o) ans[a[i].p]+=a[i].v*sum(a[i].y);
    }//排序
    for(int i=l;i<=r;i++)
        if(a[i].id<=mid&&!a[i].o) add(a[i].y,-a[i].v);
    for(int i=l;i<=r;i++){
        if(a[i].id<=mid) tp[lt++]=a[i];
        else tp[rt++]=a[i];
    }for(int i=l;i<=r;i++) a[i]=tp[i];
    cdq(l,mid);cdq(mid+1,r);
    //这样写似乎更清晰?
}signed main(){
    cin>>n;while(1){
        int k,b,c,d,e;cin>>k;if(k>2) break;
        if(k==1) cin>>b>>c>>d,ad(b,c,d,0);
        else{
            cin>>b>>c>>d>>e;cnt++;
            ad(d,e,1,1);ad(b-1,c-1,1,1);
            ad(d,c-1,-1,1);ad(b-1,e,-1,1);
        }
    }sort(a+1,a+tot+1,cmp);cdq(1,tot);
    for(int i=1;i<=cnt;i++) cout<<ans[i]<<"\n";
    return 0;
}

五、练习

[Violet 3]天使玩偶(最好用树状数组维护最大值)
[Tjoi2016&Heoi2016]序列(\(CDQ+dp\))
祝大家好运!

标签:总结,树状,int,分治,mid,lt,CDQ,排序
From: https://www.cnblogs.com/chang-an-22-lyh/p/18004184/cdq_zj

相关文章

  • noip2023总结
    三年OI一场空,不开LL见祖宗我开LL了这仅仅是个总结noip2023游记难度:CSP-J<CSP-S<NOIp本人所获分数:CSP-J<CSP-S<NOIp看着好像没什么问题是吧,你细看,你再看可能基础还是不够扎实,就连教练都说我不是正常的人了平时对一些知识点掌握不够扎实,只会一点皮毛我还有一个问......
  • 2.2寒假每日总结24
    使用的HBuilderX版本:3.98Git插件已安装:项目结构如下:右击项目目录,在git命令中-》检查已修改,可以发现还是能检索到修改过的文件:文件是有修改过的,但是在上图中没有任何的修改标识,这些文件也没有添加到.gitignore配置中。二、问题解决......
  • 八上学期总结
    这个学期这么精彩,总结还是要写写的。。。就是比较碎碎念。。。OI第一次CSPCSP2022游记每周模拟赛成绩起伏很大。。。最高#9最低#19学到很多东西更加认识到我是蒟蒻这件事实stokthsjrswjorz和高一训练认识了MO&OI都厉害的学姐@AmyYang敲开心的MO......
  • 2024.2.2寒假每日总结24
    算法题:1686.石子游戏VI-力扣(LeetCode)最最简单的超级马里奥训练过程fromnes_py.wrappersimportJoypadSpaceimportgym_super_mario_brosfromgym_super_mario_bros.actionsimportSIMPLE_MOVEMENTimporttimefrommatplotlibimportpyplotaspltfromstable_basel......
  • selenium出现“element not interactable”问题总结
    “elementnotinteractable”问题根因:元素不可交互,可能的原因及解决方法如下所示:1、检查元素的定位(XPATH、CSS_SELECTOR内的内容)是否写正确2、代码中元素进行获取的时候查看是否已经加载出来,等待元素加载可以使用显式等待element= WebDriverWait(browser,20,0.5).until(EC.p......
  • 关于Nest.js循环引用问题的总结
    首先上代码 这个东东中,AuthService就是触及了循环依赖的东西(纯自学搞了半天才找出毛病),首先什么是循环依赖,唉!问题来了在某些文章是这样说的"Circulardependency"error¶偶尔你会发现在你的应用程序中很难避免circulardependencies。您需要采取一些步骤来帮助Nest解......
  • 2023 总结
    2023总结一、我做了什么?年初的时候,印象里是元宵后吧,来北京提前实习了;然后就是毕业设计吧,把在海康的时候写的链路分析工具改了一下作为毕设项目;然后就是本科毕业,虽然才过去了半年,但好像那已经是很久很久以前了的样子;然后去了一趟西藏,我觉得我应该去一些稍微特殊一点的地方,所......
  • 【pytest】Hook钩子函数完整API总结
    pytest的钩子函数有很多,通过钩子函数的学习可以了解到pytest在执行用例的每个阶段做什么事情,也方便后续对pytest二次开发学习。详细文档可以查看pytest官方文档https://docs.pytest.org/en/latest/reference.html#hooks钩子函数总结第一部分:setuptools引导挂钩要求足够早注......
  • 最长上升子序列总结
    这是最长上升子序列最基础的例子:给定一串数字32451那么他的最长上升子序列就是345其衍生问题为:求最长递减子序列、求正方向反方向最长递增/递减子序列求先上升后下降的最长子序列、求能完全覆盖整个序列的最小下降子序列个数求能完全覆盖整个序列的最小上升和下降......
  • 2023 Airtest 年终总结来了,大佬们速来围观!
    此文章来源于项目官方公众号:“AirtestProject”版权声明:允许转载,但转载必须保留原链接;请勿用作商业或者非法用途1、前言马上要进入2024年龙年春节了~,~让我们回顾一下2023年里大家与AirtestProject一起成长的痕迹,也快来看看,在2024年,AirtestProject会有什么新的功能~2、开源产......