【算法笔记】二分查找 && 二分答案习题题解

系列文章目录

如果你对二分查找/二分答案有疑问,请看这篇博客

【算法笔记】二分查找 && 二分答案 (超详细解析,一篇让你搞懂二分)

话不多说,直接上题解



P1678 烦恼的高考志愿

在这里插入图片描述

二分查找典题,将志愿中的学校分数排序,然后循环遍历每一位学生的分数,找到第一个>=其分数的学校,然后分别计算该学校和该学校的前一个学校与分数的差,取min即可,注意要特判一下,如果该学生的分数比志愿中分最低的学校还要低,就直接输出差值,否则答案会错
还有,注意,最后的和res会超过int的范围,要开long long

每天一遍:不开long long 见祖宗

ACcode

#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
using LL = long long;  
const int N = 1e5 + 10;  
int m, n;  //m:学校数 n:学生数
int a[N], b[N];  //a[]:学校录取分数线  b[]:学生估分

int main(){
    cin >> m >> n; 
    for (int i = 1; i <= m; i++)
        cin >> a[i];  
    for (int i = 1; i <= n; i++)
        cin >> b[i];  
    
    sort(a + 1, a + m + 1);  // 二分查找之前一定不要忘了排序

    LL res = 0;  //不满意度总和
    for (int i = 1; i <= n; i++){  // 遍历每个学生的估分
        int l = 0, r = m + 1;  // 初始化二分查找的左右边界,l 为 0,r 为 m + 1
        while(l < r){ 
            int mid = l + r >> 1;  
            if(a[mid] <= b[i])  // 如果中间位置的分数线小于等于当前学生的估分
                l = mid + 1;  // 将左边界移动到 mid + 1
            else  // 否则
                r = mid;  // 将右边界移动到 mid
        }
        if(b[i] <= a[1])  // 如果当前学生的估分小于等于最小的分数线
            res += a[1] - b[i];  // 将不满意度累加为最小分数线与估分的差值
        else  
            res += min(abs(a[l - 1] - b[i]), abs(a[l] - b[i]));  // 计算并累加最小的不满意度(此时l==r,两者可以替换)
    }
    cout << res;  
    return 0;  
}

P1163 银行贷款

在这里插入图片描述

题目大意:给出n,m,k,求贷款者向银行支付的利率p,使得

∑ i = 1 k m ( 1 + p ) i = n \begin{equation} \sum_{i=1}^{k} \frac{m}{(1+p)^i} = n \end{equation} i=1k(1+p)im=n
按百分比形输出,精确到小数点后一位
运用浮点二分的模板,二分答案即可

ACcode


#include<iostream>
using namespace std;

int w0, w, m;// 分别表示初始金额,每月支付金额和月数
double sum;// 用于记录每月后的剩余金额

int check(double mid)
{
    sum = w0;// 用初始金额初始化sum
    for (int i = 1; i <= m; i++) // 循环 m 次,模拟 m 个月的还款过程
    {
        sum = sum + sum * mid - w;
    }
    if (sum > 0) // 如果最后金额大于 0,说明利率太高,无法在 m 个月内还清
        return true; 
    return false;
}

int main()
{
    cin >> w0 >> w >> m;

    double l = 0, r = 500; // 答案范围尽量开大些
    while (r - l > 1e-5)   // 精度保证
    {
        double mid = (l + r) / 2;
        if (check(mid))
            r = mid; 
        else
            l = mid;
    }
    printf("%.1f", l * 100);//保留小数用printf
    return 0;
}

P8647 [蓝桥杯 2017 省 AB] 分巧克力

在这里插入图片描述

由于答案具有单调性,因此考虑使用二分答案。
根据题目描可以得到:若当前的边长为 x,呢么对第i块巧克力,可以切出
⌊ h i x ⌋ × ⌊ w i x ⌋ \left\lfloor \frac{h_i}{x} \right\rfloor \times \left\lfloor \frac{w_i}{x} \right\rfloor xhi×xwi
块边长为x的巧克力,因此可以进行二分答案,用总共可以切出的巧克力个数作为check
如果>=k,则可行,否则不可行。

ACcode

#include <iostream>
using namespace std;

const int N = 1e5 + 10; 
int n, k; // n表示巧克力块数,k表示小朋友人数
int h[N]; // 高度
int w[N]; // 宽度

// 检查是否可以切出至少k块边长为x的正方形巧克力
bool check(int x)
{ 
    int sum = 0; // 定义变量sum,统计可以切出的正方形巧克力数
    // 遍历每块巧克力
    for (int i = 1; i <= n; i++)
        // 计算每块巧克力可以切出的边长为x的正方形数目,并累加到sum
        sum += (h[i] / x) * (w[i] / x);
    // 如果总数sum大于或等于k,返回true;否则返回false
    if (sum >= k)
        return true;
    else
        return false;
}

int main()
{
    cin >> n >> k;
    for (int i = 1; i <= n; i++)
        cin >> h[i] >> w[i];
    
    int l = 1, r = N; // 左右边界
    // 二分答案
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid))
            l = mid; 
        else
            r = mid - 1; 
    }
    cout << l;
    return 0;
}

P1024 [NOIP2001 提高组] 一元三次方程求解

很恶心也很无脑的一道题,数据范围甚至可以暴力枚举…当然如果你知道那个什么著名的 卡尔丹公式,你也可以直接套公式。这里给出二分解法–也就是找函数的零点

和一次函数求零点相似,不过三次函数有可能有三个零点,因此我们对[-100, 100]内200个长度为1的区间进行二分答案

ACcode

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

// 系数
double a, b, c, d;

// 计算f(x)函数值 
double f(double x) { 
    return a * x * x * x + b * x * x + c * x + d;
}

int main() {
    cin >> a >> b >> c >> d;
    
    // 从 -100 到 99 遍历每一个整数作为区间的左端点
    for (int i = -100; i <= 99; i++) {
        double l = i, r = i + 1;
        
        // 如果左端点的函数值为零,则输出这个根
        if (f(l) == 0)
            printf("%.2lf ", l);
        
        // 检查区间两端的函数值是否有符号变化
        if (f(l) * f(r) >= 0)
            continue; // 没有符号变化则跳过
        
        // 使用二分法在区间 [l, r] 内找根
        while (r - l >= 0.001) {
            double mid = (l + r) / 2; // 计算中点
            if (f(l) * f(mid) <= 0)
                r = mid; // 根在左半区间
            else
                l = mid; // 根在右半区间
        }
        // 输出找到的根,精确到小数点后两位
        printf("%.2lf ", l);
    }
    return 0;
}

在这里插入图片描述

P2440 木材加工

在这里插入图片描述

二分答案,对于check,我们依次要判断 a 中的每一个数并计算出能切出多少个 mid ,用一个变量 res 储存他们,如果 resk 多或者正好等于,返回true。如果是小于 k ,返回false

ACcode

#include <iostream>  
#include <algorithm> 

using namespace std;
using LL = long long; 

const int N = 1e6 + 10; 

LL n, k; //n:原木数量 k:需要的小段数量
LL a[N]; // 存储每根原木的长度

// 检查能否通过切割长度为 x 的小段得到至少 k 段
bool check(LL x) {
    LL res = 0; // 定义一个变量 res,记录可以切割得到的小段数量
    for (int i = 1; i <= n; i++) { // 遍历每一根原木
        res += a[i] / x; // 将当前原木能够切割出的长度为 x 的小段数量累加到 res
    }
    return res >= k; // 返回是否可以得到至少 k 段长度为 x 的小段
}

int main() {
    cin >> n >> k;
    for (int i = 1; i <= n; i++) 
        cin >> a[i];
    
    LL l = 0, r = 100000001; // 定义二分查找的左右边界,l 为 0,r 为 100000001(比 10^8 大 1)
    while (l < r) { // 当左边界小于右边界时
        LL mid = l + r + 1 >> 1; 
        if (check(mid)) // 如果长度为 mid 的小段可以得到至少 k 段
            l = mid; // 更新左边界为 mid
        else
            r = mid - 1; // 否则更新右边界为 mid - 1
    }
    cout << l << endl; // 输出最大的可以得到的长度 
    return 0;
}

P1577 切绳子

在这里插入图片描述

这题和上一道题思路很像,就是把砍树换成了切绳子,也是二分答案,不同的的是这题数据是浮点数,可以直接用double类型进行二分,但有时会遇到精度的问题,比较麻烦,这里我用另一种方法–将浮点数*100转化成整数,然后对整数进行二分

ACcode

#include <iostream>
#include <algorithm>
using namespace std;
using LL = long long;  
const int N = 10005; 

LL n, k, s[N], sum;  
double a[N], res;   //a:每条绳子的长度   s:每条绳子的长度*100

// 检查给定的长度mid是否能切割出至少k条绳子
bool check(LL mid)
{
    LL ans = 0;  // 初始化计数器ans
    for (int i = 1; i <= n; i++)  // 遍历每条绳子
        ans = ans + s[i] / mid;  // 累加可以切割出的绳子数量
    return ans >= k;  // 返回是否可以切割出至少k条绳子
}

int main()
{
    cin >> n >> k;  
    for (int i = 1; i <= n; i++){
        cin >> a[i];  
        s[i] = a[i] * 100;  // 转换为整数存储,乘以100以避免小数
        sum += s[i];  // 累加总长度
    }

    LL l = 0, r = sum / k + 1;  // 初始化二分查找的左右边界
    while (l < r)
    {
        LL mid = l + r + 1 >> 1;  
        if (check(mid))  //可以切割mid条,将l右移
            l = mid;  
        else
            r = mid - 1;  //否则将r左移
    }

    res = r * 1.00 / 100;  // 别忘了把结果转换回双精度浮点数,不然就WA 
    printf("%.2lf\n", res);  // 输出结果,保留两位小数
    return 0;
}

P2678 [NOIP2015 提高组] 跳石头

在这里插入图片描述

二分答案很经典的一道题,求最大的最小跳跃距离,和上篇文章的砍树类似,我记得今年蓝桥杯国赛B组就出了一道和这道题类似的典题。

我们对于一个长度x,想看看它是否可以符合删除石头数小于等于k,按照贪心的思想:可以这样做:
从位置的小到大扫遍所有石头,用一个变量存储上一个跳到的点。第一个与这上一个点的距离大于等于k的石头即是下一个跳到的点。如果不能跳到下一个石头,则移除当前石头,并增加移除计数。

贪心证明:

  • 假设我们不跳到第一个符合条件的石头,而是跳到更远的某个石头,这意味着我们跳过了一些中间的石头。
  • 跳过这些中间的石头意味着这些石头与前一个石头的距离都小于 D,所以我们必须移除这些石头。
  • 如果我们跳到更远的石头,可能会导致移除的石头数量超过允许的最大移除数M
  • 这样做会导致无法保证最小跳跃距离 D,从而无法得到正确的答案。

ACcode

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 50010; 
int a[N]; // 存储石头位置
int s, n, m, ans; // s:起点到终点的距离,n:石头数量,m:最多可以移除的石头数,ans:答案

// 检查是否可以在移除不超过m块石头的情况下,确保最小跳跃距离至少为k
int check(int k)
{
    int last = 0; // 上一个石头的位置,初始化为起点位置0
    int cnt = 0; // 记录移除的石头数量
    for (int i = 1; i <= n; ++i)
    {
        if (a[i] - last < k) // 如果当前石头和上一个石头之间的距离小于k,则移除当前石头
            cnt++;
        else
            last = a[i]; // 否则更新上一个石头的位置
    }
    return cnt <= m; // 返回是否能够在移除不超过m块石头的情况下确保最小跳跃距离至少为k
}

int main()
{
    cin >> s >> n >> m;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    n++; // 将终点作为最后一个石头
    a[n] = s; // 将终点的位置放入数组中
    int l = 0, r = s; // 二分查找的左右边界
    while (l < r)
    {
        int mid = l + r + 1 >> 1; 
        if (check(mid)) // 检查是否可以在移除不超过m块石头的情况下,确保最小跳跃距离至少为mid
            l = mid; // 如果可以,更新左边界
        else
            r = mid - 1; // 否则,更新右边界
    }
    cout << l; // 输出结果,即最大的最小跳跃距离
    return 0;
}

1460. 我在哪?

在这里插入图片描述

这题难度比较高,但是到很好的题,解决方法很多,可以用字符串哈希加二分求解,如果小白没学过一些算法可以先跳过,等修得正果再回来看

因为解决方式很多,我这里就不一一列了,详情请看 题解传送门

102. 最佳牛围栏

在这里插入图片描述

通过二分查找最大平均牛数,然后用前缀和来判断是否存在一个连续的至少包含F块田地的子数组,其牛的数量的平均值大于或等于当前中间值。

这里还要强调一个概念:子数组
对于萌新,子数组和子序列的概念应该比较模糊,这里做一下区分
首先,看定义
子数组:数组中的一个连续部分,这部分中的元素必须是连续的
子序列:从数组中按顺序提取的元素,这些元素不必须是连续的
其次,举个栗子
对于数组 a[]={1,3,2,10,4,7,11}
它的子数组可以是{1,3,2} {2,10,4,7} {1,3,2,10,4,7} 但不可一是{1,2,10}
它的子序列可以是{1,3,2} {2,10,4,7} 也可以是{1,2,10} {1,4,11},但不可以是{3,1,2} {7,2,1} 序列序列,当然要按顺序
很显然,只要他是子数组,他就一定是子序列;但如果他是子序列,它不一定是子数组------由此可见:子数组是子序列的子集

ACcode

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010; 

int n, F;          // n:田地数量,F:至少围起的田地数量
double a[N], s[N]; // a数组存储每块田地的牛数量,s数组用于存储前缀和减去平均值后的值

// 定义check函数,用于判断是否存在一个长度至少为F的子数组,其平均值大于等于x
bool check(double x)
{
    // 计算前缀和,减去x
    for (int i = 1; i <= n; i++)
        s[i] = s[i - 1] + a[i] - x;

    double mins = 0; // 初始化最小前缀和为0
    // 遍历所有长度至少为F的子数组
    for (int k = F; k <= n; k++)
    {
        mins = min(mins, s[k - F]); // 更新最小前缀和
        // 如果当前前缀和大于等于最小前缀和,说明存在一个子数组平均值大于等于x
        if (s[k] >= mins)
            return true;
    }

    return false; // 否则,不存在这样的子数组
}

int main()
{
    // 读取输入的n和F
    scanf("%d%d", &n, &F);

    double l = 0, r = 0; // 初始化二分查找的左右边界
    for (int i = 1; i <= n; i++)
    {
        // 读取每块田地的牛数量
        scanf("%lf", &a[i]);
        // 更新右边界为最大牛数量
        r = max(r, a[i]);
    }

    // 二分查找
    while (r - l > 1e-5)
    {
        double mid = (l + r) / 2; // 计算中间值
        // 检查是否存在子数组平均值大于等于mid
        if (check(mid))
            l = mid; // 如果存在,更新左边界
        else
            r = mid; // 否则,更新右边界
    }

    // 输出结果,最大平均值乘以1000并向下取整
    printf("%d\n", (int)(r * 1000));

    return 0;
}

其实只要搞清楚那几个细节(见上篇文章),记好板子,做几道题,你就会发现,二分其实真的不难。毕竟这是基础算法 仅靠这几道题还不够,去刷题吧,伟大的OIer 和ACMer!
传送门:洛谷 ACwing 牛客 codeforces

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寂空_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值