CDQ分治与总体二分小结

前言

  这是一波强行总结。html

  下面是一波瞎比比。ios

  这几天作了几道CDQ/总体二分,感受本身作题速度好慢啊。数组

  不少很显然的东西都看不出来 分治分不出来 打不出来 调不对数据结构

  上午下午晚上的效率彻底不同啊。ide

  完蛋.jpg 绝望.jpg。函数

 

关于CDQ分治

  CDQ分治,求的是三维偏序问题都知道的。
spa

  求法呢,就是在分治外面先把一维变成有序
code

  而后分治下去,左边(l,mid)关于右边(mid+1,r)就不存在某一维的逆序了,因此只有两维偏序了。htm

  这个时候来一波"树状数组求逆序对"的操做搞一下二维偏序blog

  就能够把跨过中线的,左边更新右边的状况计算出来。

  注意:只计算左边的操做对右边的询问的贡献!

  而后左右两边递归处理就行了。

  正确性:按照线段树的形态递归的CDQ分治,保证每一对三元组在第一维划分的线段树上都有且仅有一个LCA(这不废话吗),而这一组答案就会且仅会在LCA处计算。若是在LCA下面,点对不在一个work内天然不会计算。若是在LCA上面了,点对就在同一侧,不会互相更新。

  复杂度:设一次work的复杂度是f(len),则复杂度是O(f(n)logn)。

  通常都在分治里用树状数组,通常的复杂度就是O(nlog2n)的。

  通常是这样的套路:假设三维偏序分别为a,b,c;

  在main函数里保证a递增。

  而后在CDQ里先分治左右,传下去的时候a仍然递增,不破坏性质。

  而后分治完左右两边后,需保证左右两边分别b都是递增的(a不重要)。

  而后就是相似归并排序的操做了。

  此时左边的a确定都小于右边的a,那么若是对于一个右边的元素

  以前相似归并的操做就能够保证全部小于b的左边的元素都已经遍历过。

  那么找c也小于它的?值域线段树/树状数组等数据结构维护一下就行了。

  而后你这么归并了一波后,就发现统计完答案后b是有序递增的了(这个时候a已经不重要了)。

  对于上层操做,符合"左右两边分别b是递增的"了。

  BZOJ陌上花开居然是权限题?这是在搞笑。

  好吧BZOJ动态逆序对,以前写过的,作两次CDQ就行了。

  BZOJ稻草人,也是CDQ,加个单调栈。

 

还有一个就是高维偏序问题。

cogs上的2479 HZOI2016 偏序 就是四维偏序板子。

后面还有两个增强版,到了七维,不是CDQ干的事情,详情请见这个PPT

校内交流因此作的不是很严谨(吐舌)

这里只谈论四维偏序,即a<a'   b<b'   c<c'   d<d'。

作法是喜闻乐见的CDQ套CDQ套树状数组。

有个很妙的博客:Candy?

首先在外面按照a排好序。

进第一层CDQ。先递归处理,而后标记原本是在mid左边仍是右边的,左1右0,而后按b排序。

仍是只统计左边部分跨过中线对右边部分的贡献。

按照b排好序后,就变成了统计标记为0的点的"在它左边的、标记为1的、(c,d)都小于它的点的个数"。

"在它左边+(c,d)都小于它" = 三维偏序。

复制到另外一个数组里再作一次cdq就能够了。

复杂度O(nlog^3n)。

 

#include    <iostream>
#include    <cstdio>
#include    <cstdlib>
#include    <algorithm>
#include    <vector>
#include    <cstring>
#include    <queue>
#include    <complex>
#include    <stack>
#define LL long long int
#define dob double
#define FILE "partial_order"
//#define FILE "CDQ"
using namespace std;

const int N = 100010;
struct Data{int a,b,c,id;}p[N],que[N],que2[N];
int n,vis[N],tim,T[N];
LL Ans;

inline int gi(){
  int x=0,res=1;char ch=getchar();
  while(ch>'9'||ch<'0'){if(ch=='-')res*=-1;ch=getchar();}
  while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
  return x*res;
}

inline void update(int x){
  for(;x<=n;x+=x&-x){
    if(vis[x]!=tim)T[x]=0,vis[x]=tim;
    T[x]++;
  }
}

inline int query(int x,int ans=0){
  for(;x;x-=x&-x){
    if(vis[x]!=tim)T[x]=0,vis[x]=tim;
    ans+=T[x];
  }
  return ans;
}

inline void cdq(int l,int r){
  if(l==r)return;
  int mid=(l+r)>>1,i=l,j=mid+1,k=l;
  cdq(l,mid);cdq(mid+1,r);tim++;
  while(i<=mid && j<=r){
    if(que[i].b<que[j].b){
      if(que[i].id)update(que[i].c);
      que2[k++]=que[i++];
    }
    else{
      if(!que[j].id)Ans+=query(que[j].c);
      que2[k++]=que[j++];
    }
  }
  while(i<=mid)que2[k++]=que[i++];
  while(j<=r){
    if(!que[j].id)Ans+=query(que[j].c);
    que2[k++]=que[j++];
  }
  for(k=l;k<=r;++k)que[k]=que2[k];
}

inline void CDQ(int l,int r){
  if(l==r)return;
  int mid=(l+r)>>1,i=l,j=mid+1,k=l;
  CDQ(l,mid);CDQ(mid+1,r);
  while(i<=mid && j<=r){
    if(p[i].a<p[j].a)que[k]=p[i++],que[k++].id=1;
    else que[k]=p[j++],que[k++].id=0;
  }
  while(i<=mid)que[k]=p[i++],que[k++].id=1;
  while(j<=r)que[k]=p[j++],que[k++].id=0;
  for(k=l;k<=r;++k)p[k]=que[k];cdq(l,r);
}

int main()
{
  freopen(FILE".in","r",stdin);
  freopen(FILE".out","w",stdout);
  n=gi();
  for(int i=1;i<=n;++i)p[i].a=gi();
  for(int i=1;i<=n;++i)p[i].b=gi();
  for(int i=1;i<=n;++i)p[i].c=gi();
  CDQ(1,n);printf("%lld\n",Ans);
  fclose(stdin);fclose(stdout);
  return 0;
}
CDQ套CDQ

 

 

 

 

关于总体二分

  总体二分主要是把全部询问放在一块儿二分答案,而后把操做也一块儿分治。

  何时用呢?

  当你发现多组询问能够离线的时候

  当你发现询问能够二分答案并且check复杂度对于单组询问能够接受的时候

  当你发现询问的操做都是同样的的时候

  你就可使用总体二分这个东西了。

  具体作法讲起来有些玄学,其实相似主席树转化到区间的操做或者线段树上二分。

  想一想:二分答案的时候,对于一个答案,是否是有些操做是没用的,有些操做贡献是不变的?

  好比二分一个时间,那么时间后面发生的操做就是没有用的,时间前面的贡献是不变的。

  二分一个最大值,比mid大的都是没用的,比mid小的个数是必定的。

  总体二分就是利用了这么一个性质。

  平时咱们二分答案,都是这么写的:

 

inline int check(int mid){
  int num=0;
  for(int i=1;i<=m;++i)
    if(calc(i,mid))
      num++;
  return num;
}

...

int l=...,r=...,ans=-1;
while(l<=r){
  int mid=(l+r)>>1;
  if(check(mid)<k)l=mid+1;
  else ans=mid,r=mid-1;
}
1.0

  这种写法已经很优秀了。可是若是有q次询问,复杂度就是O(qmlogn)。

  换种方式:

 

inline bool check(int mid){
  int t1=0,t2=0;
  for(int i=1;i<=m;++i){
    if(calc(i,mid))que[1][++t1]=i;
    else que[2][++t2]=i;
  }
  if(t1>=k){
    m=t1;
    for(int i=1;i<=m;++i)opt[i]=que[1][i];
    return 1;
  }
  else{
    m=t2;
    for(int i=1;i<=m;++i)opt[i]=que[2][i];
    k-=t1;return 0;
  }
}

...

int l=...,r=...,ans=-1;
while(l<=r){
  int mid=(l+r)>>1;
  if(check(mid))r=mid-1,ans=mid;
  else l=mid+1;
}
2.0

 

  (如上面代码有错误请指出)

  分析起来复杂度并无什么改变......

  可是若是把二分答案当作一棵二叉树,每一个点(区间[l,r])的权值为check的操做数。

  把当前是第几回二分当作这个区间的深度(层)。

  每一层的区间相互没有交。

  那么有一个优秀的性质:只有log层,每一层的点权和为O(m)。

  因此这个时候对于多组询问一块儿处理,复杂度为O((m+q)logn)。

  

  二分答案,而后把没有用的操做扫进右边,和答案在[mid+1,r]的询问一块儿递归处理。

  把有用的操做放进左边,减去不变的贡献,和答案在[l,mid]的一块儿递归处理。

  注意答案在[mid+1,r]的询问要算上放进了左边的操做的贡献,开个变量记下来/直接减掉均可以。

  注意总体二分在solve内的复杂度必定只能与区间长度线性相关,不能每次都有别的复杂度!

  好比一次solve的复杂度是O(lenlogn)就能够,O(len+sqrt(n))就不行。

  大概就是这么一个东西。

  复杂度?和CDQ是同样的,都是O(f(len)logn)。

  例题?BZOJ3110 K大数查询 Codevs Meteors。

  同样的套路了。

 

关于一些要注意的地方

  归并必定要把剩下的搞完!每次我都忘记这码子事!

  树状数组不能暴力清零!记个time或者依葫芦画瓢减回去均可以,必定不能清零!

  不要在CDQ里面套sort,太慢辣!(必定进不了初版的!)

相关文章
相关标签/搜索