Binary Search

二分搜尋法 - Arvin Liu @ Sprout 2021

一個大家都玩過的遊戲(?)

猜猜我在想甚麼數字?

我在心中想一個介於1~10的數字,猜猜看我在想什麼

我會告訴你有沒有猜對

別騙了,這不就是要猜10次?

猜猜我在想甚麼數字?

我在心中想一個介於1~100的數字,猜猜看我在想什麼

我會告訴你有沒有猜對或有沒有比我的數字還要大

(如果你是理組)
你會下意識的想要猜中間,為甚麼?

來當五分鐘的哈佛學生

一個快快的從電話簿查名字的方法

所以他想表達甚麼?

  • 電話簿是按照字典序排序的。 (A在前面,Z在後面)
  • 假設我們要查 H開頭的名字,例如軒爺。

ABCDEFGHIJKLMNOPQRSTUVWXYZ

第一次翻書: 翻到S

不用找了

不用撕掉==

第二次翻書: 翻到L

不用找了

第三次翻書: 翻到E

不用找了

範圍變小了

一個一個找
OK的

一個酷酷的快速的搜尋法。

讓我們回到猜數字

為什麼要猜中間呢?

畫個可能圖吧!

假設說只有11個數字...

1~11

答案的可能性

猜 6

1~5

7~11

猜 3

1~2

4~5

猜 9

7~8

10~11

如果不猜中間...

1~11

答案的可能性

3

1~2

4~11

猜 6

4~5

7~11

時間複雜度?

好難喔:( 給個例子好了

要猜的次數 最多幾種可能 可以寫成

1

2

3

4

3

7

15

31

2^2-1
2^3-1
2^4-1
2^5-1
2^{\text{猜的次數+1}}-1 = \text{最多幾種可能}
\text{猜的次數} = \log_2 ({\text{最多幾種可能}+1}) - 1
O(\log_2{(\text{n}+1}) - 1) = O(\log n)

一直對半拆基本上都是log。

Exercise !

148 - Guess Number

如果他想的數字 < 你問的數字,

那就回傳1,反之回傳0。

你可以使用的函式

int less(int guess_num)

如果你已經知道他在想什麼,就用guess告訴他

void guess(int guess_num)

題目:
請在15次以內猜到範圍1~100的數字。

148 - Guess Number

1~100

less(50)

ret 1

ret 0

1~49

50~100

less(25)

ret 1

ret 0

1~24

...

25~49

less(75)

ret 1

ret 0

50~74

75~100

如果他想的數字 < 你問的數字,

那就回傳1,反之回傳0。

int less(int guess_num)

148 - Guess Number

[1, 100]

[1,49]

[50,100]

int l = 1, r = 100;
while(l != r){
    int mid = (l+r)/2;
    if (less(mid)) 
      r = mid-1;
    else 
      l = mid;
}
guess(l);

[1,24]

[25,49]

[50,74]

[75,100]

一個WA code

l

r

到甚麼時候為止?

...

剩下一個可能的時候。

為甚麼啊Q_Q?

想想看
l, r = [1, 2]會怎麼樣?

148 - Guess Number

[1, 100]

[1,49]

[50,100]

[1,24]

[25,49]

[50,74]

[75,100]

比較醜的AC code

l

r

...

int l = 1, r = 100;
while(r - l > 1){
    int mid = (l+r)/2;
    if (less(mid)) 
      r = mid-1;
    else 
      l = mid;
}
if(r==l || less(r))
  guess(l);
else
  guess(r);

148 - Guess Number

[1, 101)

[1,50)

[50,101)

int l = 1, r = 101;
while(r - l != 1){
    int mid = (l+r)/2;
    if (less(mid)) 
      r = mid;
    else 
      l = mid;
}
guess(l);

[1,25)

[25,50)

[50,75)

[75,101)

l

r

...

比較美的AC code

Hint: 記錄左換成紀錄左

為甚麼這樣
就不會有1,2問題?

Exercise ?

如何發音ssǝnꓨ ɹǝqɯnN?

你可能要先知道怎麼發音
Number Guess

你才會知道
ssǝnꓨ ɹǝqɯnN怎麼發音

364 - ssǝnꓨ ɹǝqɯnN

請你發揮卑鄙源之助的精神,讓別人猜你的數字的時候,次數最大化。

364 - ssǝnꓨ ɹǝqɯnN

範例code

自己寫。

一個比較實際的應用

尋找最近的值!

位置 0 1 2 3 4 5 6
陣列 1 4 7 8 10 13 15

題目:
給一個排序過後的陣列,
問這個陣列誰和x最相近?

假設陣列長這樣,然後x=6?

尋找最近的值!

位置 0 1 2 3 4 5 6
陣列 1 4 7 8 10 13 15

題目:
給一個排序過後的陣列,
問這個陣列誰和x最相近?

假設x=6

最近值感覺好像很難找也...

如果換成比x還要小最大數字呢?

尋找最近的值!

位置 0 1 2 3 4 5 6
陣列 1 4 7 8 10 13 15

假設x=6

,求出比x還要小最大數字

6 < 8

都不會是答案

4 < 6

都不會是答案

目前的最佳解

...

尋找最近的值! - Code

int ary[] = {1, 4, 7, 8, 10, 13, 15}, x;
cin >> x;
int l = 0, r = 7;
while (r - l != 1) {
  int mid = (r+l)/2;
  if (ary[mid] < x) { 
    // mid是目前的最好答案,左邊都不用看。
  	l = mid;
  } else {
    // mid右邊 > mid >= x,所以右邊都不用看。
  	r = mid;
  }
}
cout << ary[l];

不是,啊最近的值呢?

就...就這樣?

這個二分搜尋法也太爛了吧...

想像...

一個工廠

算不出最低成本多少 :(

想要做一個東西,
成本最高可以接受10000

但給個成本,可以告訴你做不做得出來 :)

怎麼問工廠最少次,
得出最低成本?

5000?

7500?

8750?

.......

總之就是
二分搜

so?

看起來很紙上談兵 (?)

Exercise!

APCS - 基地台

基地台

題目: 在一條數線上,給定n個住家的位置。
你可以放置k個基地台,請問基地台的覆蓋半徑最小要設定成多少才可以蓋到所有住家?

(所有基地台的半徑一樣,
基地台可以自己設定。)

1

3

11

k = 1
r最低設5
k = 2
r最低設1

1

3

11

直徑=10

直徑=2

直徑=2

好難喔?

比較簡單的           

題目:
給定n個住家位置,
可放置k個基地台,
基地台的覆蓋半徑最小
要多少才可以覆蓋全部住家?

k = 3

1

3

11

題目‧改:
給定n個住家位置,
可放置k個基地台,
基地台的覆蓋半徑為r
有沒有辦法覆蓋全部住家?

基地台

  1. 從左邊開始看,讓基地台的左界卡到第一個房子
  2. 把第一個基地台可以覆蓋到的房子全部忽略
  3. 重新回到第一步。
  • 為什麼是對的?

5

9

12

時間複雜度: O(n)

比較簡單的基地台

題目:
給定n個住家位置,
可放置k個基地台,
基地台的覆蓋半徑最小
要多少才可以覆蓋全部住家?

一個工廠

算不出最小半徑多少 :(

但給個半徑,可以告訴你做不做得出來 :)

怎麼問工廠最少次,
得出最小半徑?

總之就是
二分搜

題目‧改

一般的基地台

題目:
給定n個住家位置,
可放置k個基地台,
基地台的覆蓋半徑最小
要多少才可以覆蓋全部住家?

步驟 題目·改
(給半徑看O不OK)
二分搜尋的次數 總共的時間複雜度
複雜度 O(n) O(log D) O( n log D )

D: 村莊座標的範圍

參考用的code

#include <iostream>
#include <algorithm>
using namespace std;
int ary[50001], n, k;
// 判斷如果直徑為x,只能放k個,可不可以完成任務。
bool is_ok(int x){
    int last = -x-1, now_put = 0;  
    for(int i=0; i<n; i++){
        // 如果上一次放的基地台不能覆蓋到這個服務點,
        // 表示要覆蓋這個服務點勢必要多一個基地台。
        // 並且最好策略是是剛好讓這個服務點剛好在基地台的左界。
        if(last + x < ary[i]){
            // 多放一個。
            now_put ++;
            // 紀錄現在最靠右的基地台的左界在哪。
            last = ary[i];
            // 如果放超過k個表示不能完成任務,回傳false。
            if(now_put > k){
                return false;
            }
        }
    }
    // 一切沒事就回傳true,表示可以完成任務。
    return true;
}
int main(){
    cin >> n >> k;
    for(int i=0; i<n; i++)
        cin >> ary[i];
    // 先將服務點排序,比較好做事。
    sort(ary, ary+n);
    int L=0, R=1000000000;
    // 二分搜尋法尋找最小可以完成的個數
    while(R-L != 1){
        int mid = (L+R) / 2;
        // 如果mid可以完成任務,表示mid~R都可以完成任務,所以搜L~mid
        // 如果不行,就表示範圍在mid~R。
        if(is_ok(mid))
            R = mid;
        else
            L = mid;
    }
    cout << R << endl;
    return 0;
}

如果沒有上界呢?

Here we go again

一樣的猜數字問題,
但是不告訴你上界 🤪

我心中所想的數字是N,\\ 你能夠做到O(\log N)複雜度嗎?

(偽)倍增算法

答案的N,\\ 近在咫尺,卻遠在天邊...
可惡... \\要是知道r的話就可以用二分搜了...
啊...,N到底在哪裡呢?

(偽)倍增算法

2^0
2^1
2^2
2^3
....
2^{30}
2^{31}

哇! 看來你找到一個好的上界了!

2^{30}
2^{31}
5 \times 2^{30}
只花了O(\log N)次!

找到上界後開始二分搜尋!

還是O(\log N)次
O(\log N)

Review

  • 總之如果你要搜尋的東西是具有單調性的,
    就可以使用二分搜尋法來找你要的值。
     
  • 左閉右開的表達方法很方便!
     
  • 不知道最大多少可以使用倍增算法。

(單調性: 要馬遞增要馬遞減)

Review

結束了...嗎?

Binary Search

By Arvin Liu

Binary Search

  • 1,207