二分答案算法超详细教程

20200202这么好的日子, 多么适合学习二分答案算法呀.

老规矩, 先上模板题传送门

首先, 二分答案二分查找其实是不一样的
二分答案: 即对你要求的答案进行二分
二分查找: 对一个已知的有序数据集上进行二分的查找
可能我的归纳不太准确, 但至少可以看出来它们是不一样哒

敲重点, 这里需要特别注意一下二分答案法的使用范围

典型的使用场景: 要求我们求出某种条件的最大值的最小可能情况或者最小值的最大情况
使用前提: 
1. 答案在一个固定的区间内
2. 难以通过搜索来找到符合要求的值, 但给定一个值你可以很快的判断它是不是符合要求
3. 可行解对于区间要符合单调性, 因为有序才能二分嘛
二分答案法将一个复杂的搜索问题归约成了一个判定解是否可行的问题, 很显然, 判定操作比搜索廉价许多, 故在一定的条件下应用二分答案法非常高效.

接下来介绍一个有用的工具, 那就是STL的函数lower_boundupper_bound, 它存在的意义就是可以不用手写二分查找

1.作用
这两个是STL中的函数,作用很相似:

假设我们查找x,那么:

lower_bound会找出序列中第一个大于等于x的数

upper_bound会找出序列中第一个大于x的数

没错这俩就差个等于号╮(╯▽╰)╭

2.用法
以下都以lower_bound做栗子 (因为upper_bound做出的栗子不好吃)

(其实就是我懒得打两遍)

它们俩使用的前提是一样的:序列是有序的

对于一个数组a,在[1,n)的区间内查找大于等于x的数(假设那个数是y),函数就写成:

lower_bound(a + 1, a + 1 + n, x);
函数返回一个指向y的指针

看着是不是很熟悉?回想sort使用的时候:

sort(a, a + 1 + n, cmp);
这里a+1,a+1+n的写法是不是很像?

STL里面的函数写区间一般都这个尿性

同样的,lower_bound和upper_bound也是可以加比较函数cmp的:

lower_bound(a + 1, a + 1 + n, x, cmp);
到这里不得不说说前面的"有序序列",这里的"有序"是对什么有序?

你可能已经猜到了,它是对于比较器有序,并且必须是升序!

关于具体的用法详见关于lower_bound( )和upper_bound( )的常见用法

其实对于一个降序序列我们依然可以使用这两个函数, 只不过我们需要将比较器改成greater <int> ()
lower_bound(a + 1, a + 1 + n, x, greater <int> () );
当然, 我们也可以传入一个cmp函数来自定义我们想要的顺序

步入正题, 如何应用二分答案法解题

试想一下, 如果你不知道二分答案法, 那么你也许会用搜索 ,又或者会一个个的枚举答案, 然后判断正确性, 那么将这里的枚举换成二分, 就是我们的二分答案法啦!

没有题目一切还是显得太苍白

题目背景
一年一度的“跳石头”比赛又要开始了!

题目描述
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。
在起点和终点之间,有 N块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。

为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走M块岩石(不能移走起点和终点的岩石)。

输入格式
第一行包含三个整数 L,N,M,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。
这道题目也是一道典型的二分答案题目, 很显然, 用暴力搜索会超时, 而且我甚至不知道用暴力搜索应该如何做, 有知道的童鞋可以教教我.
分析题目后发现
1.这个最短的跳跃距离是有范围的, 最小不可能会是两块石头之间的最小距离嘛, 最大也大不过起点和终点的距离嘛, 所以满足应用条件1, 在一个区间内
2.对于一个可能的"最大的"最短跳跃距离(注意我这里打了引号, 因为它可能只是一个可行解, 而不是最优解, 原因我会解释的), 我们可以判断移走m块石头能否满足这个要求, 故满足应用条件2, 能很容易的判断一个值是否满足要求
3.由于最短距离肯定是在[1, l]之间的, 这个区间的数当热是单调有序的, 这不就满足应用条件3了吗?

综上所述, 此题可以应用二分答案

具体做法

1. 首先明确一个概念

在一个区间上可能有很多个可行解, 这些解都满足要求, 但不一定是最优的, 我们要考虑所有的可行解, 并从中找到一个最优解作为我们的答案

2. 对于这一题, 若x为可行解, 那么[1, x) 上的数一定可行, 但一定不是最优, 故最优解一定在[x, L] (注:L为总长度)这个区间上. 如果x不可行, 那么[x, L]上的解一定都不可行
3. 有了前面的知识储备, 接下来就好办了, 我们只用二分最短的跳跃距离即可, 把这个距离"认为"是最短的跳跃距离, 以这个标准去去掉石头, 先不必考虑移走石头数的限制, 待全部拿完后, 再与最大移动数目M进行比较, 若超过了, 那么就说明在最多移走M块石头的限制条件下不能达到这个最短跳跃距离, 这就是一个不可行解, 反之就是一个可行解, 这下懂了吧! _
4. 模拟

可以去模拟这个跳石头的过程。开始你在i(i=0)位置,我在跳下一步的时候去判断我这个当前跳跃的距离,如果这个跳跃距离比二分出来的mid小,那这就是一个不合法的石头,应该移走。

为什么?我们二分的是最短跳跃距离,已经是最短了,如果跳跃距离比最短更短岂不是显然不合法,是这样的吧。移走之后要怎么做?先把计数器加上1,再考虑向前跳啊。去看移走之后的下一块石头,再次判断跳过去的距离,如果这次的跳跃距离比最短的长,那么这样跳是完全可以的,我们就跳过去,继续判断,如果跳过去的距离不合法就再拿走,这样不断进行这个操作,直到i = n+1,为啥是n+1?河中间有n块石头,显然终点在n+1处。(这里千万要注意不要把n认为是终点,实际上从n还要跳一步才能到终点)。

模拟完这个过程,我们查看计数器的值,这个值代表的含义是我们以mid作为答案需要移走的石头数量,然后判断这个数量 是不是超了就行。如果超了就返回false,不超就返回true。

接下来上代码
核心函数judge
bool judge(int x) {
    int tot = 0; //需要移动的总数
    int i = 0; 
    int now = 0;//当前所在的石头

    while(i < n + 1) {
        i++;
        //如果当前石头与下一块石头之间的距离比我们设定的最短的距离要小, 那么这块石头就得移走.
        //tot++代表需要移动的加一, i++代表接下来判断移动的石头后面的一块与当前`now`石头的距离
        if (a[i] - a[now] < x) {
            //那么就将这块石头拿走
            tot++;
        } else {
            //如果满足的话
            now = i;
        }
    }
    //如果需要移动比最大可移动数目还要多的石头, 那么return false
    if (tot > m) {
        return false;
    } else {
        //否则
        return true;
    }
}

下面是完整AC代码

#include <bits/stdc++.h>
#define maxn 500010
using namespace std;

int l, r, n, m, ans, mid, d;
int a[maxn];

bool judge(int x) {
    int tot = 0;
    int i = 0;
    int now = 0;

    while(i < n + 1) {
        i++;
        if (a[i] - a[now] < x) {
            //那么就将这块石头拿走
            tot++;
        } else {
            now = i;
        }
    }
    if (tot > m) {
        return false;
    } else {
        return true;
    }
}

int main() {
    cin >> d >> n >> m;

    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    a[n + 1] = d;

    l = 1, r = d;
    while (l <= r) {
        //二分枚举
        mid = (l + r) / 2;
        if (judge(mid)) {
            //mid是可行解的情况
            ans = mid;
            l = mid + 1;
        }
        //mid不是可行解的情况
        else r = mid - 1;
    }
    cout << ans << endl;
    return 0;
}

20200202, 今天我终于有一篇博客 最小生成树算法超详细教程的阅读量超过两位数啦, 必须好好纪念这个牛逼的日子, 以后我会更认真的写博客哒!既能记录自己学习的过程, 又能输出知识, 帮助他人, 何乐而不为呢?

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值