這是一波強行總結。html
下面是一波瞎比比。ios
這幾天作了幾道CDQ/總體二分,感受本身作題速度好慢啊。數組
不少很顯然的東西都看不出來 分治分不出來 打不出來 調不對數據結構
上午下午晚上的效率徹底不同啊。ide
完蛋.jpg 絕望.jpg。函數
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; }
總體二分主要是把全部詢問放在一塊兒二分答案,而後把操做也一塊兒分治。
何時用呢?
當你發現多組詢問能夠離線的時候
當你發現詢問能夠二分答案並且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; }
這種寫法已經很優秀了。可是若是有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; }
(如上面代碼有錯誤請指出)
分析起來複雜度並無什麼改變......
可是若是把二分答案當作一棵二叉樹,每一個點(區間[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,太慢辣!(必定進不了初版的!)