先挂上个大佬讲解,sunyutian1998学长给我推荐的mlystdcall大佬的【教程】简易CDQ分治教程&学习笔记

  还有个B站小姐姐讲解的概念https://www.bilibili.com/video/av24295614?from=search&seid=4525696205028875575

  以下就是我在看完mlystdcall大佬的博客后,自己的一些思考。

  在学cdq分治的开始,我首先是把归并排序又看了一遍,归并排序也是一种分治的思想,把整个区间分成若干个区间处理,然后在合并。而cdq分治不同的地方就在于合并时,需要考虑左区间对右区间的影响。

  然后cdq分治一个比较简单的例子,归并排序求逆序对。求逆序对的话,数据量小我们直接可以暴力,而当数据量大时我们可以用线段树或者树状数组来维护,而当数据规模(也就比如给的数组中的数很大,每个都1e9)很大时,我们可以对数据进行离散化,然后再用线段树或者树状数组维护。当然我们也可以不用离散化,改用归并排序来处理这个问题,具体怎么做呢?

  首先,归并排序的话是先把每个区间分成[L,mid],[mid+1,R]两个区间,然后递归处理,这是分的过程。而当左右区间都处理好之后,这时我们就要把它们合并起来,归并排序就是简单的合并起来,而cdq分治就加上一个治的过程。因为我们是对逆序对,那么我们在合并的过程,对于L<=i<=mid,mid+1<=j<=R,有a[i]>a[j]的话,那么左区间在[i,mid]的数就都大于a[j],这时产生的逆序对就是mid-i+1个。也就是cdq分治思想中的

合并两个子问题,同时考虑到[L,M]内的修改对[M+1,R]内的查询产生的影响。即,用左边的子问题帮助解决右边的子问题。

Ultra-QuickSortOpenJ_Bailian - 2299

 #include<cstdio>
const int N=;
int a[N],temp[N];
long long ans=;
void merge(int l,int r)
{
if(l==r)
return ;
int m=(l+r)>>;
merge(l,m);
merge(m+,r);
//递归处理好左右区间后,左右区间都已经有序
int i=l,j=m+,k=l;
while(i<=m&&j<=r)
{
if(a[i]<=a[j])
temp[k++]=a[i++];
else
{
ans+=m-i+;
//如果a[i]>a[j]那么包括后面的m-i+1个数都会与a[j]产生逆序对
temp[k++]=a[j++];
}
}
while(i<=m)
temp[k++]=a[i++];
while(j<=r)
temp[k++]=a[j++];
for(i=l;i<=r;i++)
a[i]=temp[i];
}
int main()
{
int n;
while(~scanf("%d",&n)&&n)
{
for(int i=;i<n;i++)
scanf("%d",&a[i]);
ans=;
merge(,n-);
printf("%lld\n",ans);
}
return ;
}

归并排序求逆序对

  如果能明白归并排序求逆序对,那么对cdq分治的再深入,也没有太大问题。

  CDQ最基础的应用二维偏序问题

  给定N个有序对(a,b),求对于每个(a,b),满足a2<ab2<b的有序对(a2,b2)有多少个。

  在上面求逆序对中,其实我们就有涉及到二维偏序了,对于每个位置和相应的数视为有序对(位置i,数a[i])的话,那么逆序对,我们求的就是,满足位置i<位置j且a[i]>a[j]的有序对有多少个。由此扩展到二维偏序就是,我们可以先让a有序消除a元素的影响,然后合并问题时就是考虑b,也就相当于求顺序对。照着这个思想,我们可以去处理带修改和查询的问题。

  二维偏序问题的拓展

  给定一个N个元素的序列a,初始值全部为0,对这个序列进行以下两种操作:

  操作1:格式为1 x k,把位置x的元素加上k(位置从1标号到N)。

  操作2:格式为2 x y,求出区间[x,y]内所有元素的和。

  线段树或者树桩数组裸题,但用CDQ分治怎么做呢,我们把每个操作的发生时间,和操作位置视为有序队(操作时间,操作位置)的话,首先操作时间是有序的,然后就是对操作位置由小到大合并,这样当左边区间的位置小于右边时,我们就可以记录一个更新的前缀和,当左边区间的位置大于右边时,我们可以统计一个更新的值对查询的影响。

  那么说的话也就是,相应的值和操作类型的话我们需要用附加信息表示出来,op为1就代表着更新(加上k的操作),op为2就代表着查询操作中的减去[1,x-1]中的值,op为3就代表着查询操作中加上[1,y]中的值,op1和2合起来就是一整个查询操作。定义了这些之后我们就来考虑一下合并区间时,左右区间的影响。

  首先因为第一维操作发生时间已经有序了,那么左边区间的发生时间就比右边区间早,这样的话右边区间的更新对左边区间的查询就没有影响(因为左边区间的查询发生时,右边区间的更新还没发生呢),由此也有左边区间的更新对右边的查询就有影响,而相同的区间内的更新对查询的影响呢,因为它们已经合成一个区间了,那么它们的影响在合并时已经计算过了,所以我们最终要统计的就只是左边区间的更新对右边查询的影响。

  在具体的操作上就是,[L,M],[M+1,R],L<=i<=M,M+1<=j<=R这两个区间合并时,当a[i].pos<a[j].pos时,如果是a[i].op为1就记录左边区间更新的前缀和,而当a[i].pos>a[j].pos时,a[j].op为2就减去更新的前缀和,为3就加上更新的前缀和。而当a[i].pos=a[j].pos时,更新的优先级比查询高,因为我们前面所说的合并时就只需统计左边区间的更新对右边的查询就有影响,如果左区间的更新和右区间查询在同一个位置,应该先把左区间的更新算上。

  最后一个注意的地方就是,如果有初始值的话直接使初始值视为一个更新操作。

 #include<cstdio>
typedef long long ll;
const int N=,M=,Q=(M<<)+N;
struct Nop{
int pos,op,val;//更新的val代表加上的k,查询的val代表它的查询编号
friend bool operator <(const Nop &n1,const Nop &n2){
return n1.pos==n2.pos ? n1.op<n2.op : n1.pos<n2.pos;
}//位置由小到大合并,相同位置更新优先
}P[Q],temp[Q];//因为初始值视为更新操作,而查询操作分为两个
ll ans[M<<];
int pn,qn;//pn总操作编号,qn总查询编号
void cdq(int l,int r)
{
if(l==r)
return ;
int m=(l+r)>>;
cdq(l,m);
cdq(m+,r);
ll sum=;
int i=l,j=m+,k=l;
while(i<=m&&j<=r)
{
if(P[i]<P[j])
{
if(P[i].op==)//左边统计更新前缀和
sum+=1ll*P[i].val;
temp[k++]=P[i++];
}
else
{
if(P[j].op==)//右边处理查询
ans[P[j].val]-=sum;
else if(P[j].op==)
ans[P[j].val]+=sum;
temp[k++]=P[j++];
}
}
while(i<=m)//右边区间没有查询了,没必要继续统计前缀和
temp[k++]=P[i++];
while(j<=r)
{
if(P[j].op==)
ans[P[j].val]-=sum;
else if(P[j].op==)
ans[P[j].val]+=sum;
temp[k++]=P[j++];
}
for(i=l;i<=r;i++)
P[i]=temp[i];
}
int main()
{
int n,m,op,x,y;
while(~scanf("%d%d",&n,&m))
{
pn=qn=;
for(int i=;i<=n;i++)
{
P[pn].pos=i,P[pn].op=;
scanf("%d",&P[pn++].val);
}
for(int i=;i<=m;i++)
{
scanf("%d%d%d",&op,&x,&y);
if(op==)
P[pn].op=,P[pn].pos=x,P[pn++].val=y;
else//把查询拆分成两个操作
{
P[pn].op=,P[pn].pos=x-,P[pn++].val=qn;
P[pn].op=,P[pn].pos=y,P[pn++].val=qn++;
}
}
cdq(,pn-);
for(int i=;i<qn;i++)
printf("%lld\n",ans[i]);
}
return ;
}

分分治治

  如果你能耐住性子坚持到现在,那么不用我说,也可以发现,CDQ分治的前提是,更新对查询的贡献独立,更新彼此间互不影响,而且的允许离线算法,所有的操作都得先记录下来再处理。看起来,CDQ的时间和空间复杂度都不怎么低,而且CDQ分治做的线段树和树状数组好像都能做,那为什么还需要CDQ分治呢?这也是我们前面说的,当数据规模很大时,无论是空间和时间,线段树和树状数组都吃不消,而CDQ分治取决于数据量不取决于数据规模。此外,当更复杂一点,三维偏序,四维偏序等问题的时候,线段树或者树状数组就得树套树之类的,处理起来非常麻烦,而CDQ分治就可以顶替复杂的高级数据结构,能对问题起到一个降维的效果。

  至于更深的三维,四维问题,当做到相应的题时再更相应的内容。希望我的博客能对你学习CDQ分治有所帮助。

  最后,路不只一条,解决问题的方法也不只一种,当一种不行时就去尝试其他的。

CDQ分治学习思考的更多相关文章

  1. 初学cdq分治学习笔记(可能有第二次的学习笔记)

    前言骚话 本人蒟蒻,一开始看到模板题就非常的懵逼,链接,学到后面就越来越清楚了. 吐槽,cdq,超短裙分治....(尴尬) 正片开始 思想 和普通的分治,还是分而治之,但是有一点不一样的是一般的分治在 ...

  2. CDQ分治学习笔记

    数据结构中的一块内容:$CDQ$分治算法. $CDQ$显然是一个人的名字,陈丹琪(NOI2008金牌女选手) 这种离线分治算法被算法界称为"cdq分治" 我们知道,一个动态的问题一 ...

  3. cdq分治学习

    看了stdcall大佬的博客 传送门: http://www.cnblogs.com/mlystdcall/p/6219421.html 感觉cdq分治似乎很多时候都要用到归并的思想

  4. [摸鱼]cdq分治 && 学习笔记

    待我玩会游戏整理下思绪(分明是想摸鱼 cdq分治是一种用于降维和处理对不同子区间有贡献的离线分治算法 对于常见的操作查询题目而言,时间总是有序的,而cdq分治则是耗费\(O(logq)\)的代价使动态 ...

  5. CDQ分治学习笔记(三维偏序题解)

    首先肯定是要膜拜CDQ大佬的. 题目背景 这是一道模板题 可以使用bitset,CDQ分治,K-DTree等方式解决. 题目描述 有 nn 个元素,第 ii 个元素有 a_iai​.b_ibi​.c_ ...

  6. 三维偏序[cdq分治学习笔记]

    三维偏序 就是让第一维有序 然后归并+树状数组求两维 cdq+cdq不会 告辞 #include <bits/stdc++.h> // #define int long long #def ...

  7. 学习笔记 | CDQ分治

    目录 前言 啥是CDQ啊(它的基本思想) 例题 后记 参考博文 前言 博主太菜了 学习快一年的OI了 好像没有什么会的算法 更寒碜的是 学一样还不精一样TAT 如有什么错误请各位路过的大佬指出啊感谢! ...

  8. cdq分治入门and持续学习orz

    感觉cdq分治是一个很有趣的算法 能将很多需要套数据结构的题通过离线来做 目前的一些微小的理解 在一般情况下 就像求三维偏序xyz 就可以先对x排序 然后分治 1 cdq_x(L,M) ; 2 提取出 ...

  9. 【教程】简易CDQ分治教程&学习笔记

    前言 辣鸡蒟蒻__stdcall终于会CDQ分治啦!       CDQ分治是我们处理各类问题的重要武器.它的优势在于可以顶替复杂的高级数据结构,而且常数比较小:缺点在于必须离线操作. CDQ分治的基 ...

随机推荐

  1. h5中的结构元素header、nav、article、aside、section、footer介绍

    结构元素不具有任何样式,只是使页面元素的的语义更加明确. header元素 header元素是一种具有引导和导航作用的的结构元素,该元素可以包含所有通常放在页面头部的内容.header元素通常用来放置 ...

  2. python中while循环打印星星的四种形状

    在控制台连续输出五行*,每一行星号数量一次递增 * ** *** **** ***** #1.定义一个行计数器 row = 1 while row <= 5: #定义一个列计数器 col = 1 ...

  3. 句子反转——牛客刷题(java)

    题目描述: 给定一个句子(只包含字母和空格), 将句子中的单词位置反转,单词用空格分割, 单词之间只有一个空格,前后没有空格. 比如: (1) “hello xiao mi”-> “mi xia ...

  4. Java Web DNS域名解析

    一.什么是DNS DNS(Domain Name System,域名系统),因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串 ...

  5. 这是一个用于判断IE浏览器版本的紧凑脚本

    这是一个用于判断IE浏览器版本的紧凑脚本IE浏览器,不管它们是什么版本,总是与Web标准有些不兼容.对于编码人员来说,这很困难.为了考虑IE的兼容性,不管它是写CSS还是写JS,IE通常都会被特殊处理 ...

  6. 常见DML语句汇总

    DML操作是指对数据中表记录的操作,主要包括表记录的插入(insert).更新(update).删除(delete)和查询(select),是开发人员日常使用最频繁的操作,下面依次对它们进行介绍. ( ...

  7. TVM调试指南

    1. TVM安装 这部分之前就写过,为了方便,这里再复制一遍. 首先下载代码 git clone --recursive https://github.com/dmlc/tvm 这个地方最好使用--r ...

  8. JVM学习(三):垃圾回收算法

    局部性原理和分代回收思想 大学学习操作系统或者计算机组成原理的时候都提到一个重要概念,叫局部性原理. 局部性原理是指CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小 ...

  9. 在已有lnmp环境的基础上安装PHP7

    Centos7.6系统 已经安装lnmp一键环境 想装个apache跑php7, apache安装在这 https://www.cnblogs.com/lz0925/p/11227063.html 要 ...

  10. java_day09_GUI事件

    第九章:GUI事件 1.AWT事件模型概述 使用AWT或者Swing中的容器.组件和布局管理器就可以构建出图形界面,但是这时候该界面还并不能和用户进行交换,因为图形界面中的组件还没有添加事件监听器,所 ...