二分法(算法竞赛进阶指南笔记)

二分(算法竞赛进阶指南笔记)

作用:

①二分的基础用法是在单调序列或单调函数中进行查找.

②当问题的答案具有单调性时,就可以通过二分把求解转化为判定(根据复杂度理论,判定的难度小于求解)。

③还可以扩展到通过三分法去解决单峰函数的极值以及相关问题。

一、整数二分

1.模板:

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

(1)在单调序列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];

(2)在单调序列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];
2.理解:

​ 在第一段代码中,若a[mid]>=x,则根据序列a的单调性,mid之后的数会更大,所以>=x的最小的数不可能在mid之后,可行区间应该缩小为左半段。因为mid也可能是答案,故此时应该取r=mid。同理,若a[mid]<x,取l=mid+1。

​ 在第二段代码中,若a[mid]<=x,则根据序列a的单调性,mid之前的数会更小,所以<=x的最大的数不可能在mid之前,可行区间应该缩小为右半段。因为mid也可能是答案,故此时应该取l=mid。同理,若a[mid]>x,取r=mid-1。

3.注意:

①首先聊聊两者mid取法的区别:

第一种要求>=x的数最小的,所以mid要尽可能小,不+1,第二种要求<=x的数里面最大的,要从大了往小了搜,+1。

如上两种代码,这两种二分写法有两种形式:

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

r和l的分支方法 与 mid的取法是配套的。下面给出简单证明:

如果取l = mid, r = mid - 1时mid = (l + r) >> 1。当r - l = 1时,mid = 2l,所以下一轮收缩范围,如果是l变,就变成l = l死循环,如果是r变,就会发生r < l,与我们While循环以l == r结束不符。

由此同样可证其余搭配,记住:l = mid的时候,mid的取法要+1

②>>和直接 /2 的区别:

右移运算是向下取整,而整数除法是向零取整,在二分值域包含负数时后者不能正常工作。

③仔细分析这两种mid的取法,我们还发现:mid=(l+r)>>1不会取到r这个值,mid=(l+r+1)>>1不会取到l这个值。我们可以利用这一性质来处理无解的情况,把最初的二分区间[1,n]分别扩大为[1,n+1]和[0,n],把a数组的一个越界的下标包含进来。如果最后二分终止于扩大后的这个越界下标上,则说明a中不存在所求的数。

4.使用流程:

(1)分析问题,左右半段哪个是可行区间,mid归属哪半段

(2)根据分析结果,选择两种配套形式之一:

①r=mid,l=mid+1,mid=(l+r)>>1

②l=mid,r=mid-1,mid=(l+r)>>1

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

5.优缺点:

(1)优点:

①始终保持答案位于二分区间内,二分结束条件对应的值恰好在答案所处位置。

②可以自然地处理无解的情况

(2)缺点:

唯一的缺点是由两种形式共同构成,需要认真考虑实际问题选择对应的形式

二、实数域上的二分

在实数域上二分较为简单,有两种方法:

  1. 确定好精度eps,以l + eps < r为循环条件,mid的判定选r = mid 或l = mid之一即可。一般需要保留k位小数时,则取eps = 10^-(k + 2)

    while (l + 1e-5 < r) 
    {
        double mid = (l + r) / 2;
        if (calc(mid)) 
            r = mid; 
        else 
            l = mid;
    }
    
  2. 有时精度不容易确定或表示,就干脆选择循环固定次数的方法。这种方法得到的结果的精度通常比设置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右边,可以取l = lmid
  2. 反之,极大值点一定在rmid左边,可以取r = rmid

四、二分答案转化为判定

书籍分组

有N本书排成一行,已知第i本的厚度为Ai。

把他们分成连续的M组,使T最小化,其中T表示厚度之和最大的一组的厚度。

题目中出现了类似“最大值最小”的含义,这是答案具有单调性,可用二分转化为判定的最常见、最典型的特征之一。

如果我们把“把书划分为M组的方案”作为定义域,“厚度之和最大的一组的厚度”作为值域,需要最小化这个厚度值。

bool valid(int size) {
    int group = 1, rest = size;
    for (int i = 1; i <= n; i++) {
        if (rest >= a[i]) rest -= a[i];
        else group++, rest = size - a[i];
    }
    return group <= m;
}

int main() {
    int l = 0, r = sum_of_ai;
    while (l < r) {
        int mid = (l + r) >> 1;
        if (valid(mid)) r = mid; else l = mid + 1;
    }
    cout << l << endl;
}

Best Cow Fences

给定正整数数列A,求一个平均数最大的、长度不小于L的连续的子段。

二分答案,判定“是否存在一个长度不小于L的子段,平均数不小于二分的值”

如果把数列中每个数都减去二分的值,就转化为判定“是否存在一个长度不小于L的子段,子段和非负”

我们先来看以下两个问题:

  1. 求一个子段,和最大,没有“长度不小于L”这个限制

    O(n)扫描该数列,不断把新的数加入子段,当子段和变成负数,把当前的整个子段清空。扫描过程中出现过的最大子段和即为所求。

  2. 求一个子段,和最大,子段的长度不小于L

    子段和可以转换成前缀和相减的形式,即设sumi表示A1~Ai的和。则有:

    max(i-j>=L){A(j+1) + A(j+2) + ... + A(i)} = max(L<=i<=n){sumi - min(0<=j<=i-L){sumj}}

    仔细观察,我们要算的其实是sumj,然后同时i要遍历循环。i加一后,看起来min{sumj}要套个内部循环重新算,但其实sumj要比较的只多了一个sum(i-L),完全可以用一个min_val存起来。所以代码如下:

    double ans = -1e10;
    double min_val = 1e10;
    for (int i = L; i <= N; i++) {
        min_val = min(min_val, sum[i-L]);
        ans = max(ans, sum[i] - min_val);
    }
    

至此,两个问题都解决了。

我们现在只需要看一下最大子段和是不是非负数,就可以确定二分上下界的变化范围了。

const int maxn = 100001;
double a[maxn], b[maxn], sum[maxn];
int main() {
    int N, L;
    cin >> N >> L;
    for (int i = 1; i <= N; i++) scanf("%lf", &a[i]);
    double eps = 1e-5;
    double l = 1e-6, r = 1e6;
    while (r - l > eps) {
        double mid = (l + r) / 2;
        for (int i = 1; i <= N; i++) b[i] = a[i] - mid;
        for (int i = 1; i <= N; i++) sum[i] = sum[i-1] + b[i];
        double ans = -1e10;
        double min_val = 1e10;
        for (int i = L; i <= N; i++) {
            min_val = min(min_val, sum[i-L]);
            ans = max(ans, sum[i] - min_val);
		}
        if (ans >= 0) l = mid; else r = mid;
    }
    cout << int(r * 1000) << endl;
}
  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值