CDQ分治與整體二分小結


前言

  這是一波強行總結。

  下面是一波瞎比比。

  這幾天做了幾道CDQ/整體二分,感覺自己做題速度好慢啊。

  很多很顯然的東西都看不出來 分治分不出來 打不出來 調不對

  上午下午晚上的效率完全不一樣啊。

  完蛋.jpg 絕望.jpg。

 

關於CDQ分治

  CDQ分治,求的是三維偏序問題都知道的。

  求法呢,就是在分治外面先把一維變成有序

  然后分治下去,左邊(l,mid)關於右邊(mid+1,r)就不存在某一維的逆序了,所以只有兩維偏序了。

  這個時候來一波"樹狀數組求逆序對"的操作搞一下二維偏序

  就可以把跨過中線的,左邊更新右邊的情況計算出來。

  注意:只計算左邊的操作對右邊的詢問的貢獻!

  然后左右兩邊遞歸處理就好了。

  正確性:按照線段樹的形態遞歸的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,太慢辣!(一定進不了第一版的!)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM