二分搜索

  二分搜索法是通过不断缩小可能存在的范围,从而求得问题最优解的方法。

1. 从有序数组中查找某个值

  给定长度为 n 的单调非递减数列 a0......an,和一个数 k,求满足 ai ≥ k 条件的最小的 i,不存在的情况下输出n。

  

int n, k;
int a[MAX_N];
// STL
    void solve() {
        int ans = *lower_bound(a, a + n, k);
        printf("%d\n", ub);
    }
//
void solve() {
    //初始化解的范围
    int lb = -1, ub = n;
    //重复循环,直到解的范围不大于一
    while (ub - lb > 1) {
        int mid = (lb + ub) / 2;
        // 如果mid满足条件,则解的存在范围变为(lb, mid] 
        if (a[mid] >= k)
            ub = mid;
        // 如果不满足,则解的范围变为(mid, ub] 
        else
            lb = mid;
    } 
    printf("%d\n", ub);
} 

  求满足某个条件C(x)的最小的 x 这一问题。对于任意满足C(x) 的 x, 如果所有的 x' ≥ x 也满足C(x')的话,我们就可以用二分搜索来求得最小的 x。首先将区间的左端点初始化为不满足C(x)的值,右端点初始化为满足C(x) 的值,然后每次取中点 mid = (lb + ub)/ 2,判断 C(mid)是否满足并缩小范围,直到 ( lb,  ub ] 足够小为止。最后 ub 就是需要求的最小值。

  最大化的问题也可以用同样的方法求解。

2. 假定一个解并判断是否可行

  

  

  这个问题用二分搜索可以非常容易地求得答案。让我们套用二分搜索的模型来解决这个问题。

  令: 条件 C(x) := 可以得到 K 条长度为 x 的绳子

  则问题就变成了求满足 C(x)条件的最大的 x。在区间初始化时,只需要用充分大的数 INF 作为上界即可: lb = 0,  ub = INF.

  现在的问题是是否可以高效地判断 C(x)。由于长度为 Li的绳子最多可切出 floor(Li / x) 段长度为 x 的绳子,因此:

    C(x) = (floor(Li / x)的总和是否 ≥ K)

    复杂度为 O(N)

int N, K;
double L[MAX_N];
//判断是否满足条件 
bool C(double x) {
    int num = 0;
    for (int i = 0; i < N; i++)
        num += (int) (L[i] / x);
    return num >= K;
}

void solve() {
    //初始化解的范围
    double lb = 0, ub = INF;
    for (int i = 0; i < 100; i++) {
        double mid = (lb + ub) / 2;
        if (C(mid))
            lb = mid;
        else
            ub = mid;
    } 
    printf("%.2f\n", floor(ub * 100) / 100);
}

  像这样,如果在求解最大化或最小化问题中,能够比较简单地判断条件是否满足,那么使用二分搜索法就可以很好地解决问题。

二分搜索法的结束判定

  在输出小数的问题中,一般都会指定允许的误差范围或者是指定输出中小数点后面的位数。因此在使用二分搜索法时,有必要设置合理的结束条件来满足精度要求。在上面的程序中,我们指定了循环次数作为终止条件。一次循环可以把区间范围缩小一半,也可以把终止条件设为像 (ub - lb ) > EPS 这样,指定一个区间大小。在这种情况下,如果 EPS 取值太小,就可能会因为浮点数精度的原因导致陷入死循环,要小心。

 3. 最大化最小值

   

  

  类似的最大化最小值或者最小化最大值的问题,通常用二分搜索就可以很好地解决。

  定义: C(d) := 可以安排牛的位置使得最近的两头牛的距离不小于 d

  那么问题就变成了求满足 C(d)的最大的 d。另外,最近的间距不小于 d 也可以说成是所有牛的间距都不小于 d,因此就有

  C(d)= 可以安排牛的位置使得任意的牛的间距都不小于 d

  这个问题的判断可以使用贪心法就可以非常任意地求解:

    对牛舍的位置 x 进行排序;

    把第一头牛放进 x0 的牛舍;

    如果第 i 头牛放入了 xj 的话,第 i + 1 头牛就要放入满足 xj + d ≤ xk 的最小的 xk 中;

  每一次判断对每头牛最多进行一次处理,因此复杂度为 O(N)。

  

int N, M;
int x[MAX_N];

bool C(int d) {
    int last = 0;
    for (int i = 1; i < M; i++) {
        int crt = last + 1;
        while(crt < N && x[crt] - x[last] < d)
            crt++;
        if (crt == N) return false;
        last = crt;
    }
    return true;
} 

void solve() {
    sort(x, x + N);
    int lb = 0, ub = INF;
    while (ub - lb > 1) {
        int mid = (lb + ub) / 2;
        if (C(mid)) lb = mid;
        else ub = mid;
    }
    printf("%d\n", lb);
}

4. 最大化平均值

  

  

  

  一般最先想到的方法是把物品按照单位价值排序,从小到大贪心地进行选取。但是这个方法是不可行的。实际上用二分搜索可以很好地解决。

  定义: 条件 C(x) := 可以选择使得单位重量的价值不小于 x

  因此原问题就变成求满足 C(x)的最大的 x。那么该如何判断 C(x)是否可行?假设我们选了某个物品的集合 S,那么它们的单位重量的价值是

   

  因此就变成了判断是否存在 S 满足下面条件

   变形得

  因此,可以对(vi - x * wi)的值进行排序贪心地进行选取。因此就变成了

    C(x)= ((vi - x * wi)从大到小排列中的前 k 个和不小于 0)

  每次判断的复杂度是O(n log n)。

int n, k;
int w[MAX_N], v[MAX_N];
double y[MAX_N]; // v - x * w

bool C(double x) {
    for (int i = 0; i < n; i++)
        y[i] = v[i] - x * w[i];
    sort(y, y + n);
    double sum = 0;
    for (int i = 0; i < k; i++)
        sum += y[n - i - 1];
    return sum >= 0;
}
void solve() {
    double lb = 0, ub = INF;
    for (int i = 0; i < 100; i++) {
        double mid = (lb + ub) / 2;
        if (C(mid)) lb = mid;
        else ub = mid;
    }
    printf("%.2f\n", ub);
}

 

posted @ 2019-05-10 20:23  莫莫君不恋爱  阅读(425)  评论(0编辑  收藏  举报