[关闭]
@happyforever 2018-08-06T13:53:05.000000Z 字数 9093 阅读 653

CDQ分治

分治 CDQ分治


基本思想

1.要解决一系列包含修改和查询操作的问题,可以将这些问题排成一个序列,用一个区间[l,r]来表示
2.递归处理左区间[l,mid]和边区间[mid+1,r]
3.合并两个子问题的时候同时考虑到左区间[l,mid]内部的修改对右区间[mid+1,r]内产生的影响。即用左边的子问题帮助解决右边的子问题

与普通分治的主要区别:普通分治在合并两个子问题的时候[l,mid]不会对[mid+1,r]内的问题产生影响

一般步骤:
1.将整个操作序列分为两个长度相等的部分(分)
2.递归处理前一部分的子问题(治1)
3.计算前一部分的子问题中的修改操作对后一部分子问题的影响(治2)
4.递归处理后一部分子问题(治3)

用途

顶替高级数据结构

二维偏序问题

给定个有序数对,求对于每个,满足并且的有序对有多少个

回忆归并排序求逆序对的过程,在合并两个子区间的时候,要考虑到左边区间对右边区间的影响。即:每次从右边区间的有序序列中取出一个元素的时候,要把“以这个元素结尾的逆序对的个数”加上“左边区间有多少个元素比他大”。这是一个典型的CDQ分治的过程。

现在把这个问题拓展到二维偏序问题。在归并排序求逆序对的过程中,每个元素可以用一个有序对表示,其中a表示数组中的位置,b表示该位置对应的值。那么求的就是“对于每个有序对,有多少个有序对满足”,这就是一个二维偏序问题。

注意到在求逆序对的问题中,a元素是默认有序的,即我们拿到元素的时候,数组中的元素是默认从第一个到最后一个按顺序排列的,所以才能在合并子问题的时候忽略a元素带来的影响。因为我们在合并两个子问题的过程中,左边区间的元素一定出现在右边区间的元素之前,即左边区间的元素的a都小于右边区间元素的a

那么对于二维偏序问题,在拿到所有有序对的时候,先把a元素从小到大排序。这时问题就变成了“求顺序对”,因为a元素已经有序,可以忽略a元素带来的影响,和“求逆序对”的问题是一样的。

那么考虑二维偏序问题的另一种解法:用树状数组代替CDQ分治,即常用的用树状数组求顺序对。在按照a元素排序之后,对于整个序列从左到右扫描,每次扫描到一个有序对,求出“扫描过的有序对中,有多少个有序对的b值小于当前b值”,可以用权值树状数组或者权值线段树实现。

然而当b的值非常大的时候,空间和时间上就会吃不消,便可以用CDQ分治代替
所以CDQ分治具有“顶替复杂的高级数据结构”的作用。

EX_二维偏序问题

给定一个N个元素的序列A(标号从1开始),初始值全部为0,对这个序列进行以下两种操作
1.格式为1 x k,表示将位置x的元素加上k
2.格式为2 x y,表示求出区间[x,y]所有元素的和

这是个经典的树状数组问题,现在考虑用CDQ分治解决
将其转化成二维偏序问题,每个操作用一个有序数对来表示,其中表示这是第几次操作,表示操作的位置,显然a是默认有序的,所以在合并子问题的过程中,就按照b从小到大的顺序进行合并

定义结构体Query包含3个元素:type,idx,val,其中idx表示操作的位置,type为1表示修改,val表示“加上的值”。
对于查询,用前缀和的思想把他分解成两个操作:sum[1,y]-sum[1,x-1],即分解成两次前缀和的查询。在合并的过程中,type为2表示遇到了一个查询的左端点x-1,需要把该查询的结果减去当前“加上的值的前缀和”,type为3表示遇到了一个查询的右端点y,需要把查询的结果加上当前“加上的值的前缀和”,val表示“是第几个查询”。这样,我们就把每个操作转换成了带有附加信息的有序对(时间,位置),然后对整个序列进行CDQ分治

需要注意的细节:
1.对于位置相同的操作,先修改后查询
2.合并问题的时候统计“加上的值的前缀和”,只能统计左边区间内的修改操作,改动查询结果的时候,只能修改右边区间内的查询结果。因为只有左边区间内的修改值对右边区间内的查询结果的影响还没有统计

例题

  1. #include <cstdio>
  2. #include <cstring>
  3. #include <algorithm>
  4. inline void read(int &n)
  5. {
  6. n=0;int w=1;register char c=getchar();
  7. while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
  8. while(c>='0'&&c<='9')n=n*10+c-'0',c=getchar();
  9. n*=w;
  10. }
  11. typedef long long ll;
  12. const int N=500001;
  13. int n,m;
  14. struct Query{
  15. int type,idx,val;
  16. bool operator<(const Query &x)const
  17. {return idx==x.idx ? type<x.type : idx<x.idx;}
  18. }que[N*3],emp[N*3];
  19. int qidx,aidx,ans[N*3];
  20. void CDQ(int l,int r)
  21. {
  22. if(r-l<=1)return ;
  23. int mid=l+r>>1;
  24. CDQ(l,mid),CDQ(mid,r);
  25. ll sum=0;
  26. int i=l,j=mid,k=0;
  27. while(i<mid&&j<r)
  28. {
  29. if(que[i]<que[j])//只统计左边区间的修改值
  30. {
  31. if(que[i].type==1)
  32. sum+=que[i].val;
  33. emp[k++]=que[i++];
  34. }
  35. else//只修改右边区间内的查询结果
  36. {
  37. if(que[j].type==2)
  38. ans[que[j].val]-=sum;
  39. else
  40. if(que[j].type==3)
  41. ans[que[j].val]+=sum;
  42. emp[k++]=que[j++];
  43. }
  44. }
  45. while(i<mid)emp[k++]=que[i++];
  46. while(j<r)
  47. {
  48. if(que[j].type==2)
  49. ans[que[j].val]-=sum;
  50. else
  51. if(que[j].type==3)
  52. ans[que[j].val]+=sum;
  53. emp[k++]=que[j++];
  54. }
  55. for(int x=0;x<k;++x)
  56. que[x+l]=emp[x];
  57. }
  58. int main()
  59. {
  60. read(n),read(m);
  61. for(int i=1;i<=n;++i)
  62. {
  63. que[qidx].idx=i;que[qidx].type=1;
  64. read(que[qidx].val);++qidx;
  65. }
  66. for(int l,r,i=0;i<m;++i)
  67. {
  68. read(que[qidx].type);
  69. if(que[qidx].type==1)
  70. read(que[qidx].idx),read(que[qidx].val);
  71. else
  72. {//把查询操作分为两部分
  73. read(l),read(r);
  74. que[qidx].idx=l-1;que[qidx].val=aidx;++qidx;
  75. que[qidx].type=3;que[qidx].idx=r;que[qidx].val=aidx;
  76. ++aidx;
  77. }
  78. ++qidx;
  79. }
  80. CDQ(0,qidx);
  81. for(int i=0;i<aidx;++i)
  82. printf("%lld\n",ans[i]);
  83. return 0;
  84. }

三维偏序问题

给定N个有序三元组(a,b,c),求对于每个三元组,有多少个三元组满足

首先给出不用CDQ分治的方法:树套树
先按照a元素排序,从左到右扫描。按照b元素构造权值树状数组,树状数组每个节点按照c元素构造平衡树。不仅常数大,而且码量大,还容易写错

CDQ分治做法:
类似二维偏序问题,先按照a元素从小到大排序,忽略a元素的影响。然后CDQ分治,按照b元素从小到大的顺序进行归并操作。但是这时候没办法像求逆序对一样简单地统计个数了。那么对于c元素的处理,比较好的方案就是借助权值树状数组。每次从右边的序列中取出三元组(a,b,c)时,对树状数组查询c值小于(a,b,c)的三元组有多少个;每次从左边序列取出三元组(a,b,c)的时候,根据c值在树状数组中进行修改。注意,每次使用完树状数组记得把树状数组归零!

例题

  1. #include <algorithm>
  2. #include <iostream>
  3. #include <cstring>
  4. #include <cstdio>
  5. typedef long long ll;
  6. const int N=2e5+5;
  7. inline void read(int &n)
  8. {
  9. char c=getchar();n=0;int w=1;
  10. while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
  11. while(c>='0'&&c<='9')n=n*10+c-'0',c=getchar();
  12. n*=w;
  13. }
  14. int n,maxVal;
  15. struct Operation{
  16. int a,b,c,w;
  17. int f;//ans
  18. bool operator <(const Operation &r)const
  19. {return (a==r.a&&b==r.b)?c<r.c:(a==r.a?b<r.b:a<r.a);}
  20. }a[N],t[N];
  21. int c[N];
  22. inline int lowbit(int x){return x&-x;}
  23. inline void add(int p,int v)
  24. {
  25. for(;p<=maxVal;p+=lowbit(p))
  26. c[p]+=v;
  27. }
  28. inline int sum(int p)
  29. {
  30. int emp=0;
  31. for(;p;p-=lowbit(p))
  32. emp+=c[p];
  33. return emp;
  34. }
  35. int ans[N];
  36. void CDQ(int l,int r)
  37. {
  38. if(l==r)return;
  39. int mid=(l+r)>>1;
  40. CDQ(l,mid);CDQ(mid+1,r);
  41. int i=l,j=mid+1,p=l;
  42. while(i<=mid||j<=r)
  43. {
  44. if(j>r||(i<=mid&&a[i].b<=a[j].b))
  45. add(a[i].c,a[i].w),t[p++]=a[i++];
  46. else
  47. {
  48. a[j].f+=sum(a[j].c);
  49. t[p++]=a[j++];
  50. }
  51. }
  52. for(int i=l;i<=mid;++i)
  53. add(a[i].c,-a[i].w);
  54. for(int i=l;i<=r;++i)
  55. a[i]=t[i];
  56. }
  57. int main()
  58. {
  59. read(n);read(maxVal);
  60. for(int i=1;i<=n;++i)
  61. {
  62. read(a[i].a);
  63. read(a[i].b);
  64. read(a[i].c);
  65. a[i].w=1;
  66. }
  67. std::sort(a+1,a+1+n);
  68. int p=1;
  69. for(int i=2;i<=n;++i)
  70. {
  71. if(a[i].a==a[p].a&&a[i].b==a[p].b&&a[i].c==a[p].c)
  72. ++a[p].w;
  73. else a[++p]=a[i];
  74. }
  75. int n_=n;
  76. n=p;
  77. CDQ(1,n);
  78. for(int i=1;i<=n;++i)
  79. ans[a[i].f+a[i].w-1]+=a[i].w;
  80. for(int i=0;i<n_;++i)
  81. printf("%d\n",ans[i]);
  82. return 0;
  83. }

EX_三维偏序问题

平面上有N个点,每个点的横纵坐标在之间,有个询问,每个询问为查询在指定矩形之内有多少个点,矩形用的方式给出,其中为左下角坐标,为右上角坐标

把每个点的位置变成一个修改操作,用三元组(时间,横坐标,纵坐标)来表示,把每个查询分解成4个前缀和查询,同样用三元组来表示。对于修改操作,每个三元组没有附加信息;对于查询操作,每个三元组的附加信息为“第几个查询”和“对结果的影响是+还是-,用+1表示+,用-1表示-”。操作到来的时间是默认有序的,分治过程中按照横坐标从小到大排序,用树状数组维护纵坐标的信息

如果不想写CDQ分治的话可以选择二维线段树或者二维树状数组。。。但是空间消耗有点爆炸

  1. #include <cstring>
  2. #include <cstdio>
  3. #include <cmath>
  4. inline void read(int &n)
  5. {
  6. n=0;int w=1;register char c=getchar();
  7. while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
  8. while(c>='0'&&c<='9')n=n*10+c-'0',c=getchar();
  9. n*=w;
  10. }
  11. int n,m,maxy=-1;
  12. struct Query{
  13. int type,x,y,w,aid;//w表示对查询结果贡献(+还是-),aid是“第几个查询”
  14. bool operator<(const Query &rhs)const
  15. {return x==rhs.x?type<rhs.type:x<rhs.x;}
  16. }query[500001*3],emp[500001*3];
  17. int qidx,ans[500001],aidx;
  18. void addq(int type,int x,int y,int w,int aid)
  19. {query[qidx++]=(Query){type,x,y,w,aid};}
  20. inline int max(int x,int y)
  21. {return x>y?x:y;}
  22. namespace BIT
  23. {//树状数组相关
  24. int arr[10000002];
  25. inline int lowbit(int num)
  26. {return num&(-num);}
  27. void add(int idx,int val)
  28. {
  29. while(idx<=maxy)
  30. {
  31. arr[idx]+=val;
  32. idx+=lowbit(idx);
  33. }
  34. }
  35. int query(int idx)
  36. {
  37. int ans=0;
  38. while(idx)
  39. {
  40. ans+=arr[idx];
  41. idx-=lowbit(idx);
  42. }
  43. return ans;
  44. }
  45. void clear(int idx)
  46. {
  47. while(idx<=maxy)
  48. {
  49. if(arr[idx])
  50. arr[idx]=0;
  51. else break;
  52. idx+=lowbit(idx);
  53. }
  54. }
  55. }
  56. void CDQ(int l,int r)
  57. {
  58. if(r-l<=1)return;
  59. int mid=l+r>>1;
  60. CDQ(l,mid);CDQ(mid,r);
  61. int i=l,j=mid,k=l;
  62. while(i<mid&&j<r)
  63. {
  64. if(query[i]<query[j])
  65. {
  66. if(query[i].type==0)
  67. BIT::add(query[i].y,1);
  68. emp[k++]=query[i++];
  69. }
  70. else
  71. {
  72. if(query[j].type==1)
  73. ans[query[j].aid]+=query[j].w*BIT::query(query[j].y);
  74. emp[k++]=query[j++];
  75. }
  76. }
  77. while(i<mid)emp[k++]=query[i++];
  78. while(j<r)
  79. {
  80. if(query[j].type==1)
  81. ans[query[j].aid]+=query[j].w*BIT::query(query[j].y);
  82. emp[k++]=query[j++];
  83. }
  84. for(int i=l;i<r;++i)
  85. {
  86. BIT::clear(emp[i].y);//清空树状数组
  87. query[i]=emp[i];
  88. }
  89. }
  90. int main()
  91. {
  92. read(n);read(m);int x,y,x1,x2,y1,y2;
  93. while(n--)
  94. {
  95. read(x);read(y);
  96. ++x;++y;//为了方便,把坐标转化为[1,1e7+1]
  97. addq(0,x,y,0,0);maxy=max(maxy,y);//修改操作无附加信息
  98. }
  99. while(m--)
  100. {
  101. read(x1);read(y1);read(x2);read(y2);
  102. addq(1,x1,y1,1,aidx);addq(1,x1,y2+1,-1,aidx);
  103. addq(1,x2+1,y1,-1,aidx);addq(1,x2+1,y2+1,1,aidx);
  104. ++aidx;maxy=max(maxy,max(y1,y2));
  105. }
  106. CDQ(0,qidx);
  107. for(int i=0;i<aidx;++i)
  108. printf("%d\n",ans[i]);
  109. return 0;
  110. }

例题P4169 [Violet]天使玩偶/SJY摆棋子
/*
假设A为询问的点,先考虑回忆出来的点都在询问的点左下方时:

那么取到最大值时,有最小值
因此问题被转化为:对于一个询问(x,y),找到满足时间戳在其前且的点中的最大值
*/

  1. #include <algorithm>
  2. #include <iostream>
  3. #include <cstring>
  4. #include <cstdio>
  5. namespace inout
  6. {
  7. const int S=1<<20;
  8. char frd[S],*ihed=frd+S;
  9. const char *ital=ihed;
  10. inline char inChar()
  11. {
  12. if(ihed==ital)
  13. fread(frd,1,S,stdin),ihed=frd;
  14. return *ihed++;
  15. }
  16. inline int get()
  17. {
  18. char ch;int res=0;bool flag=false;
  19. while(!isdigit(ch=inChar())&&ch!='-');
  20. (ch=='-'?flag=true:res=ch^48);
  21. while(isdigit(ch=inChar()))
  22. res=res*10+ch-48;
  23. return flag?-res:res;
  24. }
  25. };
  26. const int Maxn=0x3f3f3f3f;
  27. const int L=1e6+5;
  28. int n,m,lx,ly,rx,ry;
  29. int c[L],Ans[L];
  30. inline int min(int x,int y)
  31. {return x<y?x:y;}
  32. inline int max(int x,int y)
  33. {return x>y?x:y;}
  34. inline void Clear(int x)
  35. {
  36. for(;x<=ly;x+=x&-x)
  37. if(c[x])
  38. c[x]=0;
  39. else break;
  40. }
  41. inline int Query(int x)
  42. {
  43. int res=0;
  44. for(;x;x^=x&-x)
  45. res=max(res,c[x]);
  46. return res;
  47. }
  48. inline void Modify(int x,int y)
  49. {
  50. for(;x<=ly;x+=x&-x)
  51. c[x]=max(c[x],y);
  52. }
  53. struct Ask{
  54. int x,y,t;bool f;
  55. Ask(){}
  56. Ask(const int &X,const int &Y,const int &T,const bool &F):
  57. x(X),y(Y),t(T),f(F){}
  58. bool operator<(const Ask&a)const
  59. {return x<=a.x;}
  60. }q[L],p[L],a[L];
  61. void CDQsolve(int l,int r)
  62. {
  63. if(l==r)return;
  64. int mid=l+r>>1;
  65. CDQsolve(l,mid);CDQsolve(mid+1,r);
  66. int j=l;
  67. for(int i=mid+1;i<=r;++i)
  68. if(!p[i].f)
  69. {
  70. for(;j<=mid&&p[j].x<=p[i].x;++j)
  71. if(p[j].f)
  72. Modify(p[j].y,p[j].x+p[j].y);
  73. int emp=Query(p[i].y);
  74. if(emp)
  75. Ans[p[i].t]=min(Ans[p[i].t],p[i].x+p[i].y-emp);
  76. }
  77. for(int i=l;i<j;++i)
  78. if(p[i].f)
  79. Clear(p[i].y);
  80. std::merge(p+l,p+mid+1,p+mid+1,p+r+1,q+l);//将两个有序的序列合并成一个有序的序列,默认从小到大
  81. for(int i=l;i<=r;++i)
  82. p[i]=q[i];
  83. }
  84. inline void Delete()
  85. {
  86. rx=ry=m=0;
  87. for(int i=1;i<=n;++i)
  88. if(!p[i].f)
  89. rx=max(rx,p[i].x),ry=max(ry,p[i].y);
  90. for(int i=1;i<=n;++i)
  91. if(p[i].x<=rx&&p[i].y<=ry)
  92. q[++m]=p[i];
  93. for(int i=1;i<=m;++i)
  94. p[i]=q[i];
  95. }
  96. int main()
  97. {
  98. n=inout::get();
  99. m=inout::get();
  100. int k,x,y;
  101. for(int i=1;i<=n;++i)
  102. {
  103. x=inout::get()+1;
  104. y=inout::get()+1;
  105. p[i]=Ask(x,y,i,true);
  106. lx=max(lx,x);ly=max(ly,y);
  107. }
  108. while(m--)
  109. {
  110. k=inout::get();
  111. x=inout::get()+1;
  112. y=inout::get()+1;
  113. ++n;p[n]=Ask(x,y,n,k&1);
  114. lx=max(lx,x);ly=max(ly,y);
  115. }
  116. for(int i=1;i<=n;++i)a[i]=p[i];
  117. memset(Ans,Maxn,sizeof(Ans));
  118. ++lx;++ly;
  119. Delete();CDQsolve(1,m);
  120. for(int i=1;i<=n;++i)
  121. p[i]=a[i],p[i].x=lx-p[i].x;
  122. Delete();CDQsolve(1,m);
  123. for(int i=1;i<=n;++i)
  124. p[i]=a[i],p[i].y=ly-p[i].y;
  125. Delete();CDQsolve(1,m);
  126. for(int i=1;i<=n;++i)
  127. p[i]=a[i],p[i].x=lx-p[i].x,p[i].y=ly-p[i].y;
  128. Delete();CDQsolve(1,m);
  129. for(int i=1;i<=n;++i)
  130. if(!a[i].f)
  131. printf("%d\n",Ans[i]);
  132. return 0;
  133. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注