Binary Search

10^11

目錄

  • 二分搜介紹
  • 實作
  • 題解三段跳
  • 補充競程
  • KAHOOT!

目錄

三段跳是真的跳

大概是1星3星5星難度的感覺

希望大家至少能學到3星

搜尋演算法

電腦科學中,

搜尋演算法是解決搜尋問題的任何演算法

即檢索儲存在某個資料結構中的資訊,

或者在問題的可行域中計算的資訊。

先來看看什麼是搜尋演算法

搜尋演算法

簡單來說,

就是在一群資料中,

想盡辦法找到目標資料

搜尋演算法

大概分成這幾種

  • 線性搜尋(Linear search)
  • 二元搜尋(Binary search)
  • 指數搜尋(Exponential search)
  • 插補搜尋(Interpolation search)
  • 費氏搜尋(Fibonacci search)

搜尋演算法

顯然,我們今天的重點是二分搜

順便補充一下,

二分搜尋的時間複雜度是 O(log⁡N)O(\log N)O(logN)

二分搜

大家玩過終極密碼嗎?

怎麼樣玩才能保證猜到密碼的次數最少

二分搜

每次猜測都取中間值

二分搜

65

1

100

紅色的線「不合法」,藍色的線「合法」

我們要找到藍色線的最左端

二分搜

51

1

100

[51,100]

二分搜

51

1

100

[51,74]

74

二分搜

51

1

100

[63,74]

74

63

二分搜

51

1

100

[63,67]

74

63

67

二分搜

51

1

100

[64,67]

74

62

67

65

二分搜

67

62

74

[65]

64

65

給你一個嚴格遞增的數列A1,A2,A3.....An、

k筆詢問數、

以及k個詢問的整數x,

求數列中是否存在一個Ai(1<=i<=n)的值與X相等?

二分搜

#include <bits/stdc++.h>
using namespace std;

int bs(int arr[], int n, int ans) {
    int left = 1;
    int right = n;//數組長度

    while (left <= right) {
        int mid = (left + right) / 2;

        if (arr[mid] == ans) {
            return mid; // 返回找到的索引
        }
        else if (arr[mid] < ans){
            left = mid + 1;
        }
        else{
            right = mid - 1;
        }
    }

    return 0; // 找不到則返回0
}

int main() {
    int n, k;
    cin >> n >> k;

    int arr[n];
    for (int i = 1; i <= n; ++i) {
        cin >> arr[i];
    }

    for (int i = 0; i < k; ++i) {
        int x;
        cin >> x;

        int ans = bs(arr, n, x);
        cout << ans << endl;
    }

    return 0;
}

二分搜

慢慢想

直接用模板寫

顯然

一直自己想似乎有點麻煩

二分搜-模板

left = 左界
right = 右界
while (左比右小,代表邊界還正常) {
  mid = 左右邊界的中間點
  if (數列中間點的數值大於 ans) {
    右半邊可以不用看,把右界移到中間(mid)
  } else if (數列中間點的數值小於 ans) {
    左半邊可以不用看,把左界移到中間(mid)
  } else {
    找到答案
  }
}

二分搜-實作細節

  • 記得排序

  • 確認區間定義、範圍

  • 左右界如何改變

  • 負數除法會有問題, 改用 left+(right-left)/2

二分搜

看起來很簡單?很少?

 對,我們還有一題範例

要將n個士兵升等,

每人升x等需x^2金幣,

現有c枚金幣,問全體最少可到幾等

 

範例1說明:我們有2個士兵10枚硬幣

將第1位由2升至5需3^2金幣,第2位由4升至5需1^2金幣,全體最少有5級

輸入說明:

第1行有兩個正整數N或C,分別士兵數及金幣數
第2行有N個正整數a1~an

代表N個士兵目前的等級,皆以空白隔開

 

輸出說明:

請輸出一行正整數U,表示最少可全部升至U級(含)以上

沒錯,這題的解決方法就是二分搜
直接搜尋目標的等級,
計算此等級以下的士兵升等
是否超出金幣預算

TOI的題目真的難QAQ

#include <bits/stdc++.h>
using namespace std;
long long level[2000000],n;
long long x(long long lv){
  //計算到等級lv所需要的錢
long long sum=0;
  for(long long i=0;i<n;i++){
    if(level[i]<lv){
      //如果士兵等級<lv
      sum=sum+(level[i]-lv)*(level[i]-lv);
      //計算每個士兵的花費並加總
    }
}
  return sum;
  //返回預算

}
int main(){

  long long c;
  long long l=1,r=2000000;
  cin>>n>>c;
  for(long long i=0;i<n;i++){
    cin>>level[i];
  }
  while(l<=r){
    //左閉右閉,使用等於搜索全部範圍
    long long mid=l+(r-l)/2;
    //預期的等級mid
    if(x(mid)>c){
      r=mid-1;
  }
    else{
      l=mid+1;
    }
}
 cout<<r<<endl;//如果未找到精確值,輸出低於預算的最接近等級
}

二分搜

如果想要參加競程

精進程式能力的話

在學習二分搜的時候

大多會用其他方法&工具

這邊簡單介紹一下

二分搜

lower_bound/upper_bound

這是我們今天要介紹的神奇海螺(x

二分搜

前面學到的東西

用二分搜在一個單調(遞增或遞減)的陣列中,尋找某個值在哪裡

此時我們學到的方法就是以封閉區間[l,r]去搜尋

二分搜

在程式的世界裡,我們稱呼他們為

左閉右開 左閉右閉

但相信大家在數學上學過[l,r)跟[l,r]的差別

前者是競程常用,後者是初學常用

不過,這種還是比較看習慣

二分搜

左閉右開的寫法:

int bs(){
    int l=0,r=N; //[l,r)
    while (l<r){ 
        int mid=(l+r)/2;
        if (A[mid]<ans) l=mid+1; // [mid,r)
        else r=mid; // [l,mid)
    }
    return l;
}

二分搜

整理一下一般情況的兩者差別:

左閉右閉 左閉右開
l,r初始設置 l=1,r=n l=0,r=n
mid初始設置 前面介紹的兩種 都可以
while設置 l<=r l<r
更新邊界 l=mid+1,r=mid-1 l=mid+1,r=mid

二分搜

這個模板本身是很活的東西

盡量選擇理解觀念而不是背

二分搜

前面這些觀念理解了

我們再回來看剛剛的 神奇海螺 新工具

二分搜

lower_bound(first, last, value)

在範圍[first,last)中,搜尋第一個大於或等於value的位置

upper_bound(first, last, value)

在範圍[first,last)中,搜尋第一個大於value的位置

如果找不到,兩者會返回last

<algorithm>底下,回傳型態是指標

二分搜

有什麼用?

當我們把題目這樣魔改

給定一個遞增的整數序列A[],與一整數K,

求K在此序列中的出現次數。

要求時間複雜度須在O(n)以下(不含),所以不能從頭到尾掃一遍

二分搜

範例輸入輸出也改

Sample Input 2:

A = [1, 1, 2, 2, 2, 3], K = 4

 

Sample Output 2:

0

Sample Input 1:

A = [1, 1, 2, 2, 2, 3], K = 2

 

Sample Output 1:

3

二分搜

使用剛剛新學的工具

int solve() {
    int idx_of_lb = lower_bound(A, A + N, K) - A;
    int idx_of_ub = upper_bound(A, A + N, K) - A;
    return idx_of_ub - idx_of_lb;
}

怎麼這麼簡短?

二分搜

讓我們分析一下範例

Sample Input 1:

A = [1, 1, 2, 2, 2, 3], K = 2

 

Sample Output 1:

3

二分搜

讓我們分析一下範例

Sample Input 1:

A = [1, 1, 2, 2, 2, 3], K = 2

 

Sample Output 1:

3

lower_bound=A[2]

二分搜

讓我們分析一下範例

Sample Input 1:

A = [1, 1, 2, 2, 2, 3], K = 2

 

Sample Output 1:

3

upper_bound=A[5]

lower_bound=A[2]

二分搜

讓我們分析一下範例

Sample Input 1:

A = [1, 1, 2, 2, 2, 3], K = 2

 

Sample Output 1:

3

你發現了嗎?

二分搜

讓我們分析一下範例

Sample Input 1:

A = [1, 1, 2, 2, 2, 3], K = 2

 

Sample Output 1:

3

5-2=3 就是答案!

A[2,5)的每項都是K!

二分搜

好的補充就到這邊

這後面沒聽懂沒關係

這算是更進階一點的東西了

二分搜

還有一個APCS第四題

有一個由 n 個木板所組成的柵欄,每個木板的高度為h[1],h[2],...,h[n],

有 k 張海報要張貼在柵欄上,每張海報的寬度為 w[1],w[2],⋯,w[n] 並且高度均為 1。

若要張貼海報在高度為 x 的高度,

則第 i 張海報需要張貼在一個長度為 w[i] 的連續並且高度都不小於 x 的木板上,且每張海報張貼的高度需要一致、按照順序並不能重疊 (可以相連)。

詢問最高可以貼到多高的位置。

二分搜

有點複雜,我們圖解一下

範例輸入 #2

10 3
5 3 7 5 1 7 5 3 8 4
2 2 1

範例輸出 #2

5

二分搜

先建構木板

範例輸入

10 3
5 3 7 5 1 7 5 3 8 4
2 2 1

範例輸出 

5

5

3

7

5

1

7

5

3

8

4

二分搜

二分搜check目標高度

5

3

7

5

1

7

5

3

8

4

取h=4

vector存連續長度

[1,2,2,2]

二分搜

再來放海報

從最後一個看

5

3

7

5

1

7

5

3

8

4

範例輸入

10 3
5 3 7 5 1 7 5 3 8 4
2 2 1

cnt=0

二分搜

再來放海報

從最後一個看

5

3

7

5

1

7

5

3

8

4

範例輸入

10 3
5 3 7 5 1 7 5 3 8 4
2 2 1

cnt=1

二分搜

再來放海報

從最後一個看

5

3

7

5

1

7

5

3

8

4

範例輸入

10 3
5 3 7 5 1 7 5 3 8 4
2 2 1

cnt=2

二分搜

再來放海報

從最後一個看

5

3

7

5

1

7

5

3

8

4

範例輸入

10 3
5 3 7 5 1 7 5 3 8 4
2 2 1

cnt=3

二分搜

成功放滿(true)

將左界往上移,繼續二分搜

5

3

7

5

1

7

5

3

8

4

l=mid=4

二分搜

取h=6

5

3

7

5

1

7

5

3

8

4

二分搜

來放海報

5

3

7

5

1

7

5

3

8

4

vector=[1,1,1]

cnt=0

二分搜

海報[2,2,1]

沒辦法放

5

3

7

5

1

7

5

3

8

4

cnt=1

二分搜

失敗(false)

繼續二分搜

5

3

7

5

1

7

5

3

8

4

右界下移r=5

二分搜

取h=5

5

3

7

5

1

7

5

3

8

4

二分搜

繼續放海報

5

3

7

5

1

7

5

3

8

4

vector=[1,2,2,1]

cnt=0

二分搜

繼續放海報

5

3

7

5

1

7

5

3

8

4

vector=[1,2,2,1]

cnt=1

二分搜

繼續放海報

5

3

7

5

1

7

5

3

8

4

vector=[1,2,2,1]

cnt=2

二分搜

海報[2,2,1]

成功(true)

5

3

7

5

1

7

5

3

8

4

vector=[1,2,2,1]

cnt=3

二分搜

左界l=5=r

5

3

7

5

1

7

5

3

8

4

ans=l=5

範例輸出 

5

二分搜

#include <bits/stdc++.h>
using namespace std; 
int n,k,maxhigh=0;//木板數量 海報數量 木板高度最大值
vector<int>high,wide;//儲存木板高度 海報寬度

  int check(int h){
    int count=0;//紀錄目前連續高度>=h的木板數
    vector<int>v;//儲存每段連續高度>=h的木板數
    for(int i=0;i<n;i++){
      //如果目前木板高度>=h,增加計數
      if(high[i]>=h) count++;
      else{
        //如果目前木板高度<h,並且之前有計數的木板段,則儲存該長度到v
        if(count!=0){
        v.push_back(count);
        count=0;//重置計數
      }
      }
    }
    //如果最後還有一段連續木板高度>=h,則儲存該長度到v
    if(count!=0) v.push_back(count);
    int cnt=0;//計數那些成功放置的海報數
    //從最後一張海報開始檢查能否放置,且確保木板數組不為空
    for(int i=k-1;i>=0&&v.size();i--){
      //如果目前連續木板段長度>=目前海報寬度
      if(v.back()>=wide[i]){
        v.back()=v.back()-wide[i];
        //減少該段木板的長度
        cnt++;//增加成功放置的海報的計數
        //如果該段木板長度為0,移除該段長度
        if(v.back()==0) v.pop_back();
      }
      else{
        //如果目前連續木板段長度<目前海報寬度,且確保木板數組不為空
        while(v.back()<wide[i]&&v.size()) v.pop_back();
        //不斷檢查最後一個木板段直到連續木板段長度>=目前海報寬度或木板段數組為空
          i++;
      }
    }
    if(cnt==k) return 1;
    else return 0;
    //若cnt等於k代表海報貼得完,回傳1,不然回傳0
  }
int main() {
  
  cin>>n>>k;
  high.assign(n,0);
  wide.assign(k,0);//將他們的值全部設為0
  for(int i=0;i<n;i++){
    cin>>high[i];
    maxhigh=max(maxhigh,high[i]);//找出最大高度
  }
  for(int i=0;i<k;i++) cin>>wide[i];
  //二分搜查找最高可以貼海報的位置
  int l=1,r=maxhigh,ans=0;
  while(l<=r){
    if(l==r){
      ans=l;
      break;
    }
    else if(l+1==r){
      //如果左右邊界相差1,即相鄰,檢查這兩個位置
      if(check(r)==1) ans=r;
      else ans=l;
      break;
        }
    int mid=l+(r-l)/2;
    if(check(mid)==1) l=mid;
      //如果h可以貼海報,表示h-1一定可以且h+1可能可以
    else r=mid;
    //如果h不能貼海報,表示h+1一定不行且h-1可能可以
      }
  //左閉右開
  cout<<ans<<endl;
}

二分搜

//不同方法的二分搜

//左閉右閉ans在外
while(l<=r){
  int mid=l+(r-l)/2;
  if(check(mid)==1){
    l=mid+1;//如果h可以貼海報,表示h-1一定可以且h+1可能可以
  }
  else r=mid-1;//如果h不能貼海報,表示h+1一定不行且h-1可能可以
    ans=r;
}

//左閉右閉ans在內
while(l<=r){
  int mid=l+(r-l)/2;
  if(check(mid)==1){
    l=mid+1;//如果h可以貼海報,表示h-1一定可以且h+1可能可以
    ans=mid;
  }
  else r=mid-1;//如果h不能貼海報,表示h+1一定不行且h-1可能可以
}

二分搜

恭喜大家打完BOSS了

KAHOOT!

Binary Search

By 愛錢成癡,嗜賭成癮