《算法竞赛从入门到国奖》算法基础:入门篇-二分算法

💡Yupureki:个人主页

✨个人专栏:《C++》 《算法》


🌸Yupureki🌸的简介:


目录

前言

二分查找

[1. 在排序数组中查找元素的第一个和最后一个位置](#1. 在排序数组中查找元素的第一个和最后一个位置)

算法原理

实操代码

[2. 牛可乐和魔法封印](#2. 牛可乐和魔法封印)

算法原理

实操代码

[3. 烦恼的高考志愿](#3. 烦恼的高考志愿)

算法原理

实操代码

二分答案

[1. 木材加工](#1. 木材加工)

算法原理

实操代码

[2. EKO / 砍树](#2. EKO / 砍树)

算法原理

实操代码

[3. 跳石头](#3. 跳石头)

算法原理

实操代码


前言

当我们的解ret具有二段性时,可以使用二分算法。其中ret是题目的最优解,例如较小值中的最大值,较大值中的最小值。

二分查找

1. 在排序数组中查找元素的第一个和最后一个位置

题目链接:

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

算法原理

题目的关键是非递减顺序

那么在[1,2,4,4,5,6,7,8]我们单独看4,我们会发现4之前的数字全部小于或等于4,之后的数字全部大于或等于4大。那么如何找出第一次出现的4?

我们可以发现,如果看5这个数字,那么5之后的不用看了,因为一定大于4,只用看之前的数字

如果看2这个数字,那么2之前的也不用看了,因为一定小于4,只用看2后面的数字。

因此我们单独看一个某数字,作比较就能忽略之前或之后的某一个区间,因为数组是非递减的,具有单调性。这里我们选择left和right,为数组的下标,初始left为0,right为数组的最大下标

表示在[left,right]区间(注意是闭区间)中找数字target,通过之前的分析,我们选择mid为[left,mid]这个区间的中间,即mid = (left + mid)/2 或者(left + right + 1)/2,(我们后面讲如何选择),target = 4

如果v[mid]>=4,那么mid后面的区间不用看了,肯定全部大于4,所以我们只需要看[left,mid],即令right = mid;如果v[mid] < 4,那么mid前面的区间不用看了,肯定全部小于4,所以我们只需要看[mid + 1,right],即令left = mid + 1。

直到left = right,此时找到的是第一个4

那么代码可以这样写

cpp 复制代码
int l = 1, r = n;
while(l < r)
{
    int mid = (l + r) / 2;
    if(v[mid] >= 4) r = mid;
    else l = mid + 1;
}

当然二分的模板肯定不只这一个

选择哪个模板取决于问题是什么,我们的check如何设计的。例如上个问题是找到第一个4

那么我们让check为v[mid] >= 4,这样我们就会找到一个区间,这个区间全都是大于或等于4的数字,而区间的左端点正好就是第一个4

那么为什么是r = mid 而不是r = mid -1? 因为我们判断的check是大于或等于4,因此如果mid正好落到了第一个4那里,我们让r = mid - 1,就会跳过第一个4

那如果check判断条件不包含=的情况,即v[mid] > 4,那么是不是r = mid - 1?很可惜,这样是错误的,这里不做推导。只需要记住check一定得包含=的情况即可

相反l = mid + 1就是对的而不是l = mid,因为会死循环,这里不做推导,记住即可

这是查找区间左端点的模板,那么何时使用查找区间右端点的模板?

例如我们要找最后一个出现的4的位置

我们同样可以找到一个区间,这个区间全都是小于或等于4的数字,那么区间的右端点就是最后一个4

这里我们的check选择的是v[mid]<=4,而上面的模板是v[mid]>=4,为什么不一样?

因为我们要在小于或等于4的区间内找他的右端点,那肯定是v[mid]<=4

这里mid = (l + r + 1)/2,为什么不是(l + r)/2?因为会死循环,记住即可

然后就跟上面同理l = mid,r = mid - 1

实操代码

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.size() == 0)
            return {-1,-1};
        int l = 0,r = nums.size() - 1;
        while(l < r)
        {
            int mid = (l + r)/2;
            if(nums[mid] >= target)
                r = mid;
            else
                l = mid + 1;
        }
        if(nums[l] != target)
        {
            return {-1,-1};
        }
        int ret = l;
        l = 0;r = nums.size() - 1;
        while(l < r)
        {
            int mid = (l + r + 1)/2;
            if(nums[mid] <= target)
                l = mid;
            else
                r = mid - 1;
        }
        return {ret,l};
    }
};

2. 牛可乐和魔法封印

题目链接:

牛可乐和魔法封印

算法原理

由于是找到大于等于x且小于等于y的数字个数

我们可以看作在数组中找到第一个出现的x的位置,和最后一个出现的y的位置

这里我们引入两个接口,lower_bound和upper_bound

lower_bound表示在一个有序的容器内找到第一个大于或等于val的元素

upper_bound则表示找到第一个大于val的元素

因此我们可以利用lower_bound找到第一个大于或等于x的元素

利用upper_bound找到第一个大于val的元素

由于两个函数返回的都是迭代器,相减就能得到区间的长度

实操代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    int n;cin>>n;
    vector<int> v;
    while(n--)
    {
        int num;cin>>num;
        v.push_back(num);
    }
    int m;cin>>m;
    while(m--)
    {
        int left,right;cin>>left>>right;
        cout<< upper_bound(v.begin(),v.end(),right) - lower_bound(v.begin(),v.end(),left) <<endl;
    }
    return 0;
}

3. 烦恼的高考志愿

题目链接:

P1678 烦恼的高考志愿 - 洛谷

算法原理

本题其实就是在一个数组中找离n最近的一个数字

我们用lower_bound帮我们找大于或等于n的第一个数字m

  1. 如果n == m,那么毋庸置疑,离n最近的数字肯定就是n本身
  2. 如果m > n,那么要看n左右的数字,例如有2 3 5三个数字,5是大于或等于3的第一个数字,但实际上2离3才是最近的

实操代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    int n, m; cin >> n >> m;
    vector<int> v;
    while (n--)
    {
        int num; cin >> num;
        v.push_back(num);
    }
    long long ret = 0;
    sort(v.begin(), v.end());
    while (m--)
    {
        int num; cin >> num;
        auto it = lower_bound(v.begin(), v.end(), num);
        if (it == v.end())
        {
            it--;
            ret += num - *it;
        }
        else if (it != v.begin())
        {
            int n1 = *it - num;
            it -= 1;
            int n2 = num - *it;
            ret += n1 > n2 ? n2 : n1;
        }
        else
            ret += *it - num;
    }
    cout << ret;
    return 0;
}

二分答案

1. 木材加工

题目链接:

P2440 木材加工 - 洛谷

算法原理

我们要找到符合条件的最大长度

什么时候符合条件?能够切割l长度的段数一定要大于或等于k

那么我假设最大长度为lmax,那么我切1cm肯定符合条件,切2cm也符合条件,切lmax/2也符合条件,lmax也符合条件,直到lmax + 1就不够k段了。并且超过了lmax,不论多大,就是lmax * 1000也一定不符合条件

因此我们发现,如果把长度l作为一个数组,那么小于或等于lmax一定符合条件,大于lmax一定不符合

这其实就是个二分,我们找到[1,lmax]的右端点即可,此时一定是能够切割的最大长度

只是我们在答案中二分而已

实操代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

vector<int> v;

long long check(int k)
{
    long long sum = 0;
    for (auto& it : v)
    {
        sum += it / k;
    }
    return sum;
}

int main()
{
    long long n, k; cin >> n >> k;
    int max = 0;
    for (int i = 0; i < n; i++)
    {
        int num; cin >> num;
        v.push_back(num);
        if (num > max)
            max = num;
    }

    if (check(1) < k) {
        cout << 0;
        return 0;
    }
    
    int left = 1, right = max;
    while (left < right)
    {
        int mid = (left + right + 1) / 2;
        if (check(mid) >= k)
            left = mid;
        else
            right = mid - 1;
    }
    cout << left;
    return 0;
}

2. EKO / 砍树

题目链接:

P1873 [COCI 2011/2012 #5] EKO / 砍树 - 洛谷

算法原理

这也是答案中的二分

假设我们无脑砍树,从高度为0开始砍,那肯定能够。如果从1开始,也够。直到h就不够了,并且h往上也肯定不够

实操代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

vector<int> v;

long long func(int n)
{
    long long sum = 0;
    for(auto & it : v)
    {
        int num = it - n;
        if(num > 0)
            sum += num;
    }
    return sum;
}

int main()
{
    int n,k;cin>>n>>k;
    int max = 0;
    for(int i = 0;i<n;i++)
    {
        int num;cin>>num;
        v.push_back(num);
        if(num > max)
            max = num;
    }
    int left = 0,right = max;
    while(left < right)
    {
        int mid = (left + right + 1)/2;
        if(func(mid) >= k)
            left = mid;
        else
            right = mid-1;
    }
    cout<<left;
    return 0;
}

3. 跳石头

题目链接:

P2678 [NOIP 2015 提高组] 跳石头 - 洛谷

算法原理

我们先不找最大的最短跳远距离,我们先看哪些最短跳远距离符合条件

即移动的石头小于最大的移动次数

假设最短跳远距离很短,那就不会移动石头;如果很大,那么可能要移动很多石头,甚至全移走了都不够。因此最短跳远距离只会在左区间内产生,即保证移动的石头数小于最大次数。然后右端点就是最大的最短跳远距离

实操代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

vector<long long> v;
int L, N, M;

bool check(int d)
{
    int count = 0;
    int pos = 0;//上一个石头
    for (int i = 1; i < v.size() - 1; i++)
    {
        if (v[i] - v[pos] < d)//距离小于最短跳远距离,则移动i号石头
            count++;
        else
            pos = i;//大于最短跳远距离,则更新上一个跳远的石头
    }
    if (v.back() - v[pos] < d)
        count++;
    return count <= M;//判断移走石头的数量是否符合条件
}

int main()
{
    cin >> L >> N >> M;
    v.push_back(0);
    for (int i = 0; i < N; i++) {
        int num;
        cin >> num;
        v.push_back(num);
    }
    v.push_back(L);

    sort(v.begin(), v.end());

    int left = 1, right = L;
    while (left < right)
    {
        int mid = left + (right - left + 1) / 2;//查找右端点
        if (check(mid))
            left = mid;
        else
            right = mid - 1;
    }
    cout << left;
    return 0;
}
相关推荐
xwill*2 小时前
Python 的类型提示(type hint)
开发语言·pytorch·python
汉堡go2 小时前
python_chapter3
开发语言·python
游戏23人生2 小时前
c++ 语言教程——16面向对象设计模式(五)
开发语言·c++·设计模式
Alsn862 小时前
30.登录用户名密码 RSA 加密传输-后端为java
java·开发语言
qq_463408422 小时前
React Native跨平台技术在开源鸿蒙中使用WebView来加载鸿蒙应用的网页版或通过一个WebView桥接本地代码与鸿蒙应用
javascript·算法·react native·react.js·开源·list·harmonyos
老王熬夜敲代码2 小时前
C++的decltype
开发语言·c++·笔记
Jul1en_2 小时前
【算法】位运算
算法
lxp1997412 小时前
PHP框架自带队列--更新中
开发语言·php
MoonBit月兔2 小时前
海外开发者实践分享:用 MoonBit 开发 SQLC 插件(其三)
java·开发语言·数据库·redis·rust·编程·moonbit