Binary Search
二分搜尋法 - Arvin Liu @ Sprout 2021
一個大家都玩過的遊戲(?)
猜猜我在想甚麼數字?
我在心中想一個介於1~10的數字,猜猜看我在想什麼
我會告訴你有沒有猜對
別騙了,這不就是要猜10次?
猜猜我在想甚麼數字?
我在心中想一個介於1~100的數字,猜猜看我在想什麼
我會告訴你有沒有猜對或有沒有比我的數字還要大
(如果你是理組)
你會下意識的想要猜中間,為甚麼?
來當五分鐘的哈佛學生
credit to: 台大資工b07 程式電神人生勝利組 Joe Tsai 蔡銘軒軒哥
一個快快的從電話簿查名字的方法
所以他想表達甚麼?
- 電話簿是按照字典序排序的。 (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
一直對半拆基本上都是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
1
3
11
直徑=10
直徑=2
直徑=2
好難喔?
比較簡單的
題目:
給定n個住家位置,
可放置k個基地台,
基地台的覆蓋半徑最小
要多少才可以覆蓋全部住家?
1
3
11
題目‧改:
給定n個住家位置,
可放置k個基地台,
基地台的覆蓋半徑為r
問有沒有辦法覆蓋全部住家?
基地台
- 從左邊開始看,讓基地台的左界卡到第一個房子。
- 把第一個基地台可以覆蓋到的房子全部忽略。
- 重新回到第一步。
- 為什麼是對的?
5
9
12
比較簡單的基地台
題目:
給定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
一樣的猜數字問題,
但是不告訴你上界 🤪
(偽)倍增算法
(偽)倍增算法
哇! 看來你找到一個好的上界了!
找到上界後開始二分搜尋!
Review
- 總之如果你要搜尋的東西是具有單調性的,
就可以使用二分搜尋法來找你要的值。
-
左閉右開的表達方法很方便!
- 不知道最大多少可以使用倍增算法。
(單調性: 要馬遞增要馬遞減)
Review
結束了...嗎?
Binary Search
By Arvin Liu
Binary Search
- 1,359