• 【算法学习】【洛谷】cdq分治 & P3810 三维偏序


    cdq是何许人也?请参看这篇:https://wenku.baidu.com/view/3b913556fd0a79563d1e7245.html

    在这篇论文中,cdq提出了对修改/询问型问题(Modify-Query问题)的分治做法,下面来具体讨论一下:

    我们将修改/询问看作在时间轴上的一系列元素,把修改和询问统称为“操作”,并用记号([l,r])表示第(l)个操作到第(r)个操作的序列。

    在时间轴上进行的操作,众所周知有这样的特性:时间早的会影响时间晚的,而反过来不会,这就是cdq分治的本质:用前面的修改更新后面的答案。

    假设我们要计算一段长为(m)的区间([l,r])的答案,考虑计算([l,mid])和([mid+1,r]),最终将两段区间合并,其中(mid=leftlfloorfrac{l+r}{2} ight floor)。

    这里,([l,r])的答案,指的就是,单纯的([l,r])的答案。不考虑更早的操作对([l,r])的影响。

    这样的做法是否似曾相识?典型的分治—合并的思路,就出现在最稳定的排序方法——归并排序上。

    其时间复杂度为(T(1)=Theta(1), T(n)=2T(frac{n}{2})+Theta(n)=Theta(n\,log\,n)),其中合并区间的复杂度为(Theta(n))。

    提出这个例子只是为了回忆起对分治感觉和思路。

    看一道例题,方便讲解:(这题就是论文中的题目)

    给你(q)个操作:①插入一个点((x,y)),②询问当前所有的点中,满足(x_ileq x,y_ileq y)的点(x_i,y_i)的个数((1leq q,x,yleq 100000))。

    我们采用cdq分治的方法,将这些操作按照时间顺序分治。

    假设我们要获取([l,r])的答案,而我们已经对([l,mid],[mid+1,r])进行了递归处理,这时我们需要处理出完整的答案。

    就像归并排序的过程一样,左边的区间和右边的区间都自己处理好自己的答案了,可是整个区间的答案仍没有完全算出,这是因为没有考虑左边区间对右边区间的影响。

    在本题中,“影响”就是([l,mid])中插入的点,会被统计到([mid+1,r])中去。

    这时,发现(可以造成影响的)左侧只有修改,右侧只有询问,考虑将修改和询问都按照他们的(x_i)为第一关键字,(y_i)为第二关键字,修改优先,查询后置为第三关键字排序,放到树状数组里去处理。

    因为不需要考虑时间的顺序了,所以这很容易就能做到。

    时间复杂度统计:(T(1)=Theta(1),T(n)=2T(frac{n}{2})+Theta(n\,log\,y)=Theta(n\,log\,n\,log\,y))。

    分析:使用cdq分治,我们将有时间顺序的操作转换成修改在前,询问在后的操作,相当于把问题简单化,“去时间化”了。

    更深入地说,对于这样一个问题:初始时有一些三维点((x_i,y_i,z_i)),只需要查询满足(x_ileq x,y_ileq y,z_ileq z)的点(x_i,y_i,z_i)的个数。

    这看似和上一题相比,多了一维,但是若我们将所有的点和查询按照(z_i,x_i,y_i,)修改-查询依次为关键字排序,再把(z)坐标扔掉,这不就把空间上的维度转化成时间轴上的顺序了吗?所以这题本质上和上一题相同,可以用同样的算法解决。

    这题就是洛谷的P3810 三维偏序,我的代码如下:

     1 #include<cstdio>
     2 #include<algorithm>
     3 struct ww{int x,y,z,w,I;}a[100001],tmp[100001];
     4 inline bool cmp1(ww p1,ww p2){return p1.x==p2.x?(p1.y==p2.y?p1.z<p2.z:p1.y<p2.y):p1.x<p2.x;}
     5 int n,nn,k,bit[200001],ans[100001],Ans[100001];
     6 inline void Ins(int i,int x){for(;i<=k;bit[i]+=x,i+=i&-i);}
     7 inline int Qur(int i){int sum=0;for(;i;sum+=bit[i],i-=i&-i);return sum;}
     8 void cdq(int l,int r){
     9     if(l==r) return;
    10     int mid=(l+r)>>1;
    11     cdq(l,mid); cdq(mid+1,r);
    12     for(int i=l,lf=l,rt=mid+1;lf<=mid||rt<=r;++i){
    13         if((lf<=mid&&rt>r)||(lf<=mid&&rt<=r&&a[lf].y<=a[rt].y)) Ins(a[lf].z,a[lf].w), tmp[i]=a[lf], ++lf;
    14         else Ans[a[rt].I]+=Qur(a[rt].z), tmp[i]=a[rt], ++rt;
    15     }
    16     for(int i=l;i<=mid;++i) Ins(a[i].z,-a[i].w);
    17     for(int i=l;i<=r;++i) a[i]=tmp[i];
    18 }
    19 int main(){
    20     scanf("%d%d",&n,&k);
    21     for(int i=1;i<=n;++i) scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
    22     std::sort(a+1,a+n+1,cmp1);
    23     for(int i=1;i<=n;++i) if(a[i].x!=a[i-1].x||a[i].y!=a[i-1].y||a[i].z!=a[i-1].z) a[++nn]=a[i], a[nn].w=1, a[nn].I=nn; else ++a[nn].w;
    24     cdq(1,nn);
    25     for(int i=1;i<=nn;++i) ans[Ans[a[i].I]+a[i].w-1]+=a[i].w;
    26     for(int i=0;i<n;++i) printf("%d
    ",ans[i]);
    27     return 0;
    28 }
    View Code

    偏序其实就是两个元素的值都对应地大(小)(等)于的关系,有时间顺序的问题也可以通过记时间标记转化成偏序问题,上面的两道题就是偏序问题。

    数据结构(树状数组,线段树,平衡树等)通常可以处理较低维的偏序问题,而cdq分治则是处理偏序问题的利器。

    把偏序问题中的某一维排序变为时间轴,在这时间轴之上分治处理,在合并时,就巧妙地将修改和查询分离开来,成为两个不相交的部分,没有了时间顺序的困扰,解决自然变得容易。

    若是更高维的偏序,转化后仍不能处理的,我们可以将转化后的(没有时间顺序的)操作序列再排序,分离出时间轴来,对之下的在进行一次cdq分治,即cdq套cdq。

    这样做,思维难度、代码复杂度以及调试难度都有所提升,但cdq分治本质上熟练了就很好写了,应该要多多益善地练习相关题目。

    运用cdq的注意事项:所有的修改和查询都必须是已知的,即cdq是离线算法,对于强制在线的题目,就要另寻他法了。

    cdq分治的应用不止于此,还有许多问题,运用相似的思想——分治,也能获得简便的解决,在此就不一一列举了。

    更多例题:P4093 [HEOI2016/TJOI2016]序列

  • 相关阅读:
    OCP-1Z0-052-V8.02-116题
    OCP-1Z0-052-V8.02-6题
    OCP-1Z0-052-V8.02-5题
    使用rman恢复控制文件
    Matlab-质点的运动
    Matlab中checkerboard-创建棋盘图像(二)
    OCP-1Z0-052-V8.02-4题
    OCP-1Z0-052-V8.02-3题
    Matlab中checkerboard-创建棋盘图像(一)
    OCP-1Z0-052-V8.02-1题
  • 原文地址:https://www.cnblogs.com/PinkRabbit/p/7932397.html
Copyright © 2020-2023  润新知