算法设计-二分

一、有序和单调

​ 二分本质上是一种更加智能的搜索状态空间的方式,他需要状态空间的状态呈现一种“有序的一维数组”的形式,然后再进行搜索。所以一开始的排序是无法避免的。

​ 因为二分的写法问题,所以应当怎样排序也是有一定讲究的,所以排序的时候就可以定义一定的比较方式。

​ 如果更加细致的讨论的话,其实有序只是一个“小条件”,比如说很多枚举、搜索类的题目的状态空间也是有序的,但是我们却没有用二分法,这是因为其核心是,适用于二分法的题目,它的状态和解之间的关系是单调的,如下所示

在这里插入图片描述

​ 如左图所示,如果我们对于 mid 有了一个讨论,我们就可以根据需求去选择往左或者往右,右图也是同理,我们知道只要小一点就可以将结果置高,那么最终的结果就会变成“到底小多少”,就是一个很容易解决的问题了。

​ 但是如果状态和结果之间的关系并不单调,那么就无法使用二分法了,如下所示

在这里插入图片描述

​ 左图还可以使用“三分法”,但是对于右侧,完全没有办法使用“分法”了,但是不可否认,右图的状态空间也是有序的,没准可以用动态规划求解。


二、二分模板

​ 二分一共有两个模板,这是因为二分的本质不是通过二分找到“唯一适合“的点,二分一般呈现一种“最优化”的特点,它要找到是“符合条件”的最大的或者最小的。我们下面的讨论,都默认状态空间是增序排列的。

​ 这就导致当 mid 符合条件的时候,我们需要判定该往哪一边走了,通常情况下,我们希望找到约束条件下的最大值,那么就应当向 mid 右侧去寻找,而当我们希望找到约束条件下的最小值,那么就应当向 mid 的左侧去寻找。

​ 在寻找更小的区间的时候,有两个原则需要遵守,一个是缩小后的区间一定是包括 mid 的,是不可以跳过 mid 的,这是因为 mid 可能是唯一的“最优值”,所以是不能跳过的;另一个是一定要在 mid 的基础上进行移动,比如说 mid + 1, mid - 1 这样的移动,这是因为在整数中,如果 right - left = 1 而不进行移动,也就是 left = mid, right = mid ,这就会导致死循环的出现。综上所述,我们一般在 mid 符合约束条件的时候,利用的是 left = mid, right = mid 来确保对于 mid 的保留,而当 mid 不符合条件的时候,进行 left = mid + 1, right = mid - 1 的操作来避免死循环。

​ 同时,需要注意二分法的边界条件,这个其实有多种写法,我选择了 left < right 作为循环的条件,那么退出的时候就有 left == mid == right,可以选择任何一个索引作为最终结果。

​ 最后,还需要注意当搜索不到的情况,最后会返回什么,这取决于区间缩小时的移动条件,如果是 left = mid + 1,那么就会导致在 right 暂停,相反,则在 left 暂停。

​ 所以模板如下,对于选择“最小值”,其中 check(int) 函数用于检测是否满足条件:

// 求符合约束的最小值
while (left < right)
{
    // 向下取整
    int mid = (left + right) >> 1;

    // mid 是满足条件的,那么向左侧搜索,包括 mid
    if (check(mid))
    {
        right = mid;
    }
    // mid 不满足条件,向右侧搜索,那么最可能满足条件的是 mid + 1
    else 
    {
        left = mid + 1;
    }
}

​ 对于选择“最大值”

// 求符合约束的最大值
while (left < right)
{
    // 向上取整
    int mid = (left + right + 1) >> 1;

    // 如果 mid 满足条件,那么向右侧搜索,包括 mid
    if (check(mid))
    {
        left = mid;
    }
    // mid 不满足条件,向左侧搜索,最有可能满足条件的是 mid - 1
    else
    {
        right = mid - 1;
    }
}

​ 对比如下

条目模板 1模板 2
目标求出满足约束的最小值求出满足约束的最大值
mid 取整方式left 取整right 取整
搜索为空的返回值状态空间 right状态空间 left
跳过方向向右 left = mid + 1向左 right = mid - 1

三、STL 算法

​ 与上面提出的模板类似的 stl 算法是 lower_bound() ,它可以返回第一个大于等于某个值的迭代器,也就是这个值的下界位置

// 在 [first, last) 区域内查找不小于 val 的元素
ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last,
                             const T& val);
// 在 [first, last) 区域内查找第一个不符合 comp 规则的元素
ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last,
                             const T& val, Compare comp);

​ 还有一个类似的函数是 upper_bound() ,它可以返回第一个大于某个值的迭代器,也就是这个值的上界位置

// 查找[first, last)区域中第一个大于 val 的元素。
ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last,
                             const T& val);
// 查找[first, last)区域中第一个不符合 comp 规则的元素
ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last,
                             const T& val, Compare comp);

​ 然后还有一个 equal_range() ,返回的是两个迭代器的 pair,指明了范围

// 找到 [first, last) 范围中所有等于 val 的元素
pair<ForwardIterator,ForwardIterator> equal_range (ForwardIterator first, ForwardIterator last, 
													const T& val);
// 找到 [first, last) 范围内所有等于 val 的元素
pair<ForwardIterator,ForwardIterator> equal_range (ForwardIterator first, ForwardIterator last, 
													const T& val, Compare comp);

​ 最后还有一个 bin_search() ,返回一个布尔值来确定在有序状态空间中是否有特定值

//查找 [first, last) 区域内是否包含 val
bool binary_search (ForwardIterator first, ForwardIterator last,
                      const T& val);
//根据 comp 指定的规则,查找 [first, last) 区域内是否包含 val
bool binary_search (ForwardIterator first, ForwardIterator last,
                      const T& val, Compare comp);

​ 示意图如下

在这里插入图片描述

​ 对于 lower_bound() ,其实本质是模板 1 的设计模式,找到的是符合条件的最小值,其代码如下

template <class ForwardIterator, class T> // ForwardIterator 前向迭代器
ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const T &val)
{
    // it 对应 mid
    ForwardIterator it;
    // traits 是萃取机的意思,也就是萃取 iterator 里的信息,并给到算法
    // count 是搜索区间步长,step 是下一步的步长
    iterator_traits<ForwardIterator>::difference_type count, step;
    // count 的初始值就是 first 和 last 的距离(first 对应 left,last 对应 right)
    count = distance(first, last);
    // 步长大于 0,与 left < right 相同
    while (count > 0)
    {
        it = first;
        // 二分
        step = count / 2;
        // 等价于 mid = left + (right - left) / 2
        advance(it, step);
        // 判断 mid 是否满足条件
        // mid 此时不满足条件
        if (*it < val) // 或者 if (comp(*it,val)),对应第 2 种语法格式
        {
            // first = mid + 1
            first = ++it;
            // count 约缩小一半
            count -= step + 1;
        }
        // mid 满足条件,缩小步长(与使用 last 类似)
        else
        {
            count = step;
        }
    }

    return first;
}

​ 因为与模式 1 相似,所以当不存在相似的值的时候,返回的迭代器等于 last

upper_bound() 的本质依然是模板 1,因为它寻找的是满足大于搜索值的最小值,所以代码结构与 lower_bound() 相同。

template <class ForwardIterator, class T>
ForwardIterator upper_bound(ForwardIterator first, ForwardIterator last, const T &val)
{
    ForwardIterator it;
    iterator_traits<ForwardIterator>::difference_type count, step;
    count = std::distance(first, last);
    while (count > 0)
    {
        it = first;
        step = count / 2;
        std::advance(it, step);
        // 与 lower_bound() 只有这里不同,此时表示 mid 不满足大于的条件
        if (!(val < *it)) // 或者 if (!comp(val,*it)), 对应第二种语法格式
        {
            first = ++it;
            count -= step + 1;
        }
        else
        {
            count = step;
        }
    }
    return first;
}

​ 同样的,当不存在相似的值的时候,返回的迭代器等于 last

equal_range() 本质就是调用了 lower_bound()upper_bound() ,如下所示

template <class ForwardIterator, class T>
pair<ForwardIterator, ForwardIterator> equal_range(ForwardIterator first, ForwardIterator last, const T &val)
{
    ForwardIterator it = std::lower_bound(first, last, val);
    return std::make_pair(it, std::upper_bound(it, last, val));
}

binary_search() 调用了 lower_bound() ,将返回值与 last 比较,并且确定搜索值比最小值(first)小(这是搜索不到的情况)

template <class ForwardIterator, class T>
bool binary_search(ForwardIterator first, ForwardIterator last, const T &val)
{
    first = std::lower_bound(first, last, val);
    return (first != last && !(val < *first));
}

四、二分验证法

​ 这是一类固定的题型,一般呈现“最小最大值”这样的特点(并不绝对),其本质是其答案的状态空间呈现很好的线性有序性(说不定就是个 [ 0 , m a x ) [0, max) [0,max) 的区间),我们可以通过二分答案的方法,获得 mid 值,然后利用这个 mid 值去进行模拟验证,如果可以的话,那么就二分继续搜索。

​ 比如说下面的这题

[NOIP2015 提高组] 跳石头

题目描述

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

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

输入格式

第一行包含三个整数 L , N , M L,N,M L,N,M,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。保证 L ≥ 1 L \geq 1 L1 N ≥ M ≥ 0 N \geq M \geq 0 NM0

接下来 N N N 行,每行一个整数,第 i i i 行的整数 D i ( 0 < D i < L ) D_i( 0 < D_i < L) Di(0<Di<L), 表示第 i i i 块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。

输出格式

一个整数,即最短跳跃距离的最大值。

样例 #1

样例输入 #1

25 5 2 
2
11
14
17 
21

样例输出 #1

4

提示

输入输出样例 1 说明

将与起点距离为 2 2 2 14 14 14 的两个岩石移走后,最短的跳跃距离为 4 4 4(从与起点距离 17 17 17 的岩石跳到距离 21 21 21 的岩石,或者从距离 21 21 21 的岩石跳到终点)。

数据规模与约定

对于 20 % 20\% 20%的数据, 0 ≤ M ≤ N ≤ 10 0 \le M \le N \le 10 0MN10
对于 50 % 50\% 50% 的数据, 0 ≤ M ≤ N ≤ 100 0 \le M \le N \le 100 0MN100
对于 100 % 100\% 100%的数据, 0 ≤ M ≤ N ≤ 50000 , 1 ≤ L ≤ 1 0 9 0 \le M \le N \le 50000,1 \le L \le 10^9 0MN50000,1L109

​ 可以看到这个题目的答案空间是在 [ 0 , L ) [0, L) [0,L) 之间的,虽然题目看着很复杂,但是如果考虑用二分的方法去做,对于每一个 mid 值的检验,是很容易进行模拟的,如下所示

#include <bits/stdc++.h>

using namespace std;

int l, m, n;
int d[50005];

// 对于传入的 mid,进行模拟检验
bool check(int step)
{
    int cnt = 0;
    int pre = 0;
    for (int i = 0; i <= n; i++)
    {
        // 如果距离小于 step,说明需要将这个石头移除
        if (d[i] - pre < step)
        {
            cnt++;
        }
        // 否则就更新前一个木桩
        else
        {
            pre = d[i];
        }
    }

    // 最终的结果是移除的石头不能多余 m
    return cnt <= m;
}

int main()
{
    cin >> l >> n >> m;
    for (int i = 0; i < n; i++)
    {
        cin >> d[i];
    }
    d[n] = l;

    int left = 0, right = l + 1;
    // 因为是求解最大值,所以采用的是模板 2
    while (left < right)
    {
        int mid = (left + right + 1) >> 1;

        if (check(mid))
        {
            left = mid;
        }
        else
        {
            right = mid - 1;
        }
    }

    cout << left;
    return 0;
}

​ 需要注意的是,在 check() 中,因为采用的是模拟方法,所以可能会导致复杂度比较高,所以当模拟到失败的时候,需要进行减枝优化。比如说洛谷上的这道题目 https://www.luogu.com.cn/problem/P3853 ,它的格式基本上与前面的题目相同,但是在 check() 上进行减枝

bool check(int step)
{
    int cnt = 0, pre = 0;

    for (int i = 0; i < n;)
    {
        if (d[i] - pre > step)
        {
            cnt++;
            pre += step;
        }
        else
        {
            pre = d[i];
            i++;
        }
        // 减枝
        if (cnt > m)
        {
            return false;
        }
    }

    return true;
}

五、实数二分

​ 实数二分的模板要更加容易记忆,唯一需要注意的就是精度问题,因为控制不好精度,很容易导致死循环(似乎到了 1e-7 左右就容易 tle,这可能是由于双精度浮点数的精度相比较大导致的,所以一般采用 1e-6),一定要注意题目中的描述,模板如下

double left = 0, right = 1e10;
while (left + 1e-6 < right)
{
    double mid = (left + right) / 2;
    if (check(mid))
    {
        left = mid;
    }
    else
    {
        right = mid;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/6644.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

黑马程序员 linux 学习笔记入门部分合集

ubuntu 安装 本课程使用 ubuntu 系统。 ubuntu 官网 - download。 上面会显示有两个版本&#xff0c;每年 ubuntu 发布两个版本&#xff0c;LTS 是长期维护版&#xff0c;所以相对会较稳定。 介绍 Linux 发行版本 不管什么版本&#xff0c;内核都是一样的。 RPM based&a…

“遥感+”蓝碳储量估算、红树林信息提取与论文写作

详情点击链接&#xff1a;“遥感”蓝碳储量估算、红树林信息提取与论文 一&#xff0c;光谱遥感数据及预处理 .1高光谱遥感数据 高光谱分辨率遥感是用很窄而连续的光谱通道对地物持续遥感成像的技术。在可见光到短波红外波段其光谱分辨率高达纳米数量级。高光谱图像数据…

Linux-Vim

一、Vim 配置 ​ vim界面打开以后很丑就不提了&#xff0c;关键有很多基本功能没有办法实现&#xff0c;所以需要自己配置&#xff0c;如果是linux系统&#xff0c;那么应该找到 /usr/share/vim/.vimrc​ 如果是windows装完git以后会自动一个vim&#xff0c;此时应该找到 Gi…

电子招标采购系统—企业战略布局下的采购寻源

​ 智慧寻源 多策略、多场景寻源&#xff0c;多种看板让寻源过程全程可监控&#xff0c;根据不同采购场景&#xff0c;采取不同寻源策略&#xff0c; 实现采购寻源线上化管控&#xff1b;同时支持公域和私域寻源。 询价比价 全程线上询比价&#xff0c;信息公开透明&#xff…

vue + table原生实现表格单元列列宽可重置

const tableMixin {data() {return {dragState: {}, // 记录子表的列宽移动的一些数值dragging: false // 子表是否在重置列宽}},methods: {handleMouseMove(event) {let target event.targetwhile (target && target.tagName ! TH) {target target.parentNode}if (…

算法竞赛ICPC、CCPC、NIO、蓝桥杯、天梯赛

算法竞赛前言一、为什么学习算法竞赛二、学习算法的阶段三、算法竞赛具体学习内容1、基础数据结构1.1、链表1.1.1、动态链表1.1.2、静态链表1.1.3、STL list1.2、队列1.2.1、STL queue1.2.2、手写循环队列1.2.3、双端队列和单调队列1.2.4、优先队列1.3、栈1.3.1、STL stack1.3.…

23 - x的平方根,快速幂,超级次方

文章目录1. x的平方根2. 快速幂3. 超级次方1. x的平方根 二分查找 class Solution { public:int mySqrt(int x) {int left 1, right x;while(left < right){int mid left (right - left) / 2;if(mid > x / mid){right mid - 1;}else if(mid < x / mid){left mi…

OpenShift 4 - Red Hat 是如何对容器镜像的安全风险进行评估分级的

《OpenShift / RHEL / DevSecOps 汇总目录》 文章目录RedHat 对 CVE 的风险级别的评级通用漏洞评分系统 CVSS红帽严重性分级RedHat 对容器镜像的整体风险的分级云原生应用的运行载体是容器镜像&#xff0c;因此容器镜像的安全便是云原生应用安全的关键因素。为此&#xff0c;Re…

联合解决方案|亚信科技AntDB携手蓝凌软件,助推企业数字化办公转型升级

随着企业数字化转型的深入&#xff0c;企业对于协同办公、移动门户、数字运营、智能客服等方面的需求越来越高&#xff0c;数智化正成为催生新动能和新优势的关键力量。数字化的办公平台可以帮助企业实现各类信息、流程的集中化、数字化和智能化管理&#xff0c;为企业管理者提…

老板,你的绩效管理该升级了!

中小企业的绩效考核&#xff0c;一直是一个备受关注的话题。虽然传统的绩效考核理论已经非常成熟&#xff0c;但是在实际应用中&#xff0c;我们往往会遇到各种各样的问题。因此&#xff0c;在选择绩效考核工具和方法时&#xff0c;我们应该注重实用性&#xff0c;不断探索新的…

32位单片机MM32G0140免费申请样品及开发板

灵动微MM32G系列MCU搭载ArmCortex-M0或安谋科技“星辰”STAR-MC1处理器&#xff0c;率先推出的产品支持64KB到128KB Flash存储范围&#xff0c;提供从20脚到64脚封装选项&#xff0c;适用于广泛的智能工业与电机&#xff0c;物联网&#xff0c;智能家居和消费类等应用。其中&am…

比亚迪车载Android开发岗三面经历~

前言 首先&#xff0c;我想说一下我为什么会想去比亚迪这样的车企做车载Android开发。我是一名有5年经验的Android开发工程师&#xff0c;之前一直在互联网软件公司工作&#xff0c;做过移动端App和IoT产品的开发。但我一直对汽车领域很感兴趣&#xff0c;也希望自己的技术能应…

【python+requests】接口自动化测试

这两天一直在找直接用python做接口自动化的方法&#xff0c;在网上也搜了一些博客参考&#xff0c;今天自己动手试了一下。 一、整体结构 上图是项目的目录结构&#xff0c;下面主要介绍下每个目录的作用。 Common:公共方法:主要放置公共的操作的类&#xff0c;比如数据库sql…

前端算法codewhy第一章:队列

目录 认识队列 生活中的队列 开发中队列的应用 队列类的创建 队列的常见操作 击鼓传花 import ArrayQueue from "./01_实现队列结构Queue";function hotPotato(names: string[], num: number): number {if (names.length 0) return -1;// 1.创建队列结构const queue…

数据库安装与使用、mysql、sqlite、mongodb

一、MongoDB MongoDB Server 安装 优秀文章&#xff1a; link1 link2 MongoDB 是一个文档数据库&#xff0c;旨在简化开发和扩展。 下载 官网(社区版) &#xff1a;https://www.mongodb.com/try/download/community 下载完后一路安装即可。 添加环境变量 开启 mongodb服务…

[Linux]环境变量

一.什么是环境变量 为了满足不同的运行场景&#xff0c;操作系统预先设置了一大批全局变量&#xff0c;这种可以指定操作系统运行环境的变量就是环境变量。 我们平常使用的指令本质上也是用C语言实现的一个个小程序&#xff0c;但是我们在执行我们自己的可执行程序时往往是类…

go调用docker远程API(二)-docker API 的容器操作

文章目录1 获取容器列表2 查看指定容器信息3. 查看容器日志4 创建容器4.1 简单使用4.1.1 语法4.1.2 完整示例4.2 端口映射4.2.1 语法4.2.2 完整示例4.3 挂载本机目录/文件4.3.1 语法4.3.2 完整代码5. 启动容器6 停止容器7 删除&#xff08;已停止的&#xff09;容器8 进入容器执…

线程池的7种创建方式

文章目录普通方式创建线程存在的问题什么是线程池线程池的好处线程池设计思路线程池相关类的继承关系线程池的创建方式固定容量线程池——FixedThreadPool相关构造方法示例运行结果缓存线程池——CachedThreadPool相关构造方法示例运行结果单线程线程池——SingleThreadExecuto…

关于国产化系统银河麒麟(Kylin)的问题记录--持续更新

kylin 镜像 &#xff1a; Kylin-Server-10-SP2-x86-Release-Build09-20210524 Kylin-Server-10-SP1-Release-Build20-20210518-x86_64 1.ansible 模块无法使用yum 报错&#xff1a;"msg": "The Python 2 bindings for rpm are needed for this module. If you r…

Dart语言操作符?和!的用法

一.基本使用 1. ? 操作符跟在类型后面&#xff0c;表示当前变量可为null。 int a null; //这句代码在有空安全时&#xff0c;编译会提示错误如果想给一个变量赋值null要如何处理呢&#xff1f;只需要在类型 后面添加操作符&#xff1f;即可&#xff0c;eg: int? a null…