數據結構與算法之二分法

數據結構與算法之二分法

萬物皆可分

 二分法應該是我們熟悉的內容,因爲在初高中的數學課堂中均會設計到二分的思想。比如,我們求某一無理數的近似值,就是通過二分法。比如求2\sqrt{2}的近似值,範圍在[1,2]內。顯然我們可以先取中點3/2,顯然3/2的平方大於2,故在取區間[1,3/2]然後依次操作,直至區間長度達到所需的精度。

double eps=1e-5;
int f(double mid){
  return mid*mid;
}
int two_find(){
  double left=1;
  double right=2;
  double mid;
  while(right-left<eps){//達到所需的精度時就退出
      mid=(right-left)/2;
      if(f(mid)>2){
          right=mid;//在左側的區間進行逼近
      }
      else{
          left=mid;//在右側的區間進行逼近
  
      return mid;  
  }
}

 延續這一思路,我們在查找某一特定值是時候可以用上這一算法的思想。
例如:現有一個遞增的整數序列,我們現在想知道這個整數的序列中是否存在一個值x,若存在則返回它所在的位置。
分析:我們當然可以將這個數組遍歷一遍,但是時間複雜度是O(n)。我們學數據結構與算法的意義就是用簡單、可行的方法解決一個問題。現在,我們要想想能否一個方法將時間複雜度再降低。聯繫今天介紹的內容,我們可以嘗試用二分查找的方法,就是第一個例子一樣,我們將每次查找的區間範圍減少一半。
總的來說,O(n)的方法就是從頭一個個查找,而二分查找就是先從序列的中間位置進行查找,然後再確定一個小區間,然後在折半。所以它的時間複雜度就是O(logn)。另外注意的是,查找的區間應該是有着一定的順序的,比如遞增或遞減。如果序列是無順序的無規律的,那麼就無法使用二分查找,只能老老實實的運用暴力搜索了。
例子:一組遞增的序列,查找其中是否有x,若有則返回其位置。

int a[10];
int left=0;
int right=9;
int x;
int two_find(){
    while(lef<=right){
        int mid=(left+right)/2;
        if(mid==x){
            return mid;// 查找成功,返回位置。
        }
        if(mid>x){
            right=mid+1;//在左區間
        }
        if(mid< x){
            left=mid+1;// 在右區間
        }
    }
    return -1;
}

推廣(1):現在我不滿足於找到x了,而是嘗試找到序列中第一個大於等於x的元素位置。 注意:該序列中有重複的元素,且遞增。
分析:其實我們得思考多種情況。
一是:該序列存在一個x。
二是,該序列中存在多個x,那麼我們就得尋找第一個x的位置。二是,該序列中不存在x,但是x值位於某兩個x值的之間,顯然我們得返回後一個值,作爲找到了第一個大於x的元素。三是:該序列不存在,且x大於序列中的任何一個元素值。
所以我們得在二分查找的代碼基礎上做一些改動。首先,當f(mid)==x時,不能直接返回mid值,因爲序列中可能會有多個x值,你無法保證返回的mid值就是第一個x的位置。所以,這時候第一個x肯定在左區間,這時right=mid。不能是right=mid-1,可能會錯過第一個x。
故當f(mid)>=x時。所以right=mid。
相應的,當f(mid)<x時,直接left=mid+1。程序並且返回left,因爲此時的left值肯定是滿足要求。

int a[10];
int left=0;
int right=9;
int x;
int two_find(){
   while(lef<right){//當left與right相鄰時,mid取得是left,故當right==left時,仍得運行一次,用以left+1
       int mid=(left+right)/2;
       
       if(mid>=x){
           right=mid;//在左區間
       }
       if(mid< x){
           left=mid+1;// 在右區間
       }
   }
   return left;
}

推廣(2):仍是同一個序列,但是我們要的是第一個大於x的元素的位置。
分析:相當於求出x緊鄰後邊的元素。所以如果有重複x值時,且當a[mid]==x時,我們應該將mid不斷後移才能找到符合條件的。

int a[10];
int left=0;
int right=9;
int x;
int two_find(){
   while(lef<right){//當left與right相鄰時,mid取得是left,故當right==left時,仍得運行一次,用以left+1
       int mid=(left+right)/2;
       
       if(mid>x){
           right=mid;//在左區間
       }
       if(mid< =x){
           left=mid+1;// 在右區間
       }
   }
   return left;
}

 木棒切割問題:給出N段木棒,長度均已知,現在希望通過切割它們得到至少K段長度相等(長度爲整數)木棒,問:這些長度相等的木棒的長度最大值是?
分析:假設按長度爲x來切割,得到了a段那麼目前我們可以知道x越小我們就可以得到越短的段數。所以我們依次可以通過的對x進行嘗試,直到滿足條件的a。但是嘗試也是要遵循一定的章法,聯繫上文,我們可以採用二分法來解決問題。
 首先我們先找尋x的可行區間,爲[1,maxn],其中maxn是給定木棒長度的最大值。然後進行二分,得到mid,接着我們計算此時的會切割成多少段,若此時的段數a小於k,則我們應該去往左區間繼續尋找。當大於k時,我們去右區間尋找。我們必須考慮當等於k時,我們不能直接返回了此時的x,因爲會有連續幾個x值,都會使得這些木棒被切割爲k段,我們要找最大的x值,即這些x中的最後(右邊末尾的那個),所以我們可以參考上文“尋找第一個大於x的位置”,只不過最後我們得往後退一個纔是滿足條件的臨界長度。
代碼如下:

int wud[n];//存儲每根木棒的長度
int k;//題目要求的段數
int max_l;//理論上可以滿足要求的最大切割長度
int sum=0;
for(int i=0;i<k;i++){
    sum+=wud[i];
}
max_l=sum/k;
int right,left;
int ans;
right=1;//切割長度的區間
left=max_l;
while(right<left){
    int cnt=0;
    mid=(right+left)/2;
    for(int i=0;i<k;i++){
        cnt+=wud[i]/mid;
    }
    if(cnt<k){//此時切割的長度過大
        right=mid;
    }
    if(cnt>=k){//此時切割的長度過小
        left=mid+1;
    }

}
ans=left-1;//得往後退一個,纔是臨界長度。

  總結:二分法的應用是有着條件。即一是,我們得有一個確切的區間,這樣我們才能進行查找。二是,我們得有個查找的條件,根據這個條件我們能不斷的修正我們查找的範圍,這樣最後才能找到或者無限的逼近最終的答案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章