整体二分及cdq分治学习小结

整体二分

基本思想

听上去十分的高大上,但是究其本质就是二分答案加强版,对有些题目,我们单次二分答案的代价可能无法做到 O ( log n ) 。但是对于每个询问它做的操作是几乎相同的,我们认为这类询问是可以合并在一起做的,那么大可不必对于每个询问都去二分答案,我们可以对询问进行分类,划到一个区间去再进行二分答案。

可以看一看2013年XHR的论文。要注意,分了区间之后,对于这个区间的时间代价不能是与原长n成正比,而应该是和当前区间长度成正比,否则时间复杂度就不是log的了。也就是说不能每次都memset,而应该将原来做过的操作全部回退。

例题

静态区间第k小

板子题一道。对于一个二分函数,去二分这个值,怎么统计?把数字改成01序列,然后区间求和。但其实这样是不行的,因为改成01序列的时间复杂度是和数组原长相关的。

给出一种比较好的想法,把序列改为插入操作,不妨去扫描当前被划入这一二分函数的操作,如果插入的值小于等于mid,我们就把它加进去,那么当我们要知道当前的答案与mid的大小关系,就只需要统计区间中插入了多少个数就可以了!用树状数组即可实现。

时间复杂度: O ( n log n log m a x v )

核心代码如下。

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].op)
            ans[q[i].id]=l;
        return ;
    }
    int m=(l+r)>>1,x1=0,x2=0;
    for(int i=ql,tmp;i<=qr;i++)
    {
        if(q[i].op)
        {
            tmp=sum(q[i].r)-sum(q[i].l-1);
            if(q[i].k<=tmp) q1[++x1]=q[i];
            else q2[++x2]=q[i],q2[x2].k-=tmp;
        }
        else if(q[i].k<=m) add(q[i].id,1),q1[++x1]=q[i];
        else q2[++x2]=q[i];
    }
    for(int i=1;i<=x1;i++)
      if(!q1[i].op)
        add(q1[i].id,-1);
    for(int i=1;i<=x1;i++)
      q[ql+i-1]=q1[i];
    for(int i=1;i<=x2;i++)
      q[ql+x1+i-1]=q2[i];
    binary(ql,ql+x1-1,l,m);binary(ql+x1,qr,m+1,r);
}

动态区间第k小

我们将修改操作改为删除+插入即可。

时间复杂度: O ( n log n log m a x v )

Meteors(POI2011)

二分每个国家满足需求的时间,这个显然可以二分的。注意到所有国家的空间站总和为m,那么我们如果势能分析一下,每次询问国家时,直接去找(我是说用链式前向星找)其空间站统计收集的份数的时间复杂度是 O ( n log n log k ) 的。注意一点,在最坏情况下总和可能爆long long,所以在累加的时候注意判一下。由于是区间修改,单调查询,用树状数组维护差分序列即可。

k大数查询(ZJOI2013)

也算是一道板子题了,树状数组改成线段树即可,把插入的数字离散化可以大幅度减小二分的值域,大概可以优化出一个1/3的常数。

CDQ分治

从偏序问题讲起

k维偏序问题就是给你n个点,及其k个关键信息。询问你对于每个点,有多少个点满足k个关键信息均小于等于它的关键信息。

一维偏序:……排序

二维偏序:先把第一维排序,用树状数组维护y坐标出现次数,然后按顺序扫,先询问y坐标更小的有多少,再把每个点的y坐标插入一个树状数组。排序保证了先插入的点必定x坐标是小于等于当前点的,所以这样是没有毛病的。时间复杂度 O ( n log n )

三维偏序:我们可以在第二维上用cdq来解决,cdq的过程有点类似归并排序,它的核心思想是把区间都拆开,每次计算跨立左右两子区间的贡献。再做对y轴进行归并排序时我们可以很好地统计贡献。

我们第一维仍然先排序。然后进行cdq,对y坐标进行归并排序,然后用树状数组插入z坐标。这就可以保证先插入的点x坐标和y坐标更小。

注意先修改后查询。注意初始化排序的时候x,y,z三维都要排。时间复杂度 O ( n log 2 n ) 。核心代码如下:

void cdq(int l,int r)
{
    if(r<=l) return ;
    int m=(l+r)>>1,p=l,q=m+1;
    cdq(l,m);cdq(m+1,r);top=0;
    while(p<=m&&q<=r)
    {
        if(op[p]<op[q])
        {
            if(op[p].tp==1) update(op[p].c,1),que.push(p);
            tmp[++top]=op[p++];
        }
        else
        {
            if(op[q].tp==2) ans[op[q].id]+=query(op[q].c);
            tmp[++top]=op[q++];
        }
    }
    while(p<=m) tmp[++top]=op[p++];
    while(q<=r)
    {
        if(op[q].tp==2) ans[op[q].id]+=query(op[q].c);
        tmp[++top]=op[q++];
    }
    while(!que.empty()) update(op[que.front()].c,-1),que.pop();
    for(int i=1;i<=top;i++) op[i+l-1]=tmp[i];
}

k维偏序:其实并不在我们讨论的范围之中,因为这个比较麻烦了,可能要各种套,时间复杂度达到了 O ( n log k 1 n ) 。但是我们会发现有种东西叫kd-tree,可以做到 O ( n 2 1 k ) 的复杂度。再然后还有用bitset的 O ( n n ) 的做法。以及k很大时, O ( n 2 ) 的暴力。

基本思想

讲到这里就可以对cdq的基本思想有个基本的了解了。

cdq分治是在普通的分治中一种特殊的分治方法,它可以解决一些带修改和询问的操作。在分治的过程中,它会先解决左子区间的问题,然后考虑左边操作对右边问题的影响再来计算答案。每次计算跨立两个区间的贡献,同时左子区间要考虑对右子区间答案的影响。

例题

园丁的烦恼(SHOI2007)

还是改成插入和询问操作,矩形的询问可以拆成四个以(0,0)开始的四个不同询问。然后记到同一个答案数组里即可。

不妨用tp=1表示插入操作,tp=2表示要加入贡献的询问,tp=3表示要减的询问。核心代码如下。

void cdq(int l,int r)
{
    if(r<=l) return ;
    int m=(l+r)>>1,p=l,q=m+1,sum=0;
    cdq(l,m);cdq(m+1,r);top=0;
    while(p<=m&&q<=r)
    {
        if(op[p]<op[q])
        {
            if(op[p].tp==1) sum+=1;
            tmp[++top]=op[p++];
        }
        else
        {
            if(op[q].tp==2) ans[op[q].id]+=sum;
            else if(op[q].tp==3) ans[op[q].id]-=sum;
            tmp[++top]=op[q++];
        }
    }
    while(p<=m) tmp[++top]=op[p++];
    while(q<=r)
    {
        if(op[q].tp==2) ans[op[q].id]+=sum;
        else if(op[q].tp==3) ans[op[q].id]-=sum;
        tmp[++top]=op[q++];
    }
    for(int i=1;i<=top;i++) op[l+i-1]=tmp[i];
}

稻草人

请戳解题报告

猜你喜欢

转载自blog.csdn.net/as_a_kid/article/details/81091640