• 二分搜索——不光是查找值


    摘要:

      本文主要讲述了二分搜索算法的基本思想和实现原理,着重讲解了二分搜索法在编程竞赛中的一些典型应用。

    • 基本思想
    • 实现原理
    • 典型应用
    • 例题解析

       基本思想

      二分搜索法的基本思想是通过不断的缩小解可能存在的范围,从而求得问题最优解的方法。比如一个直观的问题是在一个由小到大的数列a中找到一个数ai,使得ai满足ai>=k的同时,i最小。由于此数列是有序的,所以找到中间位置的数amid和k比较,如果k<=amid,证明k在左半区域,区间左边界变为mid,否则在右半区域,区间右边界变为mid。如此下来,直至区间长度小于1,结束循环,最小的i就是右边界的值。

      二分搜索的优势在于时间上的高效,每次去掉一半的搜索区间,可以在O(logn)的时间复杂度求得最终的解。但是这种高效是建立在事先将数排好序的基础上的。

       实现原理

      反复与区间的中点进行比较,不断的将解的范围缩小到原来的一半,直至满足一定的条件后结束算法。简单起见,直接看一道VJudge上的例题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) {
     8     int lb = -1, ub = n;//初始化区间短点 
     9     while(ub - lb > 1) {//结束条件是当区间的长度小于1的时候 
    10         int mid = (ub + lb) / 2;
    11         //通过判断缩小区间为原来的一半 
    12         if(a[mid] >= k)
    13             ub = mid;
    14         else
    15             lb = mid;
    16     }
    17     
    18     printf("%d
    ", ub);
    19 }
    20 
    21 int main()
    22 {
    23     while(scanf("%d", &n) != EOF) {
    24         for(int i = 0; i < n; i++) {
    25             scanf("%d", &a[i]);
    26         }
    27         
    28         scanf("%d", &q);
    29         while(q--) {
    30             int k;
    31             scanf("%d", &k);
    32             query(k);//查询k 
    33         }
    34     }
    35     return 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 () {
    11     while(scanf("%d", &n) != EOF) {
    12         v.clear();
    13         for(int i = 0; i < n; i++) {
    14             int tmp;
    15             scanf("%d", &tmp);
    16             v.push_back(tmp);
    17         }
    18         
    19         scanf("%d", &q);
    20         while(q--) {
    21             int k;
    22             scanf("%d", &k);
    23             
    24             printf("%d
    ", lower_bound(v.begin(), v.end(), k) - v.begin());
    25         }
    26     }    
    27     return 0;
    28 }

      在编程竞赛中的典型应用

      二分搜索算法除了在有序数组中查找值之外,在求解最优解的问题上也非常有用。比如求满足某个条件C(x)的最小x,我们可以假定一个区间,取该区间的中点值为mid,如果满足C(mid),左边界等于区间中点,即将范围缩小在右侧(最大化),如果不满足,那么右边界等于区间中点,即将范围缩小在左侧。直到将区间缩小到一定的程度后,在此精度下的问题最优解就求出来了。

      1.假定一个解并判断是否可行,例如POJ 1064 Cable master

      2.最大化最小值,例如POJ 2456 Aggressive cows

      3.最大化平均值,例如POJ 3111 K Best

      例题解析

      POJ 1064 Cable master

      有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) {
    10     int num = 0; 
    11     for(int i = 0; i < N; i++) {
    12         num += (int)(L[i] / x);
    13     }
    14     return num >= K;
    15 }
    16 void solve() {
    17     double lb = 0, ub = inf;
    18     for(int i = 0;i < 100; i++) {
    19         double mid = (ub + lb) / 2;
    20         if(C(mid))
    21             lb = mid;
    22         else 
    23             ub = mid;
    24     }
    25     
    26     printf("%.2f
    ", floor(ub * 100) / 100);
    27 }
    28 int main()
    29 {
    30     while(scanf("%d%d", &N, &K) != EOF) {
    31         for(int i = 0; i < N; i++) {
    32             scanf("%lf", &L[i]);
    33         }
    34         
    35         solve();
    36     }    
    37     return 0;
    38 } 

      POJ 2456 Aggressive cows

      将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) {
    11     int last = 0; 
    12     for(int i = 1; i < M; i++) {
    13         int crt = last + 1;
    14         while(crt < N && x[crt] - x[last] < d) {
    15             crt++;
    16         }
    17         
    18         if(crt == N)    return false;
    19         last = crt;
    20     }
    21     return true;
    22 }
    23 void solve() {
    24     sort(x, x + N);
    25     
    26     int lb = 0, ub = inf;
    27     while(ub - lb > 1) {
    28         int mid = (ub + lb) / 2;
    29         if(C(mid))
    30             lb = mid;
    31         else
    32             ub = mid;
    33     }     
    34     
    35     printf("%d
    ", lb);
    36 }
    37 int main()
    38 {
    39     while(scanf("%d%d", &N, &M) != EOF) {
    40         for(int i = 0; i < N; i++) {
    41             scanf("%d", &x[i]);
    42         }
    43         
    44         solve();
    45     }
    46     return 0;
    47 } 

      POJ 3111 K Best

      有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 {
    12     double val;
    13     int id;
    14 } y[maxn];
    15 
    16 bool cmp(struct Node a, struct Node b) {
    17     return a.val > b.val;
    18 }
    19 bool C(double x) {
    20     for(int i = 0; i < N; i++) {
    21         y[i].id = i + 1;
    22         y[i].val = (v[i] - x * w[i]);
    23     }
    24     
    25     sort(y, y + N, cmp);
    26     double sum = 0;
    27     for(int i = 0; i < K; i++) {
    28         sum += y[i].val;
    29     }
    30     return sum >= 0;
    31 }
    32 void solve() {
    33     double lb = 0, ub = inf;
    34     
    35     while(ub - lb > EPS) {
    36         double mid = (ub + lb) / 2;
    37         if(C(mid))
    38             lb = mid;
    39         else
    40             ub = mid;
    41     }    
    42     
    43     for(int i = 0; i < K - 1; i++) {
    44         printf("%d ", y[i].id);
    45     }
    46     printf("%d ", y[K - 1].id);
    47 }
    48 
    49 int main()
    50 {
    51     while(scanf("%d%d", &N, &K) != EOF) {
    52         for(int i = 0; i < N; i++) {
    53             scanf("%d%d", &v[i], &w[i]);
    54         }
    55         
    56         solve();
    57     }    
    58     return 0;
    59 }

      至此,二分搜索算法就讲完了,二分的思想就是一次去一半,寻找满足条件的最优,在解决最优解问题的时候要找好结束条件和满足的条件。算法不难,关键是思考问题和实现算法的过程。

  • 相关阅读:
    线程间协作的两种方式:wait、notify、notifyAll和Condition
    Lock
    线程池ExecutorService的使用
    使用volatile的条件
    解决共享资源竞争
    并发时捕获异常
    Executor执行器
    BufferedReader 和BufferedWriter
    彻底理解Java的Future模式
    贝叶斯网络简介--翻译版
  • 原文地址:https://www.cnblogs.com/wenzhixin/p/9630277.html
Copyright © 2020-2023  润新知