分治算法不是一種固定的算法,確切的說,它是一種思想ios
總結一下幾種經常使用的分治算法算法
二分法在以前的分治博客中已經提到過了,這裏僅做簡單的補充描述數組
首先,二分法在求最優解問題上有普遍的應用,若是一個題目提到了「存在多個答案輸出最優解」,那麼有很大的機率要運用二分數據結構
其次,也是使用二分法須要注意的點:對於一個有單調性的答案區間,咱們才能使用二分法進行求解函數
單調性:對於有解區間內的任意值\(x_1,x_2(x_1<x_2)\),帶入求解獲得解\(y_1,y_2\),那麼\(y_1,y_2\)的大小關係必定能夠肯定spa
換句話說,就是解\(y\)關於自變量\(x\)的函數,在定義域(有解區間)內,具備單調性設計
使用\(while\)或者遞歸實現二分比較方便,這裏由於我的習慣\(while\)就發一個\(while\)版本的(PS:遞歸有常數)code
int l=/*二分下界,答案可能出現的最小值*/,r=/*二分上界,答案可能出現的最大值*/ while(l<=r){ int Mid=(l+r)>>1; if(check(Mid)){ r=mid-1;//或者l=mid+1,看狀況而定 //更新答案 }else l=Mid+1; } //最後的最優解是l
這裏的\(check()\)函數是精髓,須要根據題目來編寫,用來判斷這個解合不合法,而後縮小答案所在區間的範圍,逐步求解blog
通常是\(O(klogn)\),其中\(k\)是\(check()\)函數的複雜度排序
其實二分屬於分治,可是二分須要具備單調性,分治卻不須要,因此這裏才分開來說
分治的應用範圍很是普遍,對於一個大問題能夠不斷按照某種規則劃分紅幾個小問題,而一些小問題能夠直接求解的模型,能夠考慮使用分治法
分治的典型應用就是各類\(ex\)數據結構(好比線段樹、\(st\)表、二叉堆等等)
這個真的沒啥模板了,就像\(DP\)方程同樣,須要本身思考實現
首先拿到了題面,掃了一眼以後發現了這樣一句話很扎眼,並且出題人還單獨分了一行出來給咱們看:
嗯,求最大時間最少,有二份內味了
可是不能妄下結論,先判斷一手單調性:
假設抄寫頁數最多的人花的時間是\(x\),那麼顯然其餘人的抄寫數量要\(\leq x\)
由於其餘人抄書數量在\(\leq x\)時,不對答案形成影響,因此設全部人最多能抄書的頁數爲\(y\),那麼\(y\)在當\(x\)增大時必定增大,當\(x\)減少時必定減少
換句話說,只要抄的總量不超過\(x\),每一個人想抄多少抄多少,對答案沒有影響。
爲了全部人抄的總量最大,貪心的想,每一個人要儘量多抄,假設每一個人都抄了\(x\),那麼有\(y=kx\)(\(k\)是人數),這個函數顯然具備單調性
綜上所述,本題可使用二分法求解
明確了\(y\)與\(x\)之間的關係具備單調性,如今要求\(x\)的最小值,考慮二分\(x\)的值
每二分一次,咱們都要檢查一下這個答案是否是合法(也就是設計\(check()\)函數)
題目中要求\(k\)我的去抄\(m\)本書,由於咱們二分了\(x\)的值,因此每一個人就最多抄\(x\)頁
掃一遍整個表明每本書的頁數的數組,若是一我的已經抄了\(i\)頁,下一本書是\(j\)頁
若是\(i+j\leq x\)說明這我的再抄一本書也不會超過\(x\),根據每一個人多抄的思路,咱們把下一本書也給這我的抄
若是\(i+j>x\)說明這我的再抄下一本書,他就超過那個每一個人最多抄\(x\)的限制了,此時咱們要再新找一我的來抄這\(j\)頁
掃到最後,看看這\(k\)我的在每一個人抄寫不超過\(x\)頁的狀況下能不能把全部書都抄完
若是抄不完,根據單調性,說明\(x\)取小了,若是抄完了,說明\(x\)可能取大了,更新答案,繼續二分\(x\)
題目還要求後面的人多抄,那麼咱們只要在統計答案的時候,改成從後向前掃描,依舊貪心地分配任務(儘量多抄)
二分結束後求出了最優的\(x\),再掃一遍整個數組,記錄下每一個人從第幾本抄到第幾本即爲最終答案
#include<cstdio> #include<cstring> #include<queue> #include<stack> #include<algorithm> #include<set> #include<map> #include<utility> #include<iostream> #include<list> #include<ctime> #include<cmath> #include<cstdlib> #include<iomanip> typedef long long int ll; inline int read(){ int fh=1,x=0; char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); } while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); } return fh*x; } inline int _abs(const int x){ return x>=0?x:-x; } inline int _max(const int x,const int y){ return x>=y?x:y; } inline int _min(const int x,const int y){ return x<=y?x:y; } inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; } inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); } const int maxn=505; int seq[maxn]; struct Node{ int st,ed; }wk[maxn]; int n,k,l,r; //l,r二分最快須要多少時間完成工做 inline bool check(const int Mid){ int t=0,cnt=0;//t記錄這我的抄了t頁,cnt記錄用了cnt我的 for(int i=1;i<=n;i++){ if(t+seq[i]<=Mid) t+=seq[i];//合法,分配 else t=seq[i],cnt++;//超過了二分的值,新找一我的 if(cnt>k) return false;//若是用人數已經大於k個了,直接返回false } cnt++;//最後一點任務要分配一我的來抄 if(cnt<=k) return true; else return false; } inline void make_ans(const int ans){//最終答案是ans,從後向前貪心分配每一個人的工做 int t=0,cnt=k;//從後往前安排 wk[cnt].ed=n;//最後一我的的最後一本書是n wk[1].st=1;//第一我的的第一本書是1 for(int i=n;i>=1;i--){ if(t+seq[i]<=ans) t+=seq[i];//抄第i本沒有超過ans,貪心分配 else{ wk[cnt].st=i+1;//記錄這我的抄到了第i+1本(由於是倒序枚舉的) cnt--;//找一個新的人 wk[cnt].ed=i;//新的人的最後一本書是第i本 t=seq[i];//新的人已經抄了i頁 } } } int main(){ n=read(),k=read(); for(int i=1;i<=n;i++){ seq[i]=read(); l=_max(l,seq[i]); r+=seq[i];//更新答案區間上下界 } while(l<=r){ int x=(l+r)>>1;//套板子 if(check(x)) r=x-1; else l=x+1; } make_ans(l);//統計答案 for(int i=1;i<=k;i++) printf("%d %d\n",wk[i].st,wk[i].ed); return 0; }
傳送門\(1\):洛谷P1429平面最接近點對(爸爸數據)
傳送門\(2\):洛谷P1257平面最接近點對(兒子數據)
固然了這裏教你們「打人先打臉,擒賊先擒王,罵人先罵娘」,咱們直接拿爸爸開刀,若是切了爸爸,兒子就是雙倍經驗(爽)
不正確的作題姿式\(1\)
某數據結構巨佬:我喜歡暴力數據結構,因此我用\(KD-Tree\)的板子一邊喝水一邊過了此題(我要是出題人我立刻給你卡成\(O(n^2)\)的,讓你裝13)
不正確的作題姿式\(2\)
俗話說的好:「\(n\)方過百萬,暴力碾標算,管他什麼\(SA\),\(rand()\),能\(AC\)的算法就是好算法」
因而某玄學大師:「啊?我就按照\(x\)排了個序,若是要求最短,確定橫座標差不會太大,因此我枚舉每一個點以前,以後的\(10\)個點,更新答案,而後就\(AC\)了……」
正確的作題姿式:
好習慣:打開題目看到數據範圍——\(n\leq 2*10^5\),猜到正解大概是一個複雜度爲\(O(nlogn)\)的算法
首先,題目隨機給定\(n\)個點的座標,先無論三七二十一,拿個結構體存下來一點也不虧
再考慮,如何更快的求解最小距離?忽然想到了最小值具備結合律——即整個區間的最小值等於把它分紅兩個子區間,這兩個子區間的最小值(好比線段樹\(RMQ\))
可是如今手裏的數組是無序的,這樣無法劃分區域,因此咱們須要先排序,爲了更直觀些,我選擇了按照橫座標\(x\)從小到大排序
假設咱們有這樣\(5\)個點,能夠按照這\(5\)個點組成平面中,最中間的那個點,劃分紅左平面和右平面(中間點歸左平面管)
這樣一直遞歸的劃分下去,直到一個平面裏只有\(2\)到\(3\)個點,此時咱們暴力求出點距,而後向上合併平面統計\(ans=min(ans,min(left,right));\)
簡直\(Perfect\),難道這題就這嗎?顯然不是,有一種狀況被咱們忽略了——產生最小點距的兩個點可能來自不一樣的平面
如今處理這種特殊狀況,設左平面和右平面中最接近點對的距離爲\(d\),顯然,只有橫座標與中間點橫座標距離不超過\(d\)的點纔可能更新答案
形象點說,就是這樣:
已知\(d\)的大小,若是一個左平面的點到中間點橫座標的距離已經\(>d\),那麼它想和右邊的點結合,此時兩點距離必定\(>d\),不可能更新答案,對於右平面也是這樣
因此對於上圖,咱們只用枚舉\(三、四、5\),也就是與中間點\(4\)橫座標距離小於等於\(d\)的點便可
可是這麼作,複雜度仍然有可能退化到\(O(n^2)\),由於可能有這種數據:
當卡在判斷距離以內的點過多時,這麼作其實和純種的暴力沒有區別了,怎麼辦呢?(可是對於這個題來講已經足夠了,吸個氧就過掉了)
發動人類智慧精華:既然咱們能夠按照橫座標排序劃分,爲何不能按縱座標排序劃分呢?
咱們先\(O(n)\)把距離中間點橫座標距離\(\leq d\)的點全都統計出來到一個數組\(T\)裏,而後把數組\(T\)按照縱座標排序
那麼咱們找可能更新答案的點對時,還能夠以縱座標做爲約束,這樣,根據某玄學的數學證實方法(鴿巢原理),枚舉次數不會超過\(6\)?
這樣再更新一次答案,不停合併直到獲得整個平面的最接近點對便可
Code
#include<cstdio> #include<cstring> #include<queue> #include<stack> #include<algorithm> #include<set> #include<map> #include<utility> #include<iostream> #include<list> #include<ctime> #include<cmath> #include<cstdlib> #include<iomanip> typedef long long int ll; inline int read(){ int fh=1,x=0; char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); } while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); } return fh*x; } inline int _abs(const int x){ return x>=0?x:-x; } inline int _max(const int x,const int y){ return x>=y?x:y; } inline int _min(const int x,const int y){ return x<=y?x:y; } inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; } inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); } inline double _dis(const double x1,const double y1,const double x2,const double y2){ return std::sqrt(std::pow(x1-x2,2)+std::pow(y1-y2,2)); } inline double _smin(const double a,const double b,const double c){ return std::min(a,std::min(b,c)); } const int maxn=200005; struct Node{ double x,y; }poi[maxn]; inline bool cmd1(const Node a,const Node b){ return a.x<b.x; } inline bool cmd2(const Node a,const Node b){ return a.y<b.y; } int n; double find(const int L,const int R){ if(R==L+1) return _dis(poi[L].x,poi[L].y,poi[R].x,poi[R].y); if(R==L+2) return _smin(_dis(poi[L].x,poi[L].y,poi[L+1].x,poi[L+1].y),_dis(poi[L+1].x,poi[L+1].y,poi[R].x,poi[R].y),_dis(poi[L].x,poi[L].y,poi[R].x,poi[R].y)); //點數<=3暴力統計 int Mid=(L+R)>>1;//按x二分平面 double d=std::min(find(L,Mid),find(Mid+1,R));//找最小值 int cnt=0; Node T[1005]; for(int i=L;i<=R;i++) if(poi[i].x>=poi[Mid].x-d&&poi[i].x<=poi[Mid].x+d){ T[++cnt].x=poi[i].x; T[cnt].y=poi[i].y; }//O(n)統計全部可能更新答案的點 std::sort(T+1,T+cnt+1,cmd2);//按y排個序 for(int i=1;i<=cnt;i++)//枚舉全部可能更新答案的點 for(int j=i+1;j<=cnt;j++){ if(T[j].y-T[i].y>=d) break;//若是縱座標之差超過d,顯然不可能更新答案,跳出 d=std::min(d,_dis(T[i].x,T[i].y,T[j].x,T[j].y));//嘗試更新答案 } return d;//返回平面最小值d } int main(){ n=read(); for(int i=1;i<=n;i++) poi[i].x=read(),poi[i].y=read();//讀入點對 std::sort(poi+1,poi+n+1,cmd1);//按照x排序 double ans=find(1,n); printf("%.4lf",ans);//lf精度別忘了 return 0; }