算法学习笔记 - 二分

前言 

据说,只有10%的程序员能写对二分

首先,二分的基础用法是在单调序列或单调函数中进行查找。因此当问题的答案具有单调性时,就可以通过二分把求解转化为判定(根据复杂度理论,判定的难度小于求解),这使得二分的运用范围变得很广泛。进一步地,我们还可以扩展到通过三分法去解决单峰函数的极值以及相关问题。

二分实现的方式多种多样,但其中的细节一定要仔细考虑。

对于整数域上的二分,需要注意终止边界,左右区间取舍时的开闭情况,避免漏掉答案或造成死循环;对于实数域上的二分,需要注意精度问题。

整数集合上的二分

大部分二分的写法保证最终答案处于闭区间 [L,R] 以内,循环以 L = R 结束,每次二分的中间值mid会归属于左半段与右半段二者之一。

在单调递增序列 a 中查找 ≥x 的数中最少的一个(即 x 或 x 的后继)

while(l < r){
    int mid = (l + r) >> 1;
    if(a[mid] >= x) r = mid; 
    else l = mid + 1;
}
return a[l];

缩小范围时,r = mid,l = mid + 1,取中间值时,mid = (l + r)>> 1

在单调递增序列 a 中查找 ≤x 的数中最大的一个(即 x 或 x 的前驱)

while(l < r){
    int mid = (l + r + 1) >> 1;
    if(a[mid] <= x) l = mid;
    else r = mid -1;
}
return a[l];

缩小范围时,l = mid,r = mid - 1,取中间值时,mid = (l + r + 1) >> 1

一定要对mid的取法加以区分,否则可能会造成死循环

我们还发现:mid = (l + r)>> 1 不会取到 r 这个值,mid = (l + r + 1)>> 1 不会取到l这个值

由此处理无解的情况,把最初的二分区间 [1 ,n] 分别扩大为 [1 ,n+1] 和 [0 ,n],把 a 数组的一个越界的下标包含进来。如果最后二分终止于扩大后的这个越界下标上,则说明 a 中不存在所求

总结:

1. 通过分析具体问题,确定左右半段哪一个是可行区间,以及 mid 归属哪一半段

2. 根据分析结果,选择上述一种二分方法

3.二分终止条件是 l == r,该值就是答案所在位置

C++ STL 中的 lower_bound 与 upper_bound 函数实现了在一个序列中二分查找某个整数 x 的后继

实数域上的二分

在实数域上二分较为简单,确定好所需的精度 eps,以 l + eps < r 为循环条件,每次根据在 mid 上的判定选择 r = mid 或 l = mid 分支之一即可。一般需要保留 k 位小数时,则取 eps = \small 10^{-(k+2)}

while (l + 1e5 - 5 < r){
    double mid = (l + r) / 2;
    if(calc(mid)) r = mid;
    else l = mid;
}

有时精度不容易确定或表示,就干脆采用循环固定次数的二分方法,也是一种相当不错的策略。这种方法得到的结果的精度通常比设置 eps 更高

for(int i = 0; i < 100; i++){
    double mid = ( l + r ) / 2;
    if (calc(mid)) r = mid;
    else l = mid;
}

三分求单峰函数极值

有一类函数被称为单峰函数,它们拥有唯一的极大值点,在极大值点左侧严格单调上升,右侧严格单调下降;或者拥有唯一的极小值点,在极小值点左侧严格单调下降,在极小值点右侧严格单调上升。为了避免混淆,我们也称后一种为单谷函数,。对于单峰或单谷函数,我们可以通过三分法求其极值。

以单峰函数 f 为例,我们在函数定义域 [l ,r] 上任取两个点 lmid 与 rmid ,把函数分为三段

1. 若 f(lmid) < f(rmid),则 lmid 与 rmid 要么同时处于极大值点左侧(单调上升函数段),要么处于极大值点两侧。无论哪种情况下,极大值点都在 lmid 右侧,可令 l = lmid

2. 同理,若 f(lmid) > f(rmid) ,则极大值点一定在 rmid 左侧,可令 r = rmid

    如果我们取 lmid 与 rmid 为三等分点,那么定义域范围每次缩小 1/3 。如果我们取 lmid 与 rmid在二等分点两侧极其接近的地方,那么定义域范围每次近似缩小 1/2。通过 log 级别的时间复杂度即可在指定精度下求出极值。这就是三分法。

    注意,我们在介绍单峰函数时特别强调了“严格”单调性。若在三分过程中遇到 f(lmid) = f(rmid),当函数严格单调时,令 l = mid 或 r = rmid 均可。如果函数不严格单调,即在函数中存在一段值相等的部分,那么我们无法判断定义域的左右边界如果缩小,三分法就不再适用。

二分答案转化为判定

一个宏观的最优化问题也可以抽象为函数,其“定义域”是该问题下的可行方案,对这些可行方案进行评估得到的数值构成函数的“值域”,最优解就是评估值最优的方案。借助二分,我们把求最优解的问题,转化为给定一个值mid,判定是否存在一个可行方案评分达到 mid 的问题

三分求单峰函数极值

题目链接

有一类函数被称为单峰函数,它们拥有唯一的极大值点,在极大值点左侧严格单调上升右侧严格单调下降:或者拥有唯一的极小值点,在极小值点左侧严格单调下降,在极小值点右侧严格单调上升。为了避免混淆,我们也称后一种为单谷函数。对于单峰或单谷函数,我们可以通过三分法求其极值。

以单峰函数 f 为例,我们在函数定义域 [l, r] 上任取两个点 lmid 与 rmid,把函数分为三段。

1. 若 f(lmid) < f(rmid),则 lmid 与 rmid 要么同时处于极大值点左侧(单调上升函数段),要么处于极大值点两侧。无论哪种情况下,极大值点都在 lmid 右侧,可令 l = lmid。

2. 同理,若 f(lmid) < f(rmid),则极大值点一定在 rmid 左侧,可令 r = rmid。

如果我们取 lmid 与 rmid 为三等分点,那么定义域范围每次缩小 1/3。如果我们取 lmid 与 rmid 在二等分点两侧极其接近的地方,那么定义域范围每次近似缩小 1/2.通过 log 级别的时间复杂度即可在指定精度下求出极值。这就是三分法。

注意,我们在介绍单峰函数的时特别强调了“严格”单调性。若在三分过程中遇到 f(lmid) = f(rmid),当函数严格单调时,令 l = lmid 或 r = rmid 均可。如果函数不严格单调,即在函数中存在一段值相等的部分,那么我们无法判断定义域的左右边界如何缩小,三分法就不再使用。

#include<iostream>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;

int n;
double a[20];
double l, r;

double get_y(double x){
    double sum = 0;
    for(int i = 1; i <= n + 1; i++){
        sum += a[i] * pow(x, n - i + 1);
    }
    return sum;
}

int main(){
    cin >> n >> l >> r;
    for(int i = 1; i <= n + 1; i++) cin >> a[i];
    while(r - l >= 0.0000001){
        double mid = (r - l);
        double lmid = l + mid / 3;
        double rmid = r - mid / 3;
        if(get_y(lmid) <= get_y(rmid)) l = lmid;
        else r = rmid;
    }
    cout << l << endl;
    system("pause");
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值