正經分治(一)

Part 1:簡單總結分治應用

分治算法不是一種固定的算法,確切的說,它是一種思想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\)方程同樣,須要本身思考實現

Part 2:例題梳理

洛谷P1281書的複製

傳送門

洛谷P1281書的複製

\(Solution\)

首先拿到了題面,掃了一眼以後發現了這樣一句話很扎眼,並且出題人還單獨分了一行出來給咱們看:

嗯,求最大時間最少,有二份內味了

可是不能妄下結論,先判斷一手單調性:

假設抄寫頁數最多的人花的時間是\(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\),再掃一遍整個數組,記錄下每一個人從第幾本抄到第幾本即爲最終答案

\(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); }
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平面最接近點對(兒子數據)

固然了這裏教你們「打人先打臉,擒賊先擒王,罵人先罵娘」,咱們直接拿爸爸開刀,若是切了爸爸,兒子就是雙倍經驗(爽)

\(Solution\)

不正確的作題姿式\(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;
}

感謝您的閱讀,給個三連球球辣!\(OvO\)

相關文章
相關標籤/搜索