Binary Search
10^11
目錄
- 二分搜介紹
- 實作
- 題解三段跳
- 補充競程
- KAHOOT!
目錄
三段跳是真的跳
大概是1星3星5星難度的感覺
希望大家至少能學到3星
搜尋演算法
先來看看什麼是搜尋演算法
搜尋演算法
簡單來說,
就是在一群資料中,
想盡辦法找到目標資料
搜尋演算法
大概分成這幾種
- 線性搜尋(Linear search)
- 二元搜尋(Binary search)
- 指數搜尋(Exponential search)
- 插補搜尋(Interpolation search)
- 費氏搜尋(Fibonacci search)
搜尋演算法
顯然,我們今天的重點是二分搜
順便補充一下,
二分搜尋的時間複雜度是 O(logN)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 愛錢成癡,嗜賭成癮
Binary Search
- 76