分治挑战数据结构——小记整体二分和CDQ分治

本文详细介绍了整体二分与CDQ分治两种算法的原理及应用实例,包括算法思想、代码实现和典型例题解析。适用于求解多维偏序问题和特定类型的查询操作。

整体二分

例题:bzoj3110/洛谷P3332
数据结构解决:线段树套splay
整体二分,顾名思义,就是把所有的东西拿来一起二分。在这道题里我们还要开一棵线段树。
1.把所有添加操作和询问顺序存进Q中。
2.二分一个答案,顺序处理所有操作
  2-1.对于查询操作,我们在线段树查询一下区间和(代表在这个区间里,小于mid的数的个数),依据这个个数进行分类。代码表示如下:

if(Q[i].bj==2) {
    LL kl=query(Q[i].l,Q[i].r,1,n,1);//在线段树里查询
    if(kl>=Q[i].v) Q1[++t1]=Q[i];
    else Q2[++t2]=Q[i],Q2[t2].v-=kl;//注意这个减少kl,思想类似于物理里的“转化参考系”(雾
}

  2-2.对于添加操作
    2-2-1.如果要添加的值小于等于mid,我们就在线段树里更新区间和,即增加“这个区间里,小于mid的数的个数”,并把该操作分进Q1类中。
    2-2-2.否则将该操作分进Q2类中。
3.清除在线段是里更新区间和后造成的影响
4.将Q1和Q2重新合并进Q中
5.递归进行二分(同时对操作也进行了二分)
如果哪一步不懂就看代码吧。

#include<bits/stdc++.h>
using namespace std;
int read() {
    int q=0,w=1;char ch=' ';
    while(ch!='-'&&(ch<'0'||ch>'9')) ch=getchar();
    if(ch=='-') w=-1,ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return w*q;
}
#define LL long long
const int N=50005;
int n,m,qs,laz[N<<2],ans[N];LL sum[N<<2];
struct node{int l,r,bj,id;LL v;}Q[N],Q1[N],Q2[N];
void pd(int i,int s,int t) {
    int l=(i<<1),r=(i<<1)|1,mid=(s+t)>>1;
    sum[l]+=laz[i]*(mid-s+1),sum[r]+=laz[i]*(t-mid);
    laz[l]+=laz[i],laz[r]+=laz[i],laz[i]=0;
}
void add(int l,int r,int s,int t,int i,int num) {
    if(l<=s&&t<=r) {laz[i]+=num,sum[i]+=(t-s+1)*num;return;}
    int mid=(s+t)>>1;
    if(laz[i]!=0) pd(i,s,t);
    if(l<=mid) add(l,r,s,mid,i<<1,num);
    if(mid+1<=r) add(l,r,mid+1,t,(i<<1)|1,num);
    sum[i]=sum[i<<1]+sum[(i<<1)|1];
}
LL query(int l,int r,int s,int t,int i) {
    if(l<=s&&t<=r) return sum[i];
    int mid=(s+t)>>1;LL re=0;
    if(laz[i]!=0) pd(i,s,t);
    if(l<=mid) re=query(l,r,s,mid,i<<1);
    if(mid+1<=r) re+=query(l,r,mid+1,t,(i<<1)|1);
    return re;
}
void binary(int ql,int qr,int l,int r) {
    if(ql>qr) return;
    //如果已经二分出了一个确切的答案,就更新答案
    if(l==r) {for(int i=ql;i<=qr;++i) if(Q[i].bj==2) ans[Q[i].id]=l;return;}
    int mid=(l+r)>>1,t1=0,t2=0;
    for(int i=ql;i<=qr;++i)
        if(Q[i].bj==2) {//步骤2-1
            LL kl=query(Q[i].l,Q[i].r,1,n,1);
            if(kl>=Q[i].v) Q1[++t1]=Q[i];
            else Q2[++t2]=Q[i],Q2[t2].v-=kl;
        }
        else if(Q[i].v<=mid) add(Q[i].l,Q[i].r,1,n,1,1),Q1[++t1]=Q[i];//步骤2-2-1
        else Q2[++t2]=Q[i];//步骤2-2-2
    for(int i=1;i<=t1;++i) if(Q1[i].bj==1) add(Q1[i].l,Q1[i].r,1,n,1,-1);//步骤3
    for(int i=1;i<=t1;++i) Q[ql+i-1]=Q1[i];//步骤4
    for(int i=1;i<=t2;++i) Q[ql+t1+i-1]=Q2[i];
    binary(ql,ql+t1-1,l,mid),binary(ql+t1,qr,mid+1,r);//步骤5
}
int main()
{
    n=read(),m=read();
    for(int i=1;i<=m;++i) {
        Q[i].bj=read(),Q[i].l=read(),Q[i].r=read(),Q[i].v=read();
        if(Q[i].bj==1) Q[i].v=-Q[i].v;//因为是查询第k大数,所以可以把所有数都取相反数
        else Q[i].id=++qs;
    }
    binary(1,m,-50000,50000);
    for(int i=1;i<=qs;++i) printf("%d\n",-ans[i]);
    return 0;
}

CDQ分治

例题:bzoj1176,是一道权限题,没有权限的同学可以看下面那道例题。
把一次询问拆成四次前缀和处理,然后使用CDQ分治即可(另外此题的s好像并没有用)。
1.将所有操作按照x为第一关键字,y为第二关键字,第三关键字为修改操作在查询操作前面的顺序排序。
2.对于时间(即是第几个操作)进行分治
3.使用树状数组维护y上的答案,由于已经以x为关键字排序了,所以计算x的前缀和这个条件已经满足了。
4.遍历当前时间区间的每个操作,如果这个修改操作的时间小于等于mid,就执行这一步操作。如果这个询问操作的时间大于mid,就先计算一下前mid个操作(也就是当前修改完成后的树状数组)对其答案造成的贡献。
5.清除所有修改操作的影响。
6.将当前区间时间在[l,mid]内的操作丢到前mid位,在[mid+1,r]的丢到后面,进行递归分治。
当然,CDQ分治的思想是这样的,算法执行顺序不一定如我所讲的这样。用某Cai的话来说,如果你不想动脑子,那么先cdq左半区间,再处理当前整个区间,再cdq右半区间这样的顺序比较好。如果你想写得简便一点,可以先进行递归执行,再处理现在的区间,不过需要动点脑子。

#include<bits/stdc++.h>
using namespace std;
int read() {
    int q=0,w=1;char ch=' ';
    while((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
    if(ch=='-') w=-1,ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return q;
}
int s,n,m,q;
struct node{int bj,id,qid,x,y,v;}Q[650005],tmp[650005];
int ans[10005],tr[2000005];
int lowbit(int x) {return x&(-x);}
void add(int x,int num) {while(x<=n) tr[x]+=num,x+=lowbit(x);}
int ask(int x) {
    int re=0;
    while(x) re+=tr[x],x-=lowbit(x);
    return re;
}
void cdq(int l,int r) {//步骤2
    if(l==r) return;
    int mid=(l+r)>>1;
    for(int i=l;i<=r;++i)//步骤3,4
        if(Q[i].bj==1&&Q[i].id<=mid) add(Q[i].y,Q[i].v);
        else if(Q[i].bj==2&&Q[i].id>mid) ans[Q[i].qid]+=Q[i].v*ask(Q[i].y);
    for(int i=l;i<=r;++i)//步骤5
        if(Q[i].bj==1&&Q[i].id<=mid) add(Q[i].y,-Q[i].v);
    int t1=l-1,t2=mid;
    for(int i=l;i<=r;++i)//步骤6
        if(Q[i].id<=mid) tmp[++t1]=Q[i];
        else tmp[++t2]=Q[i];
    for(int i=l;i<=r;++i) Q[i]=tmp[i];
    cdq(l,mid),cdq(mid+1,r);
}
int cmp(node a,node b) {//步骤1
    if(a.x!=b.x) return a.x<b.x;
    if(a.y!=b.y) return a.y<b.y;
    return a.bj<b.bj;
}
int main()
{
    int bj,x1,y1,x2,y2,w;
    s=read(),n=read();
    while("niconiconi") {
        bj=read();
        if(bj==3) break;
        if(bj==1) x1=read(),y1=read(),w=read(),Q[++m]=(node){1,m,0,x1,y1,w};
        else {
            x1=read(),y1=read(),x2=read(),y2=read(),++q;
            Q[++m]=(node){2,m,q,x2,y2,1};
            Q[++m]=(node){2,m,q,x1-1,y2,-1};
            Q[++m]=(node){2,m,q,x2,y1-1,-1};
            Q[++m]=(node){2,m,q,x1-1,y1-1,1};
        }
    }
    sort(Q+1,Q+1+m,cmp),cdq(1,m);
    for(int i=1;i<=q;++i) printf("%d\n",ans[i]);
    return 0;
}

再讲讲CDQ分治的最重要应用:loj112 三维偏序
如果没有CDQ分治,那么这道题就要用树套树做了。众所周知,树套树写起来是很困难的。
这题没有“时间”概念,不过我们可以把a属性视作时间。先按照a为第一关键字,b为第二关键字,c为第三关键字的顺序排序。在CDQ分治的过程中,逐步把左边和右边两个区间变成以b为第一关键字的排序,然后用树状数组维护c,用归并排序维护b的顺序。
这么讲可能很不清楚,不过代码总能说明一切。

#include<bits/stdc++.h>
using namespace std;
int read() {
    int q=0;char ch=' ';
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return q;
}
#define lowbit(x) (x&(-x))
const int N=100005,K=200005;
int n,lim,kn,tr[K],ans[N];
struct node{int a,b,c,js,cnt;}p[N],kl[N];
void add(int x,int num) {while(x<=lim) tr[x]+=num,x+=lowbit(x);}
int query(int x) {int re=0;while(x){re+=tr[x],x-=lowbit(x);}return re;}
int cmp2(int i,int j) {
    if(p[i].b!=p[j].b) return p[i].b<p[j].b;
    if(p[i].c!=p[j].c) return p[i].c<p[j].c;
    return 1;
}
void merge(int l,int r,int mid) {
    int t1=l,t2=mid+1;
    for(int i=l;i<=r;++i)
        if(t1<=mid&&(t2>r||cmp2(t1,t2))) kl[i]=p[t1],++t1;
        else kl[i]=p[t2],++t2;
    for(int i=l;i<=r;++i) p[i]=kl[i];
}
void cdq(int l,int r) {
    if(l==r) return;
    int mid=(l+r)>>1;
    cdq(l,mid),cdq(mid+1,r);
    for(int i=mid+1,j=l;i<=r;++i) {
        while(j<=mid&&p[j].b<=p[i].b) add(p[j].c,p[j].cnt),++j;
        p[i].js+=query(p[i].c);
    }
    for(int i=l;i<=mid&&p[i].b<=p[r].b;++i) add(p[i].c,-p[i].cnt);//清除影响
    merge(l,r,mid);//归并排序,比sort小3倍常数
}
int cmp1(node x,node 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;
}
int main()
{
    n=read(),lim=read();
    for(int i=1;i<=n;++i)
        p[i].a=read(),p[i].b=read(),p[i].c=read(),p[i].cnt=1;
    sort(p+1,p+1+n,cmp1);kn=1;
    for(int i=2;i<=n;++i)//去重
        if(p[i].a==p[kn].a&&p[i].b==p[kn].b&&p[i].c==p[kn].c) ++p[kn].cnt;
        else p[++kn]=p[i];
    cdq(1,kn);
    for(int i=1;i<=kn;++i) ans[p[i].js+p[i].cnt-1]+=p[i].cnt;
    for(int i=0;i<n;++i) printf("%d\n",ans[i]);
    return 0;
}

还有一道cdq分治的经典例题:戳我瞧瞧QWQ
最后我们再来提一下CDQ套CDQ的经典四维偏序问题,例题就用:COGS 2479啦,这道题不用去重而且只要考虑偏序对个数,做起来很舒服啊~
三维偏序第一维排序,第二维CDQ,第三维树状数组。那么四维偏序呢?
orzCai
……说的很有道理,不过不只线段树可以套起来,CDQ也是可以的。
(伪)伪代码如下:

CDQ2(l,r) {
    CDQ2(l,mid),CDQ2(mid+1,r);
    以b为关键字归并排序 {
        如果按a排序在左半边且初始下标在左半边,上传到树状数组中。
        如果都在右半边,在树状数组中获得答案。
    }
}
CDQ1(l,r) {
    CDQ1(l,mid),CDQ1(mid+1,r);
    以a为关键字归并排序 {把初始下标在左半边的打上标记1,在右半边的打上标记2}
    CDQ2(l,r);
}

真代码如下:

#include<bits/stdc++.h>
using namespace std;
int read() {
    int q=0;char ch=' ';
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return q;
}
#define RI register int
const int N=50005;
struct node{int a,b,c,tag;}t[N],t1[N],t2[N];
int n,ans,st[N],s[N];
#define lowbit(x) (x&(-x))
void add(int x,int num) {while(x<=n) s[x]+=num,x+=lowbit(x);}
int query(int x) {
    int re=0;
    while(x) re+=s[x],x-=lowbit(x);
    return re;
}
void cdq2(int l,int r) {
    if(l==r) return;
    int mid=(l+r)>>1,i=l,j=mid+1,top=0;
    cdq2(l,mid),cdq2(mid+1,r);
    for(RI k=l;k<=r;++k)
        if(j>r||(i<=mid&&t1[i].b<t1[j].b)) {
            t2[k]=t1[i++];
            if(t2[k].tag) add(t2[k].c,1),st[++top]=t2[k].c;
        }
        else {
            t2[k]=t1[j++];
            if(!t2[k].tag) ans+=query(t2[k].c);
        }
    for(RI k=l;k<=r;++k) t1[k]=t2[k];
    for(RI k=1;k<=top;++k) add(st[k],-1);
}
void cdq1(int l,int r) {
    if(l==r) return;
    int mid=(l+r)>>1,i=l,j=mid+1;
    cdq1(l,mid),cdq1(mid+1,r);
    for(RI k=l;k<=r;++k)
        if(j>r||(i<=mid&&t[i].a<t[j].a)) t1[k]=t[i++],t1[k].tag=1;
        else t1[k]=t[j++],t1[k].tag=0;
    for(RI k=l;k<=r;++k) t[k]=t1[k];
    cdq2(l,r);
}
int main()
{
    freopen("partial_order.in","r",stdin);
    freopen("partial_order.out","w",stdout);
    n=read();
    for(RI i=1;i<=n;++i) t[i].a=read();
    for(RI i=1;i<=n;++i) t[i].b=read();
    for(RI i=1;i<=n;++i) t[i].c=read();
    cdq1(1,n),printf("%d\n",ans);
    return 0;
}

总结

整体二分的思想是同时对处理区间和答案进行二分。
CDQ分治的思想是用处理方式进行排序,然后对时间进行二分。
整体二分可以用于求询问操作一样,而且可以二分答案解决的问题
CDQ分治可以用于求多维偏序问题
两种分治算法都比较暴力,它们的优点是代码短而清晰,缺点是复杂度玄学,必须离线。
所以,这一轮还是没有决出胜负啊。

<think>首先,用户的问题是关于CDQ分治算法的基本原理及其应用场景。我需要根据系统级指令来构建我的回答。 系统级指令关键点: - 所有行内数学表达式必须使用$...$格式。 - 独立公式必须使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 - 回答结构清晰,帮助用户逐步解决问题,保证真实可靠。 参考站内引用: - 引用[1]:关于分治思想的描述,包括分块、递归等。 - 引用[2]:关于书籍的引用,不相关。 - 引用[3]:关于下载资源的链接,不相关。 用户真正的需求是理解CDQ分治的概念原理,所以我需要专注于CDQ分治CDQ分治的基本原理: - CDQ分治是由陈丹琦(Chen Danqi)提出的一种算法,用于处理偏序问题,如三维偏序问题。 - 它是分治思想的一种应用,特别适合解决离线查询问题。 - 核心思想:将问题分成子问题,递归解决,然后合并结果,处理跨越子问题的部分。 应用场景: - 三维偏序问题:例如,给定点集,统计每个点满足某些条件的点数。 - 动态规划优化:如斜率优化等。 - 其他离线查询问题。 我的回答结构: 1. **介绍CDQ分治**:简要说明其背景目的。 2. **基本原理**:解释算法步骤,使用数学表达式描述。 3. **应用场景**:列出常见应用。 4. **生成相关问题**:在最后添加相关问题。 5. **添加引用**:在引用段落末尾自然添加引用标识。 数学表达式: - 行内数学:如变量、点坐标等,使用$...$。 - 独立公式:如方程,使用$$...$$。 例如: - 行内:点$P_i = (a_i, b_i, c_i)$ - 独立:偏序关系公式。 生成相关问题: - 基于CDQ分治,如"CDQ分治如何优化三维偏序问题?"或"CDQ分治的时间复杂度分析?" 确保回答真实可靠: - 基于算法知识,CDQ分治常用于OI竞赛中处理偏序问题。 引用处理: - 参考引用[1]是关于分治思想的,我可以引用它来支持分治部分。 - 其他引用不相关,但系统说"请尽可能多的参考用户上一次的问题你的回答",但用户没有上一次问题,所以基于当前引用。 最终回答大纲: - 开头:介绍CDQ分治。 - 原理部分:分步骤解释,使用LaTeX。 - 应用场景:列举例子。 - 相关问题:在最后添加。 - 结尾:自然结束。</think>### CDQ分治算法的基本原理及其应用场景 CDQ分治(Chen Danqi Divide and Conquer)是由中国计算机科学家陈丹琦提出的一种分治算法变体,主要用于处理离线查询问题,特别是多维偏序问题(如三维偏序)。它在算法竞赛数据处理中应用广泛,能高效解决一些复杂问题。下面我将逐步解释其基本原理应用场景,确保内容清晰可靠。 #### 1. CDQ分治的基本原理 CDQ分治基于经典分治思想,但针对偏序问题进行了优化。核心思路是将问题递归分解为子问题,处理子问题内部子问题之间的影响,最后合并结果。算法主要分为三个步骤:分、治、合。以下是详细解释: - **分(Divide)**:将输入数据(如点集)按某一维度(通常是时间或索引)分成两个大致相等的子集,记为左子集$L$右子集$R$。例如,给定点集$P = \{P_1, P_2, \dots, P_n\}$,其中每个点$P_i = (a_i, b_i, c_i)$表示三维坐标,我们按第一维$a_i$排序并分割: $$ \text{mid} = \left\lfloor \frac{n}{2} \right\rfloor $$ 这样得到$L = \{P_i \mid i \leq \text{mid}\}$$R = \{P_i \mid i > \text{mid}\}$。递归地对$L$$R$应用相同过程。 - **治(Conquer)**:递归处理左子集$L$右子集$R$,解决子问题内部的偏序关系。例如,在三维偏序问题中,目标是统计每个点$P_i$满足$a_j \leq a_i, b_j \leq b_i, c_j \leq c_i$的点数$j$($j \neq i$)。递归调用确保子集内部的问题被解决。 - **合(Merge)**:合并子问题结果,并处理跨越子集$L$$R$的影响。这是CDQ分治的关键: - 使用辅助数据结构(如树状数组或线段树)高效处理跨子集贡献。 - 例如,在三维偏序中,合并时需统计$L$中的点对$R$中点的贡献。具体做法: 1. 对$L$$R$按第二维$b_i$排序。 2. 遍历$R$中的每个点$P_j$,同时维护一个树状数组,用于记录第三维$c_i$的累积值。 3. 对于$P_j$,查询树状数组中满足$c_k \leq c_j$的点数($k$来自$L$),并累加到结果。 这确保了跨子集贡献在$O(n \log n)$时间内完成。 整个算法的时间复杂度为$O(n \log^2 n)$,其中$n$是数据规模。递归公式可表示为: $$ T(n) = 2T\left(\frac{n}{2}\right) + O(n \log n) $$ 其中$O(n \log n)$来自合并步骤的排序树状数组操作。CDQ分治通过“分而治之”高效处理了偏序关系[^1]。 #### 2. CDQ分治的应用场景 CDQ分治特别适合离线查询问题,其中数据点固定,查询可批量处理。常见应用包括: - **三维偏序问题**:如统计逆序对或点对关系。例如,在竞赛中,给定点集统计每个点“支配”的点数(即所有维度都小于等于该点的点)。CDQ分治能高效解决,时间复杂度优于暴力$O(n^2)$。 - **动态规划优化**:用于优化序列问题,如最长上升子序列(LIS)或斜率优化问题。通过分治处理状态转移,减少计算量。 - **离线查询处理**:在数据库或算法中,处理批量查询,如区间统计或范围查询。CDQ分治将查询排序后递归处理,避免实时开销。 - **其他领域**:在计算几何(如最近点对问题)机器学习数据预处理中也有应用,但需结合具体算法调整。 CDQ分治的优势在于其通用性效率,但要求问题具有“可分治性”,即子问题独立且合并步骤可高效实现。实际应用中,常结合树状数组等数据结构提升性能。 #### 相关问题 1. CDQ分治如何处理更高维度的偏序问题(如四维偏序)? 2. CDQ分治与普通分治算法的主要区别是什么? 3. CDQ分治在动态规划优化中的具体实现案例? [^1]: 分治思想的核心是递归分解问题,然后合并结果,这在CDQ分治中得到充分体现,如分割点集并递归处理子问题。 [^2]: 算法理论常参考经典书籍,但CDQ分治更常见于竞赛算法论文。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值