###### tags: `MDCPP`
#### MDCPP進階組講義04
## 二分搜 Binary Search
作者:LittlePants
----
## 何謂二分搜
- 就是每次都把搜尋區間縮小一半的搜尋方式
----
## 例題
- 給你一個長度為N的已排序陣列,要你找出大於等於x的第一個數
----
## 方法1
- 把陣列從頭到尾掃一遍
- 複雜度 $O(n)$
----
## 方法2
- 既然都給了以排序的陣列,那就可以好好利用它的性質
- 使用二分搜 複雜度$O(logN)$
----
## 思考
- 對於每個在陣列裡的數,他不是大於等於x,就是小於x
- 雖然聽起來像幹話,但轉化一下就變成
----
- 如果陣列中這個數字小於x,那x就一定在他右邊
- 反之x就會在這個數左邊或這個數就是x
----
- 我們可以善用這個性質
- 每次都先用搜索區間的中點進行判斷
- 就可以將搜索範圍減少一半
----
## 實作練習
- 我們來用程式把二分搜打出來(記得如果題目沒排序的話要先排序)
----
```cpp=
int bs(int x){ // x 是我們要找的數字
// 假設題目給的陣列存在 a[] 長度為 n
int l = 1, r = n;
// 假設數列存在 a[1] ~ a[n]
while(l < r){
int mid = (l+r)/2; // 區間中點
if(a[mid] < x) l = mid + 1;
// x 在 mid 的右邊 所以範圍變為右半邊
else r = mid; // x 在 mid 的左邊
}
return a[l];
}
```
---
## STL函式教學
----
## lower_bound / upper_bound
- lower_bound(x)可以取出陣列中大於**等於**x的第一個數
- upper_bound(x)則是取出陣列中大於x的第一個數
- 兩個函式都是回傳指向那個數的指標(或迭代器)
----
## 用法
```cpp=
int a[100], n, x;
int main(){
cin>>n>>x;
for(int i = 0; i < n; i++)
cin>>a[i];
sort(a, a+n);
auto ptr = lower_bound(a, a+n, x);
// lower_bound(first, last, goal);
//放入要搜尋的範圍 和 要搜尋的值
cout << *ptr << '\n';
}
```
vector也有這個功能,但以前教過了這裡便不多說
----
## 小問題
- 如何找到小於x最大的數?
----
```cpp=
auto ptr = prev(lower_bound(a, a+n, x));
```
---
## 小練習
----
## [CF 706B Interesting drink](https://codeforces.com/problemset/problem/706/B)
大家練習下英文吧
----
這邊有翻譯 大家可以先不要點開來看
:::spoiler
題目給你一個陣列和q個數字
要分別輸出這q個數字大於等於陣列裡的幾個數字
:::
----
----
## 思考
- 如果每次都掃描一遍陣列 時間複雜度$O(QN)$
- 很明顯會超時
----
我們改成用二分搜
首先要先排序陣列 時間複雜度$O(NlogN)$
在對每個詢問使用二分搜 複雜度$O(QlogN)$
### 總複雜度$O((Q+N)logN)$
----
### 小提醒
提醒一下大家不要太過依靠stl函式
因為有些題目一定要用自己寫的二分搜(等下會教)
所以大家一定要練習自己寫看看
---
## 進階用法
----
## 對答案二分搜
----
## 甚麼是對答案二分搜?
就是在答案的範圍內用二分搜去猜解答案
----
## 例題 終極密碼
電腦會在1 - 100000 的區間內選一個數字
你要用最少的次數猜出電腦選的數字
每次猜完後電腦會回答你 biger,lower和yes
分別代表你的猜測 大於這個數字/小於這個數字/猜對了
----
## 思考
很明顯這一題我們可以對答案範圍(1 - 100000)
進行二分搜
因為這題的答案具有單調性
----
甚麼是單調性呢?
就是我們先選一個數
然後可以判斷出答案會比這個數小 還是比這個數大
因此我們可以決定搜索區間要變成左半邊或右半邊
----
## [TIOJ 1044](https://tioj.ck.tp.edu.tw/problems/1044)
這題是互動題 跟例題有87%像 有興趣的同學可以寫寫看
----
## 例題二
----
## [MDCPP 基地台(APCS201703)](http://mdcpp.mingdao.edu.tw/problem/AP014)
請大家閱讀一下題目
----
對答案二分搜通常會跟貪心算法結合在一起
雖然我們還沒有教過 但貪心其實就是個很直覺的東西
大家應該可以很好理解
----
## 思考
首先我們先把P陣列排序
然後我們知道 答案的範圍會在
$1 \sim (P[N-1] - P[0])$之間
----
然後再想一下我們的貪心策略
解如我們選定了一個直徑 R
----
我們從左到右遍歷所有服務點
當我們遇到了一個沒被覆蓋的服務點 P[i]
我們一定要覆蓋他
所以我們可以用將基地台的最左端對齊這個點
所以我們可以覆蓋到 P[i] ~ P[i] + R
----
這樣我們就可以求出當直徑為R時
至少要多少個基地台
然後可以依據需要的基地台數大於 K 或 小於 K
作為二分搜的依據
----
二分搜複雜度 $O(log 10^9)$ 大約等於 30
排序複雜度 $O(NlogN)$
遍歷複雜度 $O(N)$
## 總複雜度$O(NlogN + Nlog10^9)$
----
參考code
:::spoiler
```cpp=
#include<bits/stdc++.h>
#define ft first
#define sc second
#define all(x) (x).begin(),(x).end()
#define sz(x) (int)(x).size()
#define mem(x,val) memset(x,val,sizeof x)
#define pii pair<int,int>
#define pb emplace_back
#define ar array
#define int long long
#define wopen(x) freopen((x),"w",stdout)
#define ropen(x) freopen((x),"r",stdin)
#define mid (l+r>>1)
using namespace std;
int n, k, l, r, p[50005];
bool check(int x){
int c = p[0] + x - 1, cnt = 1;
for(int i = 1; i < n; i++){
if(p[i] > c) cnt++, c = p[i] + x;
}
return cnt <= k;
}
signed main(){
ios_base::sync_with_stdio(0); cin.tie(0);
cin >> n >> k;
for(int i = 0; i < n; i++){
cin >> p[i];
r = max(p[i], r);
}
l = 1;
sort(p, p + n);
while(l < r){
if(check(mid)){
r = mid;
} else {
l = mid+1;
}
}
cout << l << '\n';
}
```
:::
---
## 回家練習
----
[NPSC 零](http://mdcpp.mingdao.edu.tw/problem/C032)
有用到一點數學觀念大家可以想想看
----
[夏綠蒂的回家作業](http://mdcpp.mingdao.edu.tw/problem/A051)
二分搜的應用
是一個蠻重要的觀念
----
{"metaMigratedAt":"2023-06-15T15:48:02.441Z","metaMigratedFrom":"YAML","title":"MDCPP 二分搜講義","breaks":true,"slideOptions":"{\"theme\":\"solarized\",\"transition\":\"slide\"}","contributors":"[{\"id\":\"853e1517-7034-4077-803a-7bb83a49f00a\",\"add\":4227,\"del\":279}]"}