算法競賽專題解析(1):二分法、三分法

本系列是這本算法教材的擴展:《算法競賽入門到進階》(京東 噹噹) 清華大學出版社 PDF下載地址:https://github.com/luoyongjun999/code 其中的「補充資料」 若有建議,請聯繫:(1)QQ 羣,567554289;(2)做者QQ,15512356php

[toc]html

  二分法和三分法是算法競賽中常見的算法思路,本文介紹了它們的理論背景、模板代碼、典型題目。c++

<font size=4>1. 二分法的理論背景</font>

  在《計算方法》教材中,關於非線性方程的求根問題,有一種是二分法。   方程求根是常見的數學問題,知足方程:           $f(x) = 0$              (1-1)   的數$x'$稱爲方程(1-1)的根。   所謂非線性方程,是指$f(x)$中含有三角函數、指數函數或其餘超越函數。這種方程,很難或者沒法求得精確解。不過,在實際應用中,只要獲得知足必定精度要求的近似解就能夠了,此時,須要考慮2個問題:   (1)根的存在性。用這個定理斷定:設函數在閉區間$[a, b]$上連續,且$f(a) ∙ f(b) < 0$,則$f(x) = 0$存在根。   (2)求根。通常有兩種方法:搜索法、二分法。   搜索法:把區間$[a, b]$分紅$n$等份,每一個子區間長度是∆x,計算點$x_k = a + k∆x$, $(k=0,1,2,3,4,...,n)$的函數值$f(x_k)$,若$f(x_k) = 0$,則是一個實根,若相鄰兩點知足$f(x_k) ∙ f(x_{k+1}) < 0$,則在$(x_k, x_{k+1})$內至少有一個實根,能夠取($x_k+ x_{k+1})/2$爲近似根。   二分法:若是肯定$f(x)$在區間$[a, b]$內連續,且$f(a) ∙ f(b) < 0$,則至少有一個實根。二分法的操做,就是把$[a, b]$逐次分半,檢查每次分半後區間兩端點函數值符號的變化,肯定有根的區間。   什麼狀況下用二分?兩個條件:<font color=#FF0033>上下界$[a, b]$肯定、函數在$[a, b]$內單調</font>。git

<div align=center><img src="https://img2020.cnblogs.com/blog/1954999/202003/1954999-20200304125222269-1547842922.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkxNDU5Mw==,size_16,color_FFFFFF,t_70" width="30%"></div> <center>圖1.1 單調函數</center>github

  複雜度:通過n次二分後,區間會縮小到$(b - a)/2^n$。給定$a$、$b$和精度要求$ε$,能夠算出二分次數$n$,即知足$(b - a)/2^n <ε$。因此,二分法的複雜度是$O(logn)$的。例如,若是函數在區間$[0, 100000]$內單調變化,要求根的精度是$10^{-8}$,那麼二分次數是44次。   二分很是高效。因此,若是問題是單調性的,且求解精確解的難度很高,能夠考慮用二分法。   在算法競賽題目中,有兩種題型:整數二分、實數二分。整數域上的二分,注意終止邊界、左右區間的開閉狀況,避免漏掉答案或者死循環。實數域上的二分,須要注意精度問題。算法

<font size=4>2. 整數二分模板 </font>

<font size=3>2.1 基本形式</font>

  先看一個簡單問題:在有序數列a[]中查找某個數x;若是數列中沒有x,找它的後繼。經過這個問題,給出二分法的基本代碼。   若是有x,找第一個x的位置;若是沒有x,找比x大的第一個數的位置。數組

<div align=center><img src="https://img2020.cnblogs.com/blog/1954999/202003/1954999-20200304111020679-183077438.jpg" width="60%"></div> <center> 圖2.1 (a)數組中有x         (b)數組中沒有x </center>數據結構

  示例:a[] = {-12,-6,-4,3,5,5,8,9},其中有n = 8個數,存儲在a[0]~a[7]。   1)查找x = -5,返回位置2,指向a[2] = -4;   2)查找x = 7,返回位置6,指向a[6] = 8;   3)特別地,若是x 大於最大的a[7] = 9,例如x = 12,返回位置8。因爲不存在a[8],因此此時是越界的。   下面是模板代碼。函數

<center>查找大於等於 x的最小的一個的位置(x或者x的後繼)</center> ```c int bin_search(int *a, int n, int x){ //a[0]~a[n-1]是單調遞增的 int left = 0, right = n; //注意:不是 n-1 while (left < right) { int mid = left + (right-left)/2; //int mid = (left + right) >> 1; if (a[mid] >= x) right = mid; else left = mid + 1; } //終止於left = right return left; //特殊狀況:a[n-1] < x時,返回n } ```學習

  下面對上述代碼進行補充說明:   (1)代碼執行完畢後,left==right,二者相等,即答案所處的位置。   (2)複雜度:每次把搜索的範圍縮小一半,總次數是log(n)。   (3)中間值mid    中間值寫成mid = left + (right-left)/2 或者mid = (left + right) >> 1都行 [參考李煜東《算法競賽進階指南》26頁,有mid = (left + right) >> 1的細節解釋]。不過,若是left + right很大,可能溢出,用前一種更好。   不能寫成 mid = (left + right)/2; 在有負數的狀況下,會出錯。   (4)對比測試   bin_search()和STL的lower_bound()的功能是同樣的。下面的測試代碼,比較了bin_search()和lower_bound()的輸出,以此證實bin_search()的正確性。注意,當a[n-1]<key時,lower_bound()返回的也是n。   代碼執行如下步驟:   1)生成隨機數組a[];   2)用sort()排序;   3)生成一個隨機的x;   4)分別用bin_search()和lower_bound()在a[]中找x;   5)比較它們的返回值是否相同。

<center>bin_search()和lower_bound()對比測試</center>

#include<bits/stdc++.h>
using namespace std;
#define MAX   100    //試試10000000
#define MIN  -100
int a[MAX];          //若是MAX超過100萬,大數組a[MAX]最好定義爲全局。
//大數組定義在全局的緣由是:有的評測環境,棧空間很小,大數組定義在局部佔用了棧空間致使爆棧。
//如今各大OJ和比賽都會設置編譯命令使棧空間等於內存大小,不會出現爆棧。

unsigned long ulrand(){          //生成一個大隨機數
    return (
      (((unsigned long)rand()<<24)& 0xFF000000ul)
     |(((unsigned long)rand()<<12)& 0x00FFF000ul)
     |(((unsigned long)rand())    & 0x00000FFFul));
}

int bin_search(int *a, int n, int x){    //a[0]~a[n-1]是有序的
    int left = 0, right = n;             //不是 n-1
    while (left < right) {
        int mid = left+(right-left)/2;   //int mid = (left+ right)>>1;
        if (a[mid] >= x)  right = mid;
        else    left = mid + 1;
    }
   return left;       //特殊狀況:若是最後的a[n-1] < key,left = n
}

int main(){
    int n = MAX;
    srand(time(0));
    while(1){
        for(int i=0; i< n; i++)   //產生[MIN, MAX]內的隨機數,有正有負
            a[i] = ulrand() % (MAX-MIN + 1) + MIN;
        sort(a, a + n );       //排序,a[0]~a[n-1]

        int test = ulrand() % (MAX-MIN + 1) + MIN; //產生一個隨機的x
        int ans = bin_search(a,n,test);
        int pos = lower_bound(a,a+n,test)-a;  

        //比較bin_search()和lower_bound()的輸出是否一致:
        if(ans == pos) cout << "!";             //正確
        else        {  cout << "wrong"; break;} //有錯,退出
    }
}

<font size=3>2.2 STL的lower_bound()和upper_bound()</font>

  若是隻是簡單地找x或x附近的數,就用STL的lower_bound()和upper_bound()函數。有如下狀況:   (1)查找第一個大於x的元素的位置:upper_bound()。代碼例如:         pos = upper_bound(a, a+n, test) - a;   (2)查找第一個等於或者大於x的元素:lower_bound()。   (3)查找第一個與x相等的元素:lower_bound()且 = x。   (4)查找最後一個與x相等的元素:upper_bound()的前一個且 = x。   (5)查找最後一個等於或者小於x的元素:upper_bound()的前一個。   (6)查找最後一個小於x的元素:lower_bound()的前一個。   (7)單調序列中數x的個數:upper_bound() - lower_bound()。

<font size=3>2.3 簡單例題</font>

  尋找指定和的整數對。這是一個很是直接的二分法問題。   ∎問題描述   輸入n ( n≤100,000)個整數,找出其中的兩個數,它們之和等於整數m(假定確定有解)。題中全部整數都能用int 表示。   ∎題解   下面給出三種方法:   (1)暴力搜,用兩重循環,枚舉全部的取數方法,複雜度$O(n^2)$。超時。   (2)二分法。首先對數組從小到大排序,複雜度$O(nlogn)$;而後,從頭至尾處理數組中的每一個元素a[i],在a[i]後面的數中二分查找是否存在一個等於 m - a[i]的數,複雜度也是$O(nlogn)$。兩部分相加,總複雜度仍然是$O(nlogn)$。   (3)<font color=#FF0033>尺取法</font>/雙指針/two pointers。對於這個特定問題,更好的、標準的算法是:首先對數組從小到大排序;而後,設置兩個變量L和R,分別指向頭和尾,L初值是0,R初值是n-1,檢查$a[L]+a[R]$,若是大於m,就讓R減1,若是小於m,就讓L加1,直至$a[L]+a[R] = m$。排序複雜度$O(nlogn)$,檢查的複雜度$O(n)$,總複雜度$O(nlogn)$。檢查的代碼這樣寫:   

void find_sum(int a[], int n, int m){ 
     sort(a, a + n - 1);      //先排序
     int L = 0, R = n - 1;    //L指向頭,R指向尾
     while (L < R){
		    int sum = a[L] + a[R];
		    if (sum > m)   R--;
		    if (sum < m)   L++;
		    if (sum == m){     
			    cout << a[L] << "    " << a[R] << endl;  //打印一種狀況
                L++;   //可能有多種狀況,繼續
		    }
	  }
}

<font size=4>3. 整數二分典型題目</font>

  上面給出的二分法代碼bin_search(),處理的是簡單的數組查找問題。從這個例子,咱們能學習到二分法的思想。   在用二分法的典型題目中,主要是用二分法思想來進行斷定。它的基本形式是:

while (left < right) {
        int ans;          //記錄答案
        int mid = left+(right-left)/2;   //二分
        if (check(mid)){  //檢查條件,若是成立
            ans = mid;    //記錄答案
             …           //移動left(或right)
        }
        else    …         //移動right(或left)
}

  因此,二分法的難點在於如何建模和check()條件,其中可能會套用其餘算法或者數據結構。   二分法的典型應用有:<font color=#FF0033>最小化最大值、最大化最小值。</font>

<font size=3>3.1 最大值最小化(最大值儘可能小)</font>

<font size=3>3.1.1序列劃分問題</font>

  這是典型的最大值最小化問題。   ∎題目描述   例如,有一個序列{2,2,3,4,5,1},將其劃分紅3個連續的子序列S(1)、S(2)、S(3),每一個子序列最少有一個元素,要求使得每一個子序列的和的最大值最小。   下面舉例2個分法:   分法1:S(1)、S(2)、S(3)分別是(2,2,3)、(4,5)、(1),子序列和分別是七、九、1,最大值是9;   分法2:(2,2,3)、(4)、(5,1),子序列和是七、四、6,最大值是7。   分法2更好。   ∎題解   在一次劃分中,考慮一個x,使x知足:對任意的S(i),都有S(i)<=x,也就是說,x是全部S(i)中的最大值。題目須要求的就是找到這個最小的x。這就是<font color=#FF0033>最大值最小化</font>。   如何找到這個x?從小到大一個個地試,就能找到那個最小的x。   簡單的辦法是:枚舉每個x,用貪心法每次從左向右儘可能多劃分元素,S(i)不能超過x,劃分的子序列個數不超過m個。這個方法雖然可行,可是枚舉全部的x太浪費時間了。   改進的辦法是:用二分法在[max, sum]中間查找知足條件的x,其中max是序列中最大元素,sum是全部元素的和。

<font size=3>3.1.2 通往奧格瑞瑪的道路</font>

  「通往奧格瑞瑪的道路」,來源:https://www.luogu.org/problem/P1462   ∎題目描述   給定無向圖,n個點,m條雙向邊,每一個點有點權fi(這個點的過路費),有邊權ci(這條路的血量)。求起點1到終點N的全部可能路徑中,在總邊權(總血量)不超過給定的b的前提下,所通過的路徑中最大點權(這條路徑上過路費最大的那個點)的最小值是多少。   題目數據:n≤10000,m≤50000,fi,ci,B≤1e9。   ∎題解   對點權fi進行二分,用dijkstra求最短路,檢驗總邊權是否小於b。二分法是最小化最大值問題。   這一題是二分法和最短路算法的簡單結合。   (1)對點權(過路費)二分。題目的要求是:從1到N有不少路徑,其中的一個可行路徑Pi,它有一個點的過路費最大,記爲Fi;在全部可行路徑中,找到那個有最小F的路徑,輸出F。解題方案是:先對全部點的fi排序,而後用二分法,找符合要求的最小的fi。二分次數log(fi)=log(1e9) < 30。   (2)在檢查某個fi時,刪除全部大於fi的點,在剩下的點中,求1到N的最短路,看總邊權是否小於b,若是知足,這個fi是合適的(若是最短路的邊權都大於b,那麼其餘路徑的總邊權就更大,確定不符合要求)。一次Dijkstra求最短路,複雜度是O(mlogn)。   總複雜度知足要求。

<font size=3>3.2 最小值最大化(最小值儘可能大)</font>

  ∎題目描述   「進擊的奶牛」,來源:https://www.luogu.org/problem/P1824   在一條很長的直線上,指定n個座標點(x1, ..., xn)。有c頭牛,安排每頭牛站在其中一個點(牛棚)上。這些牛喜歡打架,因此儘可能距離遠一些。問最近的兩頭牛之間距離的最大值能夠是多少。   這個題目裏,全部的牛棚兩兩之間的距離有個最小值,題目要求使得這個最小值最大化。   ∎題解   (1)暴力法。從小到大枚舉最小距離的值dis,而後檢查,若是發現有一次不行,那麼上次枚舉的就是最大值。如何檢查呢?用貪心法:第一頭牛放在x1,第二頭牛放在xj≥x1+dis的點xi,第三頭牛放在xk≥xj+dis的點xk,等等,若是在當前最小距離下,不能放c條牛,那麼這個dis就不可取。複雜度O(nc)。   (2)二分。分析從小到大檢查dis的過程,發現能夠用二分的方法找這個dis。這個dis符合二分法:它有上下邊界、它是單調遞增的。複雜度O(nlogn)。

#include<bits/stdc++.h>
using namespace std;
int n,c,x[100005];//牛棚數量,牛數量,牛棚座標

bool check(int dis){     //當牛之間距離最小爲dis時,檢查牛棚夠不夠
    int cnt=1, place=0;  //第1頭牛,放在第1個牛棚
    for (int i = 1; i < n; ++i)     //檢查後面每一個牛棚
        if (x[i] - x[place] >= dis){ //若是距離dis的位置有牛棚
            cnt++;      //又放了一頭牛
            place = i;  //更新上一頭牛的位置
        }
    if (cnt >= c) return true;   //牛棚夠
    else          return false;  //牛棚不夠
}

int main(){
    scanf("%d%d",&n, &c);
    for(int i=0;i<n;i++)    scanf("%d",&x[i]);
    sort(x,x+n);             //對牛棚的座標排序
    int left=0, right=x[n-1]-x[0];  //R=1000000也行,由於是log(n)的,很快
							        //優化:把二分上限設置爲1e9/c
    int ans = 0;
    while(left < right){
        int mid = left + (right - left)/2;     //二分
        if(check(mid)){       //當牛之間距離最小爲mid時,牛棚夠不夠?
            ans = mid;        //牛棚夠,先記錄mid
            left = mid + 1;   //擴大距離
           }
        else
            right = mid;      //牛棚不夠,縮小距離
    }

    cout << ans;              //打印答案
    return 0;
}

<font size=4>4. 實數二分</font>

<font size=3>4.1 基本形式</font>

  實數域上的二分,比整數二分簡單。   <center>實數二分的基本形式</center>

const double eps =1e-7;        //精度。若是下面用for,能夠不要eps
while(right - left > eps){     //for(int i = 0; i<100; i++){
      double mid = left+(right-left)/2;
      if (check(mid)) right = mid;           //斷定,而後繼續二分
      else            left  = mid;
}

  其中,循環用2種方法均可以:   

  while(right - left > eps)   { ... }
  或者:
  for(int i = 0; i < 100; i++) { ... }

  若是用for循環,因爲循環內用了二分,執行100次,至關於實現了 $1/2^100$的精度,通常比eps更精確。   for循環的100次,比while的循環次數要多。若是時間要求不是太苛刻,用for循環更簡便*[參考李煜東《算法競賽進階指南》27頁的說明]*。

<font size=3>4.2 實數二分例題</font>

  ∎題目描述   「Pie」,題目來源:http://poj.org/problem?id=3122   主人過生日,m我的來慶生,有n塊半徑不一樣的圓形蛋糕,由m+1我的(加上主人)分,每人的蛋糕必須同樣重,並且是一整塊(不能是幾個蛋糕碎塊,也就是說,每一個人的蛋糕都是從一塊圓蛋糕中切下來的完整一塊)。問每一個人能分到的最大蛋糕是多大。   ∎題解   最小值最大化問題。設每人能分到的蛋糕大小是x,用二分法枚舉x。 <center>「Pie」題的代碼</center>

#include<stdio.h>
#include<math.h>
double PI = acos(-1.0);    //3.141592653589793;
#define eps 1e-5
double area[10010];
int n,m;
bool check(double mid){ 
    int sum = 0;
    for(int i=0;i<n;i++)        //把每一個圓蛋糕都按大小mid分開。統計總數
        sum += (int)(area[i] / mid);
    if(sum >= m) return true;   //最後看總數夠不夠m個
    else         return false;
}
int main(){
    int T; scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&m); m++;
        double maxx = 0;
        for(int i=0;i<n;i++){
            int r; scanf("%d",&r);
            area[i] = PI*r*r;
            if(maxx < area[i]) maxx = area[i]; //最大的一塊蛋糕
        }
        double left = 0, right = maxx;  
        for(int i = 0; i<100; i++){  
      //while((right-left) > eps)   {      //for或者while都行
            double mid = left+(right-left)/2;
            if(check(mid))   left  = mid;   //每人能分到mid大小的蛋糕
            else             right = mid;   //不夠分到mid大小的蛋糕
        }
        printf("%.4f\n",left);    // 打印right也對
    }
    return 0;
}

<font size=4>5. 二分法習題</font>

  飢餓的奶牛  https://www.luogu.org/problem/P1868   尋找段落   https://www.luogu.org/problem/P1419   小車問題   https://www.luogu.org/problem/P1258
  借教室    https://www.luogu.org/problem/P1083
  跳石頭    https://www.luogu.org/problem/P2678   聰明的質監員 https://www.luogu.org/problem/P1314   分梨子    https://www.luogu.org/problem/P1493
  第k大    http://acm.hdu.edu.cn/showproblem.php?pid=6231

<font size=4>6. 三分法求極值</font>

<font size=3>6.1 原理</font>

  三分法求單峯(或者單谷)的極值,是二分法的一個簡單擴展。   單峯函數和單谷函數以下圖,函數f(x)在區間[l, r]內,只有一個極值v,在極值點兩邊,函數是單調變化的。以單峯函數爲例,在v的左邊,函數是嚴格單調遞增的,在v右邊是嚴格單調遞減的。

<div align=center><img src="https://img2020.cnblogs.com/blog/1954999/202003/1954999-20200304125834578-73426390.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkxNDU5Mw==,size_16,color_FFFFFF,t_70" width="70%"></div> <center>圖6.1  (1)單峯函數       (2)單谷函數</center>

  下面的講解都以求單峯極值爲例。   如何求單峯函數最大值的近似值?雖然不能直接用二分法,不過,只要稍微變形一下,就能用了。   在[l, r]上任取2個點,mid1和mid2,把函數分紅三段。有如下狀況:   (1)若f(mid1) < f(mid2),極值點v必定在mid1的右側。此時,mid1和mid2要麼都在v的左側,要麼分別在v的兩側。以下圖所示。

<div align=center><img src="https://img2020.cnblogs.com/blog/1954999/202003/1954999-20200304125932965-1232705578.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkxNDU5Mw==,size_16,color_FFFFFF,t_70" width="60%"></div> <center>圖6.2 狀況(1):極值點v在mid1右側</center>

  下一步,令l = mid1,區間從[l, r]縮小爲[mid1, r],而後再繼續把它分紅三段。   (2)同理,若f(mid1) > f(mid2),極值點v必定在mid2的左側。以下圖所示。下一步,令 r = mid2,區間從[l, r]縮小爲[l, mid2]。

<div align=center><img src="https://img2020.cnblogs.com/blog/1954999/202003/1954999-20200304130022831-1321455925.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkxNDU5Mw==,size_16,color_FFFFFF,t_70" width="60%"></div> <center>圖6.3 狀況(2):極值點v在mid1右側</center>

  不斷縮小區間,就能使得區間[l, r]不斷逼近v,從而獲得近似值。   如何取mid1和mid2?有2種基本方法:   (1)三等分:mid1和mid2爲[l, r]的三等分點。那麼區間每次能夠減小三分之一。   (2)近似三等分:計算[l, r]中間點mid = (l + r) / 2,然讓mid1和mid2很是接近mid,例如mid1 = mid - eps,mid2 = mid + eps,其中eps是一個很小的值。那麼區間每次能夠減小接近一半。   方法(2)比方法(1)要稍微快一點。   (<font color=#FF0033>有網友說</font>不要用方法(2):「由於在有些狀況下這個 eps 太小可能致使這兩個算出來的相等,若是相等就有可能會判斷錯方向,因此其實不建議這麼寫,log3 和 log2 本質上是同樣的。」)   注意:單峯函數的左右兩邊要嚴格單調,不然,可能在一邊有f(mid1) == f(mid2),致使沒法判斷如何縮小區間。

<font size=3>6.2 實數三分</font>

  下面用一個模板題給出實數三分的兩種實現方法。   ∎題目描述   「模板三分法」,來源:https://www.luogu.com.cn/problem/P3382
  給出一個N次函數,保證在範圍[l, r]內存在一點x,使得[l, x]上單調增,[x, r]上單調減。試求出x的值。   ∎題解   下面分別用前面提到的2種方法實現:(1)三等分;(2)近似三等分。   (1)三等分

<center>mid1和mid2爲[l, r]的三等分點</center>

#include<bits/stdc++.h>
using namespace std;
const double eps = 1e-6;
int n;
double a[15];
double f(double x){       //計算函數值
    double s=0;
    for(int i=n;i>=0;i--) //注意函數求值的寫法
        s = s*x + a[i];
    return s;
}
int main(){
    double L,R;
    scanf("%d%lf%lf",&n,&L,&R);
    for(int i=n;i>=0;i--) scanf("%lf",&a[i]);
    while(R-L > eps){  // for(int i = 0; i<100; i++){   //用for也行
        double k =(R-L)/3.0;
        double mid1 = L+k, mid2 = R-k;
        if(f(mid1) > f(mid2)) 
               R = mid2;
        else   L = mid1;
    }
    printf("%.5f\n",L);
    return 0;
}

  (2)近似三等分

<center>mid1和mid2在[l, r]的中間點附近</center>

#include<bits/stdc++.h>
using namespace std;
const double eps = 1e-6;
int n; double a[15];
double f(double x){
    double s=0;
    for(int i=n;i>=0;i--) s=s*x+a[i];
    return s;
}
int main(){
    double L,R;
    scanf("%d%lf%lf",&n,&L,&R);
    for(int i=n;i>=0;i--) scanf("%lf",&a[i]);
    while(R-L > eps){   // for(int i = 0; i<100; i++){   //用for也行
        double mid = L+(R-L)/2; 
        if(f(mid - eps) > f(mid))
             R = mid;
        else L = mid;
     }
    printf("%.5f\n",L);
    return 0;
}

<font size=3>6.2.1 實數三分習題</font>

  (1)「三分求極值」,題目來源:http://hihocoder.com/problemset/problem/1142   ∎題目描述:在直角座標系中有一條拋物線y = ax^2 + bx + c和一個點P(x, y),求點P到拋物線的最短距離d。   ∎題解:直接求距離很麻煩。觀察這一題的距離D,發現它知足單谷函數的特徵,用三分法很合適。   (2)<font color=#FF0033>三分套三分</font>,是計算幾何的常見題型。   「Line belt」,題目來源:http://acm.hdu.edu.cn/showproblem.php?pid=3400   ∎題目描述:給定兩條線段AB、CD ,一我的在AB上跑的時候速度是p,在CD上速度是q,在其餘地方跑速度是r。問從A點到D點最少的時間。   ∎題解:從A出發,先走到AB上一點X,而後走到CD上一點Y,最後到D。時間是: time = |AX|/p + |XY|/r + |YD|/q   假設已經肯定了X,那麼目標就是在CD上找一點Y,使|XY|/r + |YD|/q最小,這是個單峯函數。三分套三分就能夠了。

<font size=3>6.3 整數三分</font>

  整數三分的形式是:

while (left < right) {
      int mid1 = left + (right - left)/3;
      int mid2 = right- (right - left)/3;
      if(check(mid1) > check(mid2))
           …         //移動right
      else
           …         //移動left
}

  下面是一個例題。   ∎題目描述   「期末考試」,題目來源:https://www.lydsy.com/JudgeOnline/problem.php?id=4868   有n位同窗,每位同窗都參加了所有的m門課程的期末考試,都在焦急的等待成績的公佈。第i位同窗但願在第ti天或以前得知全部課程的成績。若是在第ti天,有至少一門課程的成績沒有公佈,他就會等待最後公佈成績的課程公佈成績,每等待一天就會產生C不愉快度。對於第i門課程,按照本來的計劃,會在第bi天公佈成績。有以下兩種操做能夠調整公佈成績的時間:1.將負責課程X的部分老師調整到課程Y,調整以後公佈課程X成績的時間推遲一天,公佈課程Y成績的時間提早一天;每次操做產生A不愉快度。2.增長一部分老師負責學科Z,這將致使學科Z的出成績時間提早一天;每次操做產生B不愉快度。上面兩種操做中的參數X,Y,Z都可任意指定,每種操做都可以執行屢次,每次執行時均可以從新指定參數。如今但願你經過合理的操做,使得最後總的不愉快度之和最小,輸出最小的不愉快度之和便可。   ∎題解   不愉快度是一個下凹的函數,用三分法。代碼略。

相關文章
相關標籤/搜索