一、二分
二分法,在一個單調有序的集合或函數中查找一個解,每次分為左右兩部分,判斷解在那個部分並調整上下界,直到找到目標元素,每次二分都將舍棄一般的查找空間,因此效率很高。
二分常見模型
1、二分答案
最小值最大(或是最大值最小)問題,這類雙最值問題常常選用二分法求解,也就是確定答案后,配合貪心,DP等其他算法檢驗這個答案是否合理,將最優化問題轉化為判定性問題。例如,將長度為n的序列ai分為最多m個連續段,求所有分法中每段和的最大值的最小是多少?
2、二分查找
用具有單調性的布爾表達式求解分界點,比如在有序數列中求數字x的排名。
3、代替三分
有時,對於一些單峰函數,我們可以用二分導函數的方法求解函數的極值,這時通常將函數的定義域定義為整數域求解比較方便,此時dx可以直接去整數1。
二分寫法
參考博客:傳送門
版本1
當我們將區間[l, r]划分成[l, mid]和[mid + 1, r]時,其更新操作是r = mid或者l = mid + 1;,計算mid時不需要加1。
C++ 代碼模板:

1 int bsearch_1(int l, int r) 2 { 3 while (l < r) 4 { 5 int mid = l + r >> 1; 6 if (check(mid)) r = mid; 7 else l = mid + 1; 8 } 9 return l; 10 }
版本2
當我們將區間[l, r]划分成[l, mid - 1]和[mid, r]時,其更新操作是r = mid - 1或者l = mid;,此時為了防止死循環,計算mid時需要加1。
C++ 代碼模板:

int bsearch_2(int l, int r) { while (l < r) { int mid = l + r + 1 >> 1; if (check(mid)) l = mid; else r = mid - 1; } return l; }
二分使用范圍:
必須具備單調性或者是二段性
參考leetcode暑假打卡活動2019——week1中
視頻鏈接:傳送門
寫二分的過程:
1、確定二分邊界
2、編寫二分的代碼框架
3、設計一個check(性質)
4、判斷一下區間如何更新
5、如果更新方式是 L = Mid , R = Mid - 1 ,那么在算mid時要加1
如果答案落在綠色線上,則用模板1,否則利用模板2。
【例題1】憤怒的牛
題目描述
牛們並不喜歡這種布局,而且幾頭牛放在一個隔間里,它們就要發生爭斗。為了不讓牛互相傷害。John決定自己給牛分配隔間,使任意兩頭牛之間的最小距離盡可能的大,那么,這個最大的最小距離是多少呢?
輸入
第二行為n個用空格隔開的整數,表示位置xi。
輸出
樣例輸入
5 3
1 2 8 4 9
樣例輸出
3
提示
把牛放在1,4,8這樣最小距離是3
2≤n≤1e5 , 0≤xi≤1e9, 2≤m≤n
【思路】:
類似的最大值最小化問題,通常用二分法就可以很快地解決。我們定義:
設C(d)表示可以安排牛的位置,並使得最近的兩頭牛的距離不小於d。
那么問題就轉換為求滿足C(d)的最大的d,另外,最近的間距不小於d也可以看成是所有牛的間距都小於d,因此就可以用C(d)表示可以安排牛的位置,並使得任意的兩頭牛的距離不小於d。
對於這個問題的判斷,使用貪心法便可非常容易地求解
1、對牛舍的位置x進行排序。
2、把第一頭牛放入x0的牛舍。
3、如果第i頭牛放入了xj間牛舍,則第i+1頭牛就要放入滿足xj+d<=xk的最小牛舍xk中。

1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef long long ll; 4 const int N = 1e5+100; 5 const ll Inf = (1ll<<20); 6 ll a[N],A,B; 7 bool check(ll x){ 8 ll tot = 1 ; 9 ll pre = 0; 10 for(int i=1;i<A;i++){ 11 if( a[i] - a[pre] >= x ){ 12 pre = i; 13 tot++; 14 } 15 } 16 if( tot >= B ) return true; 17 else return false; 18 } 19 int main() 20 { 21 ll L = 1, R = 0 , mid , ans = 0 ; 22 scanf("%lld%lld",&A,&B); 23 for(int i=0;i<A;i++){ 24 scanf("%lld",&a[i]); 25 } 26 sort( a , a+A ); 27 R = a[A-1] - a[0] ; 28 while( L<=R ){ 29 mid = (L+R) >> 1 ; 30 if( check( mid ) ){ 31 ans = mid ; 32 L = mid + 1 ; 33 }else{ 34 R = mid - 1 ; 35 } 36 } 37 return 0*printf("%lld\n",ans); 38 }
【例題2】Best Cow Fences
題目描述
輸入
第二行為n個用空格隔開的整數,表示Ai。
輸出
樣例輸入
10 6
6 4 2 10 3 8 5 9 4 1
樣例輸出
6500
提示
1≤n≤1e5 , 1≤Ai≤2000
【注意】:這里指的序列並不是(我們字符串的序列,好比最長上升子序列),而是(子串)性的序列。
【思路】:
二分結果,判斷“是否存在一個長度不小於L的子序列,平均數不小於二分的值”
如果把數列中每個數都減去二分的值,就轉化為判定“是否存在一個長度不小於的L的子序列,子序列的和非負”。
下面着重解決兩個問題:
1、求一個子序列,它的和最大,沒有“長度不小於L”的限制
無長度限制的最大子序列和問題是一個經典的問題,只需O(n)掃描該數列,不斷地把新的數加入子序列,當子序列和變成負數時,把當前整個子序列清空,掃描過程中出現過的最大子序列和即為所求。
2、求一個子序列,它的和最大,子序列的長度不小於L。
子序列和可以轉化為前綴和相減的形式,即設sum_i 表示 A1~Ai的和,則有:
max i-j >=t { Aj+1+Aj+2+……+Ai } = max L <= i <= n { sum - min 0<= j <= i-L {sum j } }
仔細觀察上面的式子可以發現,隨着i的增長,j的取值范圍0~i-L每次只會增加1。換而言之,每次只會有一個新的值假如min{sum j }的候選集合,所以我們沒有必要每次循環枚舉j次,只需要用一個變量記錄當前最小值,每次與新的取值sum i-L 取min就可以了。
解決問題2后,我們只需要判斷最大子序列和是不是非負數,就可以確定二分上下界的變化范圍了。

1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N = 1e5+10 ; 4 const double eps = 1e-6 ; 5 typedef long long ll; 6 double a[N],b[N],sum[N]; 7 int n,m; 8 void Input(){ 9 cin >> n >> m ; 10 for ( int i=1 ; i<=n ; i++ ){ 11 cin >> a[i] ; 12 } 13 } 14 int main() 15 { 16 ios_base :: sync_with_stdio(0) ; 17 cin.tie(NULL) ; cout.tie(NULL) ; 18 19 Input() ; 20 21 double L = -1e6 , R = 1e6 , Mid , Ans , Minz ; 22 while ( R-L > eps ){ 23 Mid = (L+R) / 2 ; 24 for ( int i = 1 ; i<=n ; i++ ) { 25 b[i] = a[i] - Mid ; 26 sum[i] = sum[i-1] + b[i] ; 27 } 28 Ans = -1e10 ; 29 Minz = 1e10 ; 30 for ( int i = m ; i<=n ; i++ ){ 31 Minz = min( Minz , sum[i-m] ); 32 Ans = max ( Ans , sum[i] - Minz ) ; 33 } 34 if ( Ans >= 0 ) L = Mid ; 35 else R = Mid ; 36 } 37 cout << int ( R * 1000 ) << endl ; 38 return 0; 39 }
二、三分
三分法適用於求解凸性函數的極值問題,二次函數就是一個典型的單峰函數。
三分法與二分法一樣,它會不斷縮小答案所在的求解區間,二分法縮小區間利用的原理是函數的單調性,而三分法利用的則是函數的單峰性。
設當前求解的區間為[L,R],令 m1 = L + (R-L) / 3 ,m2 = R - (R-L) / 3 , 接着我們計算這兩個點的函數值f(m1),f(m2),之后我們將兩點中函數值更優的那個點成為好點(若計算最大值,則f更大的那個點就為好點,計算最小值同理),而函數值較差的那個點就稱為壞點。
我們可以證明,最優點可能會與好點或壞點同側。
如m1是好點,則m2是壞點。
因此,最后的最優點會 與m1 與m2的左側 ,即我們求解的區間由 [L,R] 變為 [ L,m2 ] ,因此根據這個結論我們可以不停縮小求解區間,直到求出近似值。

1 double L = 0 , R = 1e9 ; 2 while ( R - L <= 1e-3 ){ 3 double M1 = L + (R-L)/3 ; 4 double M2 = R - (R-L)/3 ; 5 if ( F(M1) < F(M2) ) 6 L = M1 ; 7 else 8 R = M2 ; 9 }
【例題3】曲線(curves)
題目描述

輸入
接下來n行,每行3個整數a,b,c,用來表示每個二次函數的3個系數。注意:二次函數有可能退化成一次。
輸出
樣例輸入
2
1
2 0 0
2
2 0 0
2 -4 2
樣例輸出
0.0000
0.5000
提示
對於50%的數據,1≤n≤100;
對於100%的數據,1≤T≤10,1≤n≤1e5,0≤a≤100, 0≤∣b∣≤5000,0≤∣c∣≤5000。
【思路】
由於函數S是開口向上的二次函數(當a=0時,是一次函數),由S的定義可知,S或者是一個先單調減、后單調增的下凸函數,或者是一個單調函數,F(x)=max(S(x))也滿足單調性。選用三分法很容易求得某個區間內的最小值。
【代碼】:

1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N = 1e4+100; 4 const double EPS = 1e-11 ; 5 int n; 6 double a[N],b[N],c[N]; 7 double F(double x) { 8 double maxz = -0x7fffffff; 9 for ( int i=1 ; i<=n ; i++ ){ 10 maxz = max ( maxz , a[i]*x*x + b[i]*x + c[i] ); 11 } 12 return maxz ; 13 } 14 int main(){ 15 int T ; 16 scanf("%d",&T); 17 while(T--){ 18 scanf("%d",&n); 19 for(int i=1;i<=n;i++){ 20 scanf("%lf%lf%lf",&a[i],&b[i],&c[i]); 21 } 22 double L = 0 , R = 1000, Lmid , Rmid ; 23 while ( R - L > EPS ) { 24 Lmid = L + (R-L) / 3 ; 25 Rmid = R - (R-L) / 3 ; 26 if ( F(Lmid) <= F(Rmid) ){ 27 R = Rmid ; 28 }else{ 29 L = Lmid ; 30 } 31 } 32 printf("%.4f\n",F(L) ) ; 33 } 34 return 0 ; 35 }
【習題1】數列分段
題目描述
例如,將數列4 2 4 5 1要分成3段:
若分為[4 2][4 5][1],各段的和分別為6,9,1,和的最大值為9;
若分為[4][2 4] [5 1],各段的和分別為4,6,6,和的最大值為6;
並且無論如何分段,最大值不會小於6。
所以可以得到要將數列4 2 4 5 1要分成3段,每段和的最大值最小為6。
輸入
第2行包含N個空格隔開的非負整數Ai,含義如題目所述。
輸出
樣例輸入
5 3
4 2 4 5 1
樣例輸出
6
提示
對於100%的數據,有N≤106,M≤N,Ai之和不超過109
【思路】
這個題目類似與憤怒的牛,求最大值的最小值,
二分答案,然后判斷是否正確。

1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef long long ll; 4 const int N = 1e5+100; 5 ll a[N],n,m; 6 bool check ( ll x ){ 7 int cnt = 1 ; 8 ll sum = 0; 9 for (int i=1;i<=n;i++){ 10 if ( sum + a[i] > x ){ 11 cnt ++ ; 12 sum = a[i] ; 13 }else{ 14 sum += a[i] ; 15 } 16 } 17 return cnt <= m ; 18 } 19 int main() 20 { 21 ll L = -0x7fffffff , R = 0 ; 22 scanf("%lld%lld",&n,&m); 23 for(int i=1;i<=n;i++){ 24 scanf("%lld",&a[i]); 25 L = max ( a[i],L) ; 26 R += a[i] ; 27 } 28 ll mid , ans ; 29 while( L<=R ){ 30 mid = L+R >> 1 ; 31 if( check(mid) ){ 32 R = mid - 1 ; 33 ans = mid ; 34 }else{ 35 L = mid + 1 ; 36 } 37 } 38 printf("%lld\n",ans); 39 return 0; 40 }
【習題2】擴散
題目描述
e(u,a0),e(a0,a1),…e(ak,v)。
給定平面上的n個點,問最早什么時候它們形成一個連通塊。

輸入
輸出
樣例輸入
2
0 0
5 5
樣例輸出
5
提示
對於100%的數據,滿足1≤n≤50,1≤Xi,Yi≤1e9。
【思路】:
這個題目其實用了兩方面:
1、連通塊需要用並查集來完成。
2、一個點的擴散,就像一個菱形在坐標外擴散,然后只有當 兩個點在t時間后相通即為:曼哈頓距離小於等於2t。
【代碼】:

1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef long long ll; 4 const int N = 55; 5 ll x[N],y[N]; 6 int pre[N]; 7 int Find(int x){ 8 return pre[x] = ( x==pre[x] ? x : Find(pre[x]) ); 9 } 10 int n; 11 void Init(){ 12 scanf("%d",&n); 13 for(int i=1;i<=n;i++){ 14 scanf("%lld%lld",&x[i],&y[i]) ; 15 } 16 } 17 int vis[55]; 18 bool check(ll val){ 19 int cnt = 0 ; 20 memset ( vis, 0 , sizeof vis ); 21 22 for (int i=1 ;i<=n;i++) pre[i] = i ; 23 for(int i=1;i<=n;i++){ 24 for(int j=i+1;j<=n;j++){ 25 if( abs(x[i]-x[j]) + abs(y[i]-y[j]) <= 2*val ){ 26 int Fu = Find(i) ; 27 int Fv = Find(j) ; 28 if( Fu != Fv ) { 29 pre[Fv] = Fu ; 30 } 31 } 32 } 33 } 34 35 for(int i=1;i<=n;i++){ 36 cnt += (pre[i]==i); 37 } 38 return cnt == 1 ; 39 } 40 int main() 41 { 42 Init(); 43 ll L = 0 , R = 1e9 , Mid ,ans =0 ; 44 while ( L<=R ){ 45 Mid = L+R >> 1 ; 46 if ( check(Mid) ){ 47 R = Mid - 1 ; 48 ans = Mid ; 49 }else{ 50 L = Mid + 1 ; 51 } 52 } 53 printf("%lld\n",ans ); 54 return 0; 55 } 56 /* 57 58 5 59 5 5 60 7 8 61 6 8 62 5 8 63 4 6 64 65 */
【習題3】燈泡(ZOJ 3203)
題目描述

輸入
對於每組測試數據,僅一行,包含三個實數H,h和D,H表示燈泡的高度,h表示mildleopard的身高,D表示燈泡和牆的水平距離。
輸出
樣例輸入
3
2 1 0.5
2 0.5 3
4 3 4
樣例輸出
1.000
0.750
4.000
提示
T≤100,10^−2≤H,h,D≤1e3,10^−2≤H−h
【題解】

1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef long long ll; 4 const double eps = 1e-7; 5 double H,h,D; 6 double F(double x){ 7 double L = H - (H-h)*D / x ; 8 return D-x+L; 9 } 10 int main() 11 { 12 int T; 13 scanf("%d",&T); 14 while(T--){ 15 scanf("%lf%lf%lf",&H,&h,&D); 16 double L = D*(H-h)/H , R = D ; 17 while ( R-L >= eps ){ 18 double Lmid = L + (R-L)/3 ; 19 double Rmid = R - (R-L)/3 ; 20 if ( F(Lmid) <= F(Rmid) ){ 21 L = Lmid ; 22 }else{ 23 R = Rmid ; 24 } 25 } 26 printf("%.3f\n",F(L)); 27 } 28 return 0 ; 29 }
【習題4】傳送帶
題目描述
輸入
第二行是4個整數,表示C和D的坐標,分別為Cx,Cy,Dx,Dy
第三行是3個整數,分別是P,Q,R
輸出
樣例輸入
0 0 0 100
100 0 100 100
2 2 1
樣例輸出
136.60
提示
對於100%的數據,1<= Ax,Ay,Bx,By,Cx,Cy,Dx,Dy<=1000,1<=P,Q,R<=10
【題解】:
分三部分來計算,三分套三分,第一個三分是分AB段的,第二個三分是分CD段的,用百分比來進行三分,最后得到的點連接。
個人感覺這個題目 寫倒不是什么問題,關鍵是可能沒想出來還能這樣用百分比來寫。

1 #include<bits/stdc++.h> 2 using namespace std; 3 const double eps = 1e-6; 4 double Ax,Ay,Bx,By,Cx,Cy,Dx,Dy ; 5 double P , Q , R ; 6 typedef struct node{ 7 double x,y; 8 }point ; 9 point A,B,C,D,p1,p2; 10 double dis ( point u , point v ){ 11 return sqrt( (u.x - v.x)*(u.x - v.x) + 12 (u.y - v.y)*(u.y - v.y) ) ; 13 } 14 double Cal ( double u , double v ){ 15 p1.x = Ax + (Bx-Ax)*u ; 16 p1.y = Ay + (By-Ay)*u ; 17 18 p2.x = Cx + (Dx-Cx)*v ; 19 p2.y = Cy + (Dy-Cy)*v ; 20 21 return dis(A,p1) / P + dis(p1,p2) / R + dis(p2,D) /Q ; 22 } 23 double F (double u) { 24 double L = 0 , R = 1 ; 25 double Lmid ,Rmid ; 26 while ( R-L >= eps ){ 27 Lmid = L + ( R-L )/3 ; 28 Rmid = R - ( R-L )/3 ; 29 if( Cal(u,Lmid) <= Cal(u,Rmid) ){ 30 R = Rmid ; 31 }else{ 32 L = Lmid ; 33 } 34 } 35 return Cal(u,L); 36 } 37 int main() 38 { 39 ios_base :: sync_with_stdio(0) ; 40 cin.tie(NULL) ; 41 cout.tie(NULL); 42 43 cin >> Ax >> Ay >> Bx >> By >> Cx >> Cy >> Dx >> Dy ; 44 cin >> P >> Q >> R ; 45 A.x = Ax ; A.y = Ay ; 46 B.x = Bx ; B.y = By ; 47 C.x = Cx ; C.y = Cy ; 48 D.x = Dx ; D.y = Dy ; 49 double L = 0 , R = 1 ; 50 double Lmid ,Rmid ; 51 while ( R-L >= eps ){ 52 Lmid = L + ( R-L )/3 ; 53 Rmid = R - ( R-L )/3 ; 54 if( F(Lmid) <= F(Rmid) ){ 55 R = Rmid ; 56 }else{ 57 L = Lmid ; 58 } 59 } 60 printf("%.2f\n",F(L)); 61 return 0; 62 }