【信息學奧賽一本通 提高組】第二章 二分與三分


一、二分

  二分法,在一個單調有序的集合或函數中查找一個解,每次分為左右兩部分,判斷解在那個部分並調整上下界,直到找到目標元素,每次二分都將舍棄一般的查找空間,因此效率很高。

二分常見模型

  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 }
二分模板1

版本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;
}
二分模板2

 

二分使用范圍:

  必須具備單調性或者是二段性

 

參考leetcode暑假打卡活動2019——week1中

視頻鏈接:傳送門

寫二分的過程:

  1、確定二分邊界

  2、編寫二分的代碼框架

  3、設計一個check(性質)

  4、判斷一下區間如何更新

  5、如果更新方式是 L = Mid , R = Mid - 1 ,那么在算mid時要加1

 

如果答案落在綠色線上,則用模板1,否則利用模板2。

 


 

 

【例題1】憤怒的牛

題目描述

農夫約翰建造了一座有n間牛舍的小屋,牛舍排在一條直線上,第i間牛舍在xi的位置,但是約翰的m頭牛對小屋很不滿意,因此經常互相攻擊。約翰為了防止牛之間互相傷害,因此決定把每頭牛都放在離其它牛盡可能遠的牛舍。也就是要最大化最近的兩頭牛之間的距離。
牛們並不喜歡這種布局,而且幾頭牛放在一個隔間里,它們就要發生爭斗。為了不讓牛互相傷害。John決定自己給牛分配隔間,使任意兩頭牛之間的最小距離盡可能的大,那么,這個最大的最小距離是多少呢?

輸入

第一行用空格分隔的兩個整數n和m;
第二行為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的正整數序列A,求一個平均數最大的,長度不小於L的子段。

輸入

第一行用空格分隔的兩個整數n和L;
第二行為n個用空格隔開的整數,表示Ai。

輸出

輸出一個整數,表示答案的1000倍。不用四舍五入,直接輸出。

樣例輸入

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 }
Best Cow Fences

 

 二、三分

  三分法適用於求解凸性函數的極值問題,二次函數就是一個典型的單峰函數。

  三分法與二分法一樣,它會不斷縮小答案所在的求解區間,二分法縮小區間利用的原理是函數的單調性,而三分法利用的則是函數的單峰性。

  設當前求解的區間為[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個二次函數Si(x)=ax2+bx+c,他突發奇想設計了一個新的函數F(x)=max{Si(x)},i=1…n。
                                           
明明現在想求這個函數在[0,1000]的最小值,要求精確到小數點后四位,四舍五入。

 

輸入

輸入包含T組數據,每組第一行一個整數n;
接下來n行,每行3個整數a,b,c,用來表示每個二次函數的3個系數。注意:二次函數有可能退化成一次。

 

輸出

每組數據輸出一行,表示新函數 F(x)的在區間 [0,1000]上的最小值。精確到小數點后四位,四舍五入。

 

樣例輸入

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】數列分段

題目描述

對於給定的一個長度為N的正整數數列A,現要將其分成M段,並要求每段連續,且每段和的最大值最小。
例如,將數列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。

 

輸入

第1行包含兩個正整數N,M;
第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】擴散

題目描述

一個點每過一個單位時間就會向4個方向擴散一個距離,如圖所示:兩個點a、b連通,記作e(a,b),當且僅當a、b的擴散區域有公共部分。連通塊的定義是塊內的任意兩個點u、v都必定存在路徑
e(u,a0),e(a0,a1),…e(ak,v)。
給定平面上的n個點,問最早什么時候它們形成一個連通塊。
                 

輸入

第一行一個數n,以下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)

題目描述

相比 wildleopard 的家,他的弟弟 mildleopard 比較窮。他的房子是狹窄的而且在他的房間里面僅有一個燈泡。每天晚上,他徘徊在自己狹小的房子里,思考如何賺更多的錢。有一天,他發現他的影子的長度隨着他在燈泡和牆壁之間走到時發生着變化。一個突然的想法出現在腦海里,他想知道他的影子的最大長度。
                    

輸入

第一行包含一個整數T,表示測試數據的組數。
對於每組測試數據,僅一行,包含三個實數H,h和D,H表示燈泡的高度,h表示mildleopard的身高,D表示燈泡和牆的水平距離。

 

輸出

共T行,每組數據占一行表示影子的最大長度,保留三位小數。

 

樣例輸入

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】傳送帶

題目描述

在一個2維平面上有兩條傳送帶,每一條傳送帶可以看成是一條線段。兩條傳送帶分別為線段AB和線段CD。lxhgww在AB上的移動速度為P,在CD上的移動速度為Q,在平面上的移動速度R。現在lxhgww想從A點走到D點,他想知道最少需要走多長時間

 

輸入

第一行是4個整數,表示A和B的坐標,分別為Ax,Ay,Bx,By
第二行是4個整數,表示C和D的坐標,分別為Cx,Cy,Dx,Dy
第三行是3個整數,分別是P,Q,R


輸出

一行,表示lxhgww從A點走到D點的最短時間,保留到小數點后2位

樣例輸入

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 }
傳送帶

 

 


免責聲明!

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



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