二分搜尋——不光是查詢值
摘要:
本文主要講述了二分搜尋演算法的基本思想和實現原理,著重講解了二分搜尋法在程式設計競賽中的一些典型應用。
- 基本思想
- 實現原理
- 典型應用
- 例題解析
基本思想
二分搜尋法的基本思想是通過不斷的縮小解可能存在的範圍,從而求得問題最優解的方法。比如一個直觀的問題是在一個由小到大的數列a中找到一個數ai,使得ai滿足ai>=k的同時,i最小。由於此數列是有序的,所以找到中間位置的數amid和k比較,如果k<=amid,證明k在左半區域,區間左邊界變為mid,否則在右半區域,區間右邊界變為mid。如此下來,直至區間長度小於1,結束迴圈,最小的i就是右邊界的值。
二分搜尋的優勢在於時間上的高效,每次去掉一半的搜尋區間,可以在O(logn)的時間複雜度求得最終的解。但是這種高效是建立在事先將數排好序的基礎上的。
實現原理
反覆與區間的中點進行比較,不斷的將解的範圍縮小到原來的一半,直至滿足一定的條件後結束演算法。簡單起見,直接看一道VJudge上的例題ITP2_6_C" target="_blank" rel="nofollow,noindex">Lower Bound ,具體細節參考如下程式碼:
1 #include <cstdio> 2 3 const int maxn = 100000 + 10; 4 int a[maxn]; 5 int n, q; 6 7 void query(int k) { 8int lb = -1, ub = n;//初始化區間短點 9while(ub - lb > 1) {//結束條件是當區間的長度小於1的時候 10int mid = (ub + lb) / 2; 11//通過判斷縮小區間為原來的一半 12if(a[mid] >= k) 13ub = mid; 14else 15lb = mid; 16} 17 18printf("%d\n", ub); 19 } 20 21 int main() 22 { 23while(scanf("%d", &n) != EOF) { 24for(int i = 0; i < n; i++) { 25scanf("%d", &a[i]); 26} 27 28scanf("%d", &q); 29while(q--) { 30int k; 31scanf("%d", &k); 32query(k);//查詢k 33} 34} 35return 0; 36 }
當然C++中的STL以lower_bound函式的形式實現了二分搜尋,直接可以呼叫。參考程式碼如下:
1 #include <cstdio> 2 #include <algorithm> 3 #include <vector> 4 using namespace std; 5 6 const int maxn = 100000 + 10; 7 int n, q; 8 vector<int> v; 9 10 int main () { 11while(scanf("%d", &n) != EOF) { 12v.clear(); 13for(int i = 0; i < n; i++) { 14int tmp; 15scanf("%d", &tmp); 16v.push_back(tmp); 17} 18 19scanf("%d", &q); 20while(q--) { 21int k; 22scanf("%d", &k); 23 24printf("%d\n", lower_bound(v.begin(), v.end(), k) - v.begin()); 25} 26} 27return 0; 28 }
在程式設計競賽中的典型應用
二分搜尋演算法除了在有序陣列中查詢值之外,在求解最優解的問題上也非常有用。比如求滿足某個條件C(x)的最小x,我們可以假定一個區間,取該區間的中點值為mid,如果滿足C(mid),左邊界等於區間中點,即將範圍縮小在右側(最大化),如果不滿足,那麼右邊界等於區間中點,即將範圍縮小在左側。直到將區間縮小到一定的程度後,在此精度下的問題最優解就求出來了。
1.假定一個解並判斷是否可行,例如POJ 1064 Cable master
2.最大化最小值,例如POJ 2456 Aggressive cows
3.最大化平均值,例如 POJ 3111 K Best
例題解析
有N條繩子,長度分別是Li,如果從它們中切割出K條長度相同的繩子的話,每條繩子最長是多少
我們將區間設為0到INF(最大),然後嘗試區間的中點,向滿足條件的最大值逼近(迴圈100次精度大概是1e-30)。條件C(x):Li/x的總和是否大於K,程式碼如下:(坑點是最後輸出保留兩位小數,不進位)
1 #include <cstdio> 2 #include <cmath> 3 const int maxn = 10000 + 10; 4 const int inf = 99999999; 5 6 int N, K; 7 double L[maxn]; 8 9 bool C(double x) { 10int num = 0; 11for(int i = 0; i < N; i++) { 12num += (int)(L[i] / x); 13} 14return num >= K; 15 } 16 void solve() { 17double lb = 0, ub = inf; 18for(int i = 0;i < 100; i++) { 19double mid = (ub + lb) / 2; 20if(C(mid)) 21lb = mid; 22else 23ub = mid; 24} 25 26printf("%.2f\n", floor(ub * 100) / 100); 27 } 28 int main() 29 { 30while(scanf("%d%d", &N, &K) != EOF) { 31for(int i = 0; i < N; i++) { 32scanf("%lf", &L[i]); 33} 34 35solve(); 36} 37return 0; 38 }
將M頭牛放在N個牛舍裡,使得這M頭牛之間的舉例儘可能的大。
意即求解滿足C(d)的最大d。而其中的d表示M頭牛之間的舉例均不小於d。區間的結束條件就是區間長度大於等於1,關鍵是如何判斷滿足條件,其實也好判,採用貪心的方法,一次往後使用即可,如果M頭牛安排完了,滿足,否則不滿足。程式碼如下:
1 #include <cstdio> 2 #include <algorithm> 3 using namespace std; 4 5 const int maxn = 100000 + 10; 6 const int inf = 1000000000 + 10; 7 int N, M; 8 int x[maxn]; 9 10 bool C(int d) { 11int last = 0; 12for(int i = 1; i < M; i++) { 13int crt = last + 1; 14while(crt < N && x[crt] - x[last] < d) { 15crt++; 16} 17 18if(crt == N)return false; 19last = crt; 20} 21return true; 22 } 23 void solve() { 24sort(x, x + N); 25 26int lb = 0, ub = inf; 27while(ub - lb > 1) { 28int mid = (ub + lb) / 2; 29if(C(mid)) 30lb = mid; 31else 32ub = mid; 33} 34 35printf("%d\n", lb); 36 } 37 int main() 38 { 39while(scanf("%d%d", &N, &M) != EOF) { 40for(int i = 0; i < N; i++) { 41scanf("%d", &x[i]); 42} 43 44solve(); 45} 46return 0; 47 }
有n個物品的價值和重量分別是vi和mi,從中選出k件物品使得單位重量的價值最大。
C(x)=((vi - x * wi) 從大到小排列中的前k個的和不小於0)
精度1e-30
1 #include <cstdio> 2 #include <algorithm> 3 using namespace std; 4 5 const int maxn = 100000 + 10; 6 const double inf = 0x3f3f3f3f; 7 const double EPS = 1.0e-6; 8 9 int N, K; 10 int v[maxn], w[maxn]; 11 struct Node { 12double val; 13int id; 14 } y[maxn]; 15 16 bool cmp(struct Node a, struct Node b) { 17return a.val > b.val; 18 } 19 bool C(double x) { 20for(int i = 0; i < N; i++) { 21y[i].id = i + 1; 22y[i].val = (v[i] - x * w[i]); 23} 24 25sort(y, y + N, cmp); 26double sum = 0; 27for(int i = 0; i < K; i++) { 28sum += y[i].val; 29} 30return sum >= 0; 31 } 32 void solve() { 33double lb = 0, ub = inf; 34 35while(ub - lb > EPS) { 36double mid = (ub + lb) / 2; 37if(C(mid)) 38lb = mid; 39else 40ub = mid; 41} 42 43for(int i = 0; i < K - 1; i++) { 44printf("%d ", y[i].id); 45} 46printf("%d ", y[K - 1].id); 47 } 48 49 int main() 50 { 51while(scanf("%d%d", &N, &K) != EOF) { 52for(int i = 0; i < N; i++) { 53scanf("%d%d", &v[i], &w[i]); 54} 55 56solve(); 57} 58return 0; 59 }
至此,二分搜尋演算法就講完了,二分的思想就是一次去一半,尋找滿足條件的最優,在解決最優解問題的時候要找好結束條件和滿足的條件。演算法不難,關鍵是思考問題和實現演算法的過程。