序言

\(CDQ\) 分治和整体二分都是基于分治的思想,把复杂的问题拆分成许多可以简单求的解子问题。但是这两种算法必须离线处理,不能解决一些强制在线的题目。不过如果题目允许离线的话,这两种算法能把在线解法吊起来打(如树套树)。


前置知识:分治

个人觉得分治的经典例子便是归并排序。

大家都知道,归并排序就是每次将区间 \([l,r]\) 拆分成 \([l,mid]\) 和 \([mid+1,r]\),然后再 \(O(n)\) 合并两个有序数组,再将 \([l,r]\) 的答案传到上一层去。

那么我们可以得到 \(T(n)=2\times T(\frac n2)+n\)

因为这样递归层数不会超过 \(\log n\) 层,所以时间复杂度为 \(O(n\log n)\)

void mergesort(int l,int r){
if(l == r) return ;
int mid=(l+r)>>1;
mergesort(l,mid);
mergesort(mid+1,r);
int p=l,q=mid+1,cnt=l;
while(p<=mid&&q<=r){
if(a[p]<a[q]) t[cnt++]=a[p++];
else t[cnt++]=a[q++];
}
while(p<=mid) t[cnt++]=a[p++];
while(q<=r) t[cnt++]=a[q++];
for(int i=l;i<=r;i++) a[i]=t[i];
}

归并排序的另一个用途:求一个序列的逆序对。

我们发现在合并两个有序数组的时候,若 a[p]>a[q] 的时候,那么 \(a[p]\sim a[mid]\) 的数一定比 \(a[q]\) 大,所以我们在归并排序的过程中加入一句 if(a[p]>a[q]) ans+=mid-l+1

这种思想在分治中非常有用。


\(CDQ\) 分治

讲 \(CDQ\) 分治的时候就少不了经典的多维偏序问题了。

二维偏序问题

给定 \(n\) 个元素,第 \(i\) 个元素有 \(a_i\)、\(b_i\) 两个属性,设 \(f(i)\) 表示满足 \(a_j\leq a_i\) 且 \(b_j\leq b_i\) 的 \(j\) 的数量。

对于 \(d\in [0,n]\),求满足 \(f(i)=d\) 的数量。

首先,我们可以把两个元素抽象成一个点 \((a,b)\),那么我们就是求一个矩形中有多少个点。

比如我们要求这个矩形内有多少个点:

首先我们可以按照 \(x\) 轴排个序,发现矩形右边的点已经不在答案的贡献里了。那么 \(f(i)\) 就是在排序后的数组中找 \(1\sim i-1\) 中有几个元素 \(b\) 比 \(b_i\) 小。

那么我们直接树状数组即可,时间复杂度 \(O(n\log n)\)

例题:HDU1541 Stars

#include <bits/stdc++.h>
#define lowbit(x) ((x)&(-(x)))
using namespace std;
const int maxn=100000+10;
int n,c[maxn],ans[maxn]; struct Stars{
int x,y;
}a[maxn]; bool cmp(Stars a,Stars b){
if(a.x!=b.x) return a.x<b.x;
return a.y<b.y;
} inline int read(){
register int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
return (f==1)?x:-x;
}
void add(int x,int y){
for(;x<maxn;x+=lowbit(x)) c[x]+=y;
}
int sum(int x){
int ans=0;
for(;x;x-=lowbit(x)) ans+=c[x];
return ans;
} int main()
{
n=read();
for(int i=1;i<=n;i++)
a[i].x=read(),a[i].y=read();
sort(a+1,a+n+1,cmp);
int now;
for(int i=1;i<=n;i++){
now=sum(a[i].y+1);
ans[now]++;
add(a[i].y+1,1);
}
for(int i=0;i<n;i++)
printf("%d\n",ans[i]);
return 0;
}

自己的题目:【模板】二维偏序

三维偏序问题

其实三维偏序就是在二维偏序上加一维而已。

我们先按每个元素的属性 \(a\) 排个序,然后第二维用归并排序,第三维用树状数组。

我们在归并的时候考虑 \([l,mid]\) 对 \([mid+1,r]\) 的贡献。因为我们已经按属性 \(a\) 排过序了,所以在排序属性 \(b\) 的时候,无论属性 \(a\) 怎么被打乱,\([mid+1,r]\) 所有元素的属性 \(a\) 一定不小于 \([l,mid]\) 中所有元素的属性 \(a\),所以第二维是成立的。

在满足前两维都是有序的时候,类似二维偏序的解法,我们可以用树状数组来统计答案了。

在【模板】三维偏序中,\(a_j\leq a_i\) 且 \(b_j\leq b_i\) 且 \(c_j\leq c_i\) 中是有取等号的,所以我们需要对元素进行去重,最后统计最终的答案,时间复杂度 \(O(n\log^2 n)\)

例题:【模板】三维偏序

#include <bits/stdc++.h>
#define lowbit(x) ((x)&(-(x)))
using namespace std;
const int maxn=100000+10;
int n,m,c[maxn<<1],ans[maxn],cnt; struct Element{
int a,b,c,w,f;
}e[maxn],t[maxn]; bool cmp(Element x,Element y){
if(x.a!=y.a) return x.a<y.a;
if(x.b!=y.b) return x.b<y.b;
return x.c<y.c;
} inline int read(){
register int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
return (f==1)?x:-x;
} void update(int x,int y){
for(;x<=m;x+=lowbit(x)) c[x]+=y;
}
int sum(int x){
int ans=0;
for(;x;x-=lowbit(x)) ans+=c[x];
return ans;
} void CDQ(int l,int r){
int mid=(l+r)>>1;
if(l==r) return ;
CDQ(l,mid);CDQ(mid+1,r);
int p=l,q=mid+1,tot=l;
while(p<=mid&&q<=r){
if(e[p].b<=e[q].b) update(e[p].c,e[p].w),t[tot++]=e[p++];
else e[q].f+=sum(e[q].c),t[tot++]=e[q++];
}
while(p<=mid) update(e[p].c,e[p].w),t[tot++]=e[p++];
while(q<=r) e[q].f+=sum(e[q].c),t[tot++]=e[q++];
for(int i=l;i<=mid;i++) update(e[i].c,-e[i].w);
for(int i=l;i<=r;i++) e[i]=t[i];
} int main()
{
n=read();m=read();
for(int i=1;i<=n;i++)
e[i].a=read(),e[i].b=read(),e[i].c=read(),e[i].w=1;
sort(e+1,e+n+1,cmp);
cnt=1;
for(int i=2;i<=n;i++){
if(e[i].a==e[cnt].a&&e[i].b==e[cnt].b&&e[i].c==e[cnt].c) e[cnt].w++;
else e[++cnt]=e[i];
}
CDQ(1,cnt);
for(int i=1;i<=cnt;i++) ans[e[i].f+e[i].w-1]+=e[i].w;
for(int i=0;i<n;i++) printf("%d\n",ans[i]);
return 0;
}

四维偏序问题

四维偏序就比较恶心了,需要 \(CDQ\) 套 \(CDQ\) 套 树状数组

怎么叫 \(CDQ\) 套 \(CDQ\) 呢?

我们可以在第二维 \(CDQ\) 的时候,记下那些元素在左区间还在右区间。在第三维 \(CDQ\) 的时候保持前两维的有序时,加一个树状数组,时间复杂度 \(O(n\log^3 n)\)

例题:HDU5126 stars

不过这道题带修改,还要判断是什么操作。这里要保证时间的有序和 \(x,y,z\) 三维的有序,所以是四维偏序。

并且求在 \((x_1,y_1,z_1)\) 和 \((x_2,y_2,z_2)\) 的点对个数时要将这些限制拆成八个询问,容斥一下就好了。

#include <bits/stdc++.h>
#define lowbit(x) ((x)&(-(x)))
using namespace std;
const int maxn=400000+10;
int n,m,mp[maxn],c[maxn],ans[maxn],tot; struct Element{
int op,x,y,z,w,id,flag;
}e[maxn],t[maxn],d[maxn]; inline int read(){
register int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
return (f==1)?x:-x;
}
void add(int x,int y){
for(;x<maxn;x+=lowbit(x)) c[x]+=y;
}
int sum(int x){
int ans=0;
for(;x;x-=lowbit(x)) ans+=c[x];
return ans;
} void CDQ3d(int l,int r){
if(l==r) return ;
int mid=(l+r)>>1;
CDQ3d(l,mid);CDQ3d(mid+1,r);
int p=l,q=mid+1,cnt=l;
while(p<=mid&&q<=r){
if(t[p].y<=t[q].y){
if(t[p].op==1&&t[p].flag==0)
add(t[p].z,1);
d[cnt++]=t[p++];
}
else {
if(t[q].op==2&&t[q].flag==1)
ans[t[q].id]+=t[q].w*sum(t[q].z);
d[cnt++]=t[q++];
}
}
while(p<=mid){
if(t[p].op==1&&t[p].flag==0)
add(t[p].z,1);
d[cnt++]=t[p++];
}
while(q<=r){
if(t[q].op==2&&t[q].flag==1)
ans[t[q].id]+=t[q].w*sum(t[q].z);
d[cnt++]=t[q++];
}
for(int i=l;i<=mid;i++){
if(t[i].op==1&&t[i].flag==0)
add(t[i].z,-1);
}
for(int i=l;i<=r;i++) t[i]=d[i];
} void CDQ2d(int l,int r){
if(l==r) return ;
int mid=(l+r)>>1;
CDQ2d(l,mid);CDQ2d(mid+1,r);
int p=l,q=mid+1,cnt=l;
while(p<=mid&&q<=r){
if(e[p].x<=e[q].x){
t[cnt++]=e[p++];t[cnt-1].flag=0;
}
else {
t[cnt++]=e[q++];t[cnt-1].flag=1;
}
}
while(p<=mid){
t[cnt++]=e[p++];t[cnt-1].flag=0;
}
while(q<=r){
t[cnt++]=e[q++];t[cnt-1].flag=1;
}
for(int i=l;i<=r;i++) e[i]=t[i];
CDQ3d(l,r);
} int main()
{
int T=read();
while(T--){
memset(ans,0,sizeof(ans));
m=read();tot=0;
int op,x1,y1,z1,x2,y2,z2,t=0;
for(int i=1;i<=m;i++){
op=read();
if(op==1){
x1=read(),y1=read(),z1=read();
e[++tot]=(Element){1,x1,y1,z1,1,tot,0};
}
else {
x1=read(),y1=read(),z1=read(),x2=read(),y2=read(),z2=read();
t++;
e[++tot]=(Element){2,x2,y2,z2,1,t,0};
e[++tot]=(Element){2,x1-1,y2,z2,-1,t,0};
e[++tot]=(Element){2,x2,y1-1,z2,-1,t,0};
e[++tot]=(Element){2,x2,y2,z1-1,-1,t,0};
e[++tot]=(Element){2,x1-1,y1-1,z2,1,t,0};
e[++tot]=(Element){2,x1-1,y2,z1-1,1,t,0};
e[++tot]=(Element){2,x2,y1-1,z1-1,1,t,0};
e[++tot]=(Element){2,x1-1,y1-1,z1-1,-1,t,0};
}
}
for(int i=1;i<=tot;i++) mp[i]=e[i].z;
sort(mp+1,mp+tot+1);
int cnt=unique(mp+1,mp+tot+1)-mp-1;
for(int i=1;i<=tot;i++) e[i].z=lower_bound(mp+1,mp+cnt+1,e[i].z)-mp;
CDQ2d(1,tot);
for(int i=1;i<=t;i++) printf("%d\n",ans[i]);
}
return 0;
}

更高的偏序问题就要用到 \(bitset\) 啦,时间复杂度 \(O(\frac{n^2}{32})\),今天就不讲了。

习题:[CQOI2011]动态逆序对

这道题离线做法就是化为 \(Time_i<Time_j\) 且 \(Pos_i<Pos_j\) 且 \(Val_i<Val_j\),然后用 \(CDQ\) 分治解决经典的三维偏序问题


整体二分

整体二分类似于一些决策单调性的分治,可以解决诸多区间第 \(k\) 小或区间第 \(k\) 大的问题。

整体二分 solve(l,r,L,R) 表示答案在 \([l,r]\) 中,与操作 \([L,R]\) 有关(操作 \([L,R]\) 不一定对应原来 \([L,R]\) 的操作)

我们就拿静态区间第 \(k\) 小来说好了。如果原序列的数 \(\leq mid\),那么就在树状数组中对应位置 \(+1\)。如果碰到询问操作,那么查询询问区间 \([ql,qr]\) 的值相当于查询了区间中值在 \([l,mid]\) 的个数,如果个数 \(\leq k\),那么答案在 \([mid+1,r]\) 中,那么把 \(k\) 减掉对应的个数,把操作分到右区间。否则答案在 \([l,mid]\) 中,把操作分到左区间。

如果 \(l=r\),那么直接把 \(ans\) 记录一下就好了。时间复杂度 \(O(n\log^2 n)\)

不过我刚刚想到,如果将答案离散化一下,常数理论上会小下很多。读者有兴趣可以实现一下。

例题:【模板】可持久化线段树 1(主席树)

#include <bits/stdc++.h>
#define lowbit(x) ((x)&(-(x)))
using namespace std;
const int maxn=200000+10;
const int inf=1e9;
int n,m,a[maxn],c[maxn],ans[maxn],cnt; struct Query{
int l,r,k,op,id;
}q[maxn<<1],q1[maxn<<1],q2[maxn<<1]; inline int read(){
register int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
return (f==1)?x:-x;
}
void add(int x,int y){
for(;x<=n;x+=lowbit(x)) c[x]+=y;
}
int sum(int x){
int ans=0;
for(;x;x-=lowbit(x)) ans+=c[x];
return ans;
} void solve(int l,int r,int L,int R){
if(L>R) return ;
if(l==r){
for(int i=L;i<=R;i++)
if(q[i].op==2) ans[q[i].id]=l;
return ;
}
int mid=(l+r)>>1,cnt1=0,cnt2=0,x;
for(int i=L;i<=R;i++){
if(q[i].op==1){
if(q[i].l<=mid) q1[++cnt1]=q[i],add(q[i].id,q[i].r);
else q2[++cnt2]=q[i];
}
else {
x=sum(q[i].r)-sum(q[i].l-1);
if(q[i].k<=x) q1[++cnt1]=q[i];
else q[i].k-=x,q2[++cnt2]=q[i];
}
}
for(int i=1;i<=cnt1;i++)
if(q1[i].op==1) add(q1[i].id,-q1[i].r);
for(int i=1;i<=cnt1;i++) q[L+i-1]=q1[i];
for(int i=1;i<=cnt2;i++) q[L+i+cnt1-1]=q2[i];
solve(l,mid,L,L+cnt1-1);
solve(mid+1,r,L+cnt1,R);
} int main()
{
n=read(),m=read();
int l,r,k;
for(int i=1;i<=n;i++) a[i]=read(),q[++cnt]=(Query){a[i],1,0,1,i};
for(int i=1;i<=m;i++) l=read(),r=read(),k=read(),q[++cnt]=(Query){l,r,k,2,i};
solve(-inf,inf,1,cnt);
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
return 0;
}

那么动态区间第 \(k\) 小带修改操作怎么搞呢?

我们可以把原来的减掉再加上后来的,然后跑一遍整体二分就好了。

例题:Dynamic Rankings

#include <bits/stdc++.h>
#define lowbit(x) ((x)&(-(x)))
using namespace std;
const int maxn=200000+10;
const int inf=1e9;
int n,m,a[maxn],c[maxn],ans[maxn],cnt,tot; struct Query{
int l,r,k,id,op;
}q[maxn*3],q1[maxn*3],q2[maxn*3]; inline int read(){
register int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
return (f==1)?x:-x;
}
void add(int x,int y){
for(;x<=n;x+=lowbit(x)) c[x]+=y;
}
int sum(int x){
int ans=0;
for(;x;x-=lowbit(x)) ans+=c[x];
return ans;
} void solve(int l,int r,int L,int R){
if(L > R) return ;
if(l == r){
for(int i=L;i<=R;i++)
if(q[i].op==2) ans[q[i].id]=l;
return ;
}
int mid=(l+r)>>1,cnt1=0,cnt2=0,x;
for(int i=L;i<=R;i++){
if(q[i].op==1){
if(q[i].l <= mid) q1[++cnt1]=q[i],add(q[i].id,q[i].r);
else q2[++cnt2]=q[i];
}
else {
x=sum(q[i].r)-sum(q[i].l-1);
if(q[i].k <= x) q1[++cnt1]=q[i];
else q[i].k-=x,q2[++cnt2]=q[i];
}
}
for(int i=1;i<=cnt1;i++)
if(q1[i].op==1) add(q1[i].id,-q1[i].r);
for(int i=1;i<=cnt1;i++) q[L+i-1]=q1[i];
for(int i=1;i<=cnt2;i++) q[L+i+cnt1-1]=q2[i];
solve(l,mid,L,L+cnt1-1);
solve(mid+1,r,L+cnt1,R);
} int main()
{
n=read(),m=read();
int l,r,k;char op;
for(int i=1;i<=n;i++) a[i]=read(),q[++cnt]=(Query){a[i],1,0,i,1};
for(int i=1;i<=m;i++){
op=getchar();
while(!isalpha(op)) op=getchar();
if(op=='Q') l=read(),r=read(),k=read(),q[++cnt]=(Query){l,r,k,++tot,2};
else l=read(),r=read(),q[++cnt]=(Query){a[l],-1,0,l,1},q[++cnt]=(Query){a[l]=r,1,0,l,1};
}
solve(-inf,inf,1,cnt);
for(int i=1;i<=tot;i++) printf("%d\n",ans[i]);
return 0;
}

习题:[ZJOI2013]K大数查询

我们把这个区间插入变成在线段树上区间增减,然后查询排名就相当于查询区间和。

不过线段树清空的时候直接再加一个懒标记在 \(pushdown\) 中下传就好了,不用像树状数组一样把原来加上的值减掉。

[学习笔记]CDQ分治和整体二分的更多相关文章

  1. CDQ分治与整体二分学习笔记

     CDQ分治部分 CDQ分治是用分治的方法解决一系列类似偏序问题的分治方法,一般可以用KD-tree.树套树或权值线段树代替. 三维偏序,是一种类似LIS的东西,但是LIS的关键字只有两个,数组下标和 ...

  2. [学习笔记] CDQ分治&整体二分

    突然诈尸.png 这两个东西好像都是离线骗分大法... 不过其实这两个东西并不是一样的... 虽然代码长得比较像 CDQ分治 基本思想 其实CDQ分治的基本思想挺简单的... 大概思路就是长这样的: ...

  3. [学习笔记] CDQ分治 从感性理解到彻底晕菜

    最近学了一种叫做CDQ分治的东西...用于离线处理一系列操作与查询似乎跑得很快233 CDQ的名称似乎源于金牌选手陈丹琦 概述: 对于一坨操作和询问,分成两半,单独处理左半边和处理左半边对于右半边的影 ...

  4. CDQ分治与整体二分小结

    前言 这是一波强行总结. 下面是一波瞎比比. 这几天做了几道CDQ/整体二分,感觉自己做题速度好慢啊. 很多很显然的东西都看不出来 分治分不出来 打不出来 调不对 上午下午晚上的效率完全不一样啊. 完 ...

  5. 学习笔记 | CDQ分治

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

  6. 学习笔记——CDQ分治

    再次感谢这位大佬的博客:https://www.cnblogs.com/ljc20020730/p/10395866.html CDQ分治,是一种在分治合并中计算前面值对后面答案的贡献的一种算法.今天 ...

  7. 技巧专题3(cdq分治、整体二分等)

    cdq分治与整体二分 cdq来源于2008年国家集训队作业陈丹琦(雅礼巨佬),用一个log的代价完成从静态到动态(很多时候是减少时间那一维的). 对于一个时间段[L, R],我们取mid = (L + ...

  8. 算法笔记--CDQ分治 && 整体二分

    参考:https://www.luogu.org/blog/Owencodeisking/post-xue-xi-bi-ji-cdq-fen-zhi-hu-zheng-ti-er-fen 前置技能:树 ...

  9. Codeforces 1039D You Are Given a Tree [根号分治,整体二分,贪心]

    洛谷 Codeforces 根号分治真是妙啊. 思路 考虑对于单独的一个\(k\)如何计算答案. 与"赛道修建"非常相似,但那题要求边,这题要求点,所以更加简单. 在每一个点贪心地 ...

随机推荐

  1. 关于js的function.来自百度知道的回答,学习了.

    在js中,创建一个函数对象的语法是var myFunction = new Function(arg1,…,agrN, body);其中,该函数对象的N个参数放在 函数主体参数body的前面,即函数主 ...

  2. Linux日志文件总管——logrotate

    日志文件包含了关于系统中发生的事件的有用信息,在排障过程中或者系统性能分析时经常被用到.对于忙碌的服务器,日志文件大小会增长极快,服务器会很快消耗磁盘空间,这成了个问题.除此之外,处理一个单个的庞大日 ...

  3. ManageEngine OMP帮助台委派

  4. TP5在lnmp环境中不能重写的问题

    说到坑,这个问题困扰了我一两天时间,本地可以,线上环境检查了好久. 基本检查的地方有几个了,首先就是nginx下面的重写配置,这个大家在网上都能搜到,至于定义的变量和配置路径,修改一下即可. 还有就是 ...

  5. git常规命令

    $ mkdir filename 创建一个空目录 $ git init 把这个目录变成Git可以管理的仓库 $ pwd 用于显示当前目录 $ cat <file> 查看文件内容 $ git ...

  6. Arria10_emif

    DDR3 由排(Rank),体(Bank),行(Row),列(Column)组成的四维结构. Arria10是第一批支持ddr4的altera Arria10与老器件相比的新结构 (1)  更多的硬( ...

  7. 微信官方api & 非官方api

    1.微信公众平台开发者文档 http://mp.weixin.qq.com/wiki/home/index.html 2.微信公众平台 https://mp.weixin.qq.com/ 3.第三方a ...

  8. Warning:The /usr/local/mysql/data directory is not owned by the 'mysql' or '_mysql'

    Mac OS X的升级或其他原因可能会导致MySQL启动或开机自动运行时   在MySQL操作面板上会提示“Warning:The /usr/local/mysql/data directory is ...

  9. Java数据库技术

    JDBC即Java数据库连接     是接口,用于执行SQL语句,包含Java写的类和界面.几乎可以把SQL传给任何数据库,不用单独编写SQL.     用处,1是与数据库建立连接,2是向数据库发送S ...

  10. web-day1

    第1章   WEB01-HTML篇 今日任务 网站信息页面案例 网站图片信息页面案例 网站友情链接页面案例 网站首页案例 网站注册页面案例 网站后台页面案例 教学导航 教学目标 了解什么是标记语言 了 ...