二分搜 === ---- ## 什麼是二分搜? ---- 一種快速的在資料中 尋找所要的資料的方法 --- 在介紹他之前先講一個小遊戲 ---- ### 猜數字 一個人想1~100中的一個數字 另一個人不斷地猜數字職猜中為止 想數字的人只會回答 __比答案大__ 或 __比答案小__ 及 __猜中__ ---- 怎麼樣猜得最快? <span><!-- .element: class="fragment" data-fragment-index="1" -->從1一個一個猜到100?</span> <span><!-- .element: class="fragment" data-fragment-index="2" -->X</span> <span><!-- .element: class="fragment" data-fragment-index="3" --> **每次都猜中間值!** </span> ---- 我們來看看為什麼那是最快的 <span><!-- .element: class="fragment" data-fragment-index="1" -->假設答案是82</span> <span><!-- .element: class="fragment" data-fragment-index="2" -->那我們就會猜 _50_ , _87_ , _68_ , _77_ , _82_ , 花了五次就找到了答案</span> <span><!-- .element: class="fragment" data-fragment-index="3" -->而從1開始找則會花82次才能找到</span> ---- #### 來分析一下時間複雜度 先來看第一個方法 從1開始找 <span><!-- .element: class="fragment" data-fragment-index="1" -->對他來說 最糟的情況就是目標在n</span> <span><!-- .element: class="fragment" data-fragment-index="2" -->1到n會跑N次 所以時間複雜度是O(N) </span> ---- 接下來我們來看 用中間值去找的方法: <span><!-- .element: class="fragment" data-fragment-index="1" -->因為每次要找的範圍都會除2</span> <span><!-- .element: class="fragment" data-fragment-index="2" -->所以最糟的情況下 2$^{k}$=n</span> <span><!-- .element: class="fragment" data-fragment-index="3" -->k=log(N)</span> <span><!-- .element: class="fragment" data-fragment-index="4" -->所以時間複雜度是O(log(N)) </span> --- ### 這樣的演算法就叫「二分搜尋法」 利用這項演算法 我們可以在「有序陣列」中 以O( log(N) ) )的複雜度找到目標 ---- 實際上要怎麼寫? ---- 我們有3種寫二分搜的方法 <span><!-- .element: class="fragment" data-fragment-index="1" -->第1個是用一個while迴圈 不斷移動左界和右界來找目標</span> <span><!-- .element: class="fragment" data-fragment-index="2" -->第2個是用for迴圈一段一段往前跳</span> <span><!-- .element: class="fragment" data-fragment-index="3" -->第3個則是用內建的STL</span> <span><!-- .element: class="fragment" data-fragment-index="4" -->接下來會一一列出程式碼說明</span> --- 我們利用這個例題來解釋 ``` 給一個長度N的陣列,進行M次詢問K,請輸出陣列中大於K的最小值 N,M<1e5,陣列的值及k<1e9 ``` ---- 先來分析複雜度 如果直接用一個一個找的話複雜度會是 __O(NM)__ 很明顯不行 這時候就要用二分搜了 ---- 利用二分搜我們會需要先排列這個陣列 <span><!-- .element: class="fragment" data-fragment-index="1" -->排列的複雜度是O( Nlog(N) )</span> <span><!-- .element: class="fragment" data-fragment-index="2" -->而搜尋m次每次的複雜度都是O( log(N) )</span> <span><!-- .element: class="fragment" data-fragment-index="3" -->所以搜尋的部分就是O( Mlog(N) )</span> <span><!-- .element: class="fragment" data-fragment-index="4" -->整個的複雜度就是max( Nlog(N) , Mlog(N) ) )</span> ---- 看起來時間是不會爆的 那我們就可以開始寫了 --- ### 用while的方法: ```cpp= #include<bits/stdc++.h> using namespace std; #define int long long const int N=1e5+5; int n,m,k,arr[N]; signed main(){ cin>>n>>m; for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+n+1);//排列 arr[n+1]=-1; //找不到時回傳-1 for(int i=1;i<=m;i++){ cin>>k; int l=1,r=n+1,mid;//初始化左界、右界、中點、左閉右開 while(l<r){ //當在正常範圍時重複: mid=(l+r)/2; //計算中點(注意無條件捨去會偏左) if(arr[mid]>k){ //當k在mid的左邊時: r=mid; //將右界移到mid }else{ l=mid+1; //否則把右界移到mid+1 } } cout<<arr[l]<<"\n"; } return 0; } ``` ---- ### 用跳的找 ```cpp= #include<bits/stdc++.h> using namespace std; #define int long long const int N=1e5+5; int n,m,k,arr[N]; signed main(){ cin>>n>>m; for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+n+1);//排列 arr[n+1]=-1; //找不到時回傳-1 for(int i=1;i<=m;i++){ cin>>k; int w=0; for(int jump=n;jump>0;jump/=2){ //注意要用while不是if while(w+jump<=n and arr[w+jump]<=k){ w+=jump; } } cout<<arr[w+1]<<"\n"; //w會停在最大<=k的地方,所以要加1 } return 0; } ``` ---- ### 用STL ```cpp= #include<bits/stdc++.h> using namespace std; #define int long long const int N=1e5+5; int n,m,k,arr[N]; signed main(){ cin>>n>>m; for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+n+1);//排列 arr[n+1]=-1; //找不到時回傳-1 for(int i=1;i<=m;i++){ cin>>k; int ans=*upper_bound(arr+1,arr+1+n,k); cout<<ans<<"\n"; } return 0; } ``` --- ### 二分搜習題 [MD Judge A051](http://mdcpp.mingdao.edu.tw/problem/A051) [MD Judge B029](http://mdcpp.mingdao.edu.tw/problem/B029) [MD Judge B051](http://mdcpp.mingdao.edu.tw/problem/B051) --- ### 外掛二分搜 外掛二分搜就是把判斷要往左往右找 的條件式另外寫一個bool函式 通常執行一次bool函式時間複雜度會是$O(N)$ 整體會是$O(NlogN)$ ---- 例題 [恢復能量的白雲熊膽丸](http://mdcpp.mingdao.edu.tw/problem/AP015) ---- 這題比較簡單的想法是 把F從1一直跑到$10^{10}$ 只要滿足條件就break出來輸出F 這樣做不用算時間複雜度一定爆 我們可以觀察到 當F越大 需要嗑藥的次數就越少 是 __有序的__ 所以我們就能夠用外掛二分搜解決這題 ---- code ```cpp= #include<bits/stdc++.h> using namespace std; #define int long long const int N=1e5+5; int n,m,p[N]; bool check(int f){ int power=f; int cnt=0; for(int i=1;i<=n;i++){ if(p[i]>f) return 1; if(power<p[i]){ power=f; cnt++; } power-=p[i]; } if(cnt>m) return 1; else return 0; } signed main(){ ios::sync_with_stdio(0);cin.tie(0); cin>>n>>m; for(int i=1;i<=n;i++) cin>>p[i]; int ans=0; p[0]=0; for(int jump=1e15;jump>0;jump/=2){ while(check(ans+jump)){ ans+=jump; } } cout<<ans+1; } ``` ---- ### 練習題 [基地台](http://mdcpp.mingdao.edu.tw/problem/AP014)
{"title":"二分搜","image":"https://hackmd.io/_uploads/rkSqrRtkp.png","contributors":"[{\"id\":\"443d51bf-5a61-411d-b3cb-c3371649227c\",\"add\":16553,\"del\":11678}]"}
    330 views