二分搜尋法 - Arvin Liu @ Sprout 2021
我會告訴你有沒有猜對
別騙了,這不就是要猜10次?
我會告訴你有沒有猜對或有沒有比我的數字還要大
(如果你是理組)
你會下意識的想要猜中間,為甚麼?
credit to: 台大資工b07 程式電神人生勝利組 Joe Tsai 蔡銘軒軒哥
第一次翻書: 翻到S
不用找了
不用撕掉==
第二次翻書: 翻到L
不用找了
第三次翻書: 翻到E
不用找了
範圍變小了
一個一個找
OK的
一個酷酷的快速的搜尋法。
為什麼要猜中間呢?
畫個可能圖吧!
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。
如果他想的數字 < 你問的數字,
那就回傳1,反之回傳0。
你可以使用的函式
int less(int guess_num)
如果你已經知道他在想什麼,就用guess告訴他
void guess(int guess_num)
題目:
請在15次以內猜到範圍1~100的數字。
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)
[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]會怎麼樣?
[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);
[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問題?
你可能要先知道怎麼發音
Number Guess
你才會知道
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
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
陣列 | 1 | 4 | 7 | 8 | 10 | 13 | 15 |
假設x=6
6 < 8
都不會是答案
4 < 6
都不會是答案
目前的最佳解
...
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?
.......
總之就是
二分搜
看起來很紙上談兵 (?)
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: 村莊座標的範圍
#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;
}
一樣的猜數字問題,
但是不告訴你上界 🤪
哇! 看來你找到一個好的上界了!
找到上界後開始二分搜尋!
(單調性: 要馬遞增要馬遞減)