NO.62十六届蓝桥杯备战|基础算法-二分查找|查找元素的第一个和最后一个位置|牛可乐和魔法封印|A-B数对|烦恼的高考意愿(C++)

⼆分算法是我觉得在基础算法篇章中最难的算法。⼆分算法的原理以及模板其实是很简单的,主要的难点在于问题中的各种各样的细节问题。因此,⼤多数情况下,只是背会⼆分模板并不能解决题⽬,还要去处理各种乱七⼋糟的边界问题

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

当我们的解具有⼆段性时,就可以使⽤⼆分算法找出答案:

  • 根据待查找区间的中点位置,分析答案会出现在哪⼀侧;
  • 接下来舍弃⼀半的待查找区间,转⽽在有答案的区间内继续使⽤⼆分算法查找结果
查找初始位置
a[mid] >= t  ->  right = mid
a[mid] < t   ->  left = mid + 1
  1. while循环里的判断如何写?
    while (left < right)
    while (left <= right)
    选择第一个,因为第二个有可能会导致死循环
  2. 求中点的方式
    (left + right) / 2
    (left + right + 1) / 2
    选第一个,因为第二个有可能会导致死循环
  3. 二分之后,相遇点的情况
    需要判断一下,循环结束后,是否是想要的结果
查找终止位置
a[mid] <= t  ->  left = mid
a[mid] > t   ->  right = mid - 1
  1. while循环里的判断如何写?
    while (left < right)
    while (left <= right)
    选择第一个,因为第二个有可能会导致死循环
  2. 求中点的方式
    (left + right) / 2
    (left + right + 1) / 2
    选第二个,因为第一个有可能会导致死循环
  3. 二分之后,相遇点的情况
    需要判断一下,循环结束后,是否是想要的结果
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int n = nums.size();
        //处理边界情况
        if (n == 0) return {-1, -1};

        //求起始位置
        int left = 0, right = n - 1;
        while (left < right)
        {
            int mid = (left + right) / 2;
            if (nums[mid] >= target) right = mid;
            else left = mid + 1;
        }
        //left或者right所指的位置有可能是最终结果
        if (nums[left] != target) return {-1, -1};
        int retleft = left; //记录起始位置
  
        //求终止位置
        left = 0, right = n - 1;
        while (left < right)
        {
            int mid = (left + right + 1) / 2;
            if (nums[mid] <= target) left = mid;
            else right = mid - 1;
        }
        return {retleft, left};
    }
};
模板

⼆分的模板在⽹上⾄少能搜出来三个以上。但是,我们仅需掌握⼀个,并且⼀直使⽤下去即可

// ⼆分查找区间左端点  
int l = 1, r = n;  
while(l < r)  
{  
	int mid = (l + r) / 2;  
	if(check(mid)) r = mid;  
	else l = mid + 1;  
}  
// ⼆分结束之后可能需要判断是否存在结果
// ⼆分查找区间右端点  
int l = 1, r = n;  
while(l < r)  
{  
	int mid = (l + r + 1) / 2;  
	if(check(mid)) l = mid;  
	else r = mid - 1;  
}  
// ⼆分结束之后可能需要判断是否存在结果

为了防⽌溢出,求中点时可以下⾯的⽅式:

  • mid = l + (r - l) / 2
时间复杂度

每次⼆分都会去掉⼀半的查找区域,因此时间复杂度为logN

模板记忆⽅式
  1. 不⽤死记硬背,算法原理搞清楚之后,在分析题⽬的时候⾃然⽽然就知道要怎么写⼆分的代码;
  2. 仅需记住⼀点,if/else 中出现-1 的时候,求mid 就+1就够了。
⼆分问题解决流程
  1. 先画图分析,确定使⽤左端点模板还是右端点模板,还是两者配合⼀起使⽤;
  2. ⼆分出结果之后,不要忘记判断结果是否存在,⼆分问题细节众多,⼀定要分析全⾯。
STL中的⼆分查找

<algorithm>

  1. lower_bound :⼤于等于x的最⼩元素,返回的是迭代器;时间复杂度:O(log N) 。
  2. upper_bound :⼤于x的最⼩元素,返回的是迭代器。时间复杂度:O(log N) 。
    ⼆者均采⽤⼆分实现。但是STL中的⼆分查找只能适⽤于"在有序的数组中查找",如果是⼆分答案就不能使⽤。因此还是需要记忆⼆分模板
牛可乐和魔法封印
找大于等于x的起始位置

![[Pasted image 20250403103550.png]]

a[mid] >= x  ->  right = mid
a[mid] < x   ->  left = mid + 1

直接用(left+right)/2
循环结束之后的位置:

if (a[left] < x)  //返回0
找小于等于y的位置

![[Pasted image 20250403105244.png]]

a[mid] <= y  ->  left = mid
a[mid] > y   ->  right = mid - 1

直接用(left+right+1)/2
循环结束之后的位置:

if (a[left] > y)  //返回0
#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;
int n;
int a[N];

int binary_search(int x, int y)
{
    //大于等于x
    int left = 1, right = n;
    while (left < right)
    {
        int mid = (left + right) / 2;
        if (a[mid] >= x) right = mid;
        else left = mid + 1;
    }
    if (a[left] < x) return 0;
    int tmp = left;
    //小于等于y
    left = 1, right = n;
    while (left < right)
    {
        int mid = (left + right + 1) / 2;
        if (a[mid] <= y) left = mid;
        else right = mid - 1;
    }
    if (a[left] > y) return 0;
    return left - tmp + 1;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    
    int Q; cin >> Q;
    while (Q--)
    {
        int x, y; cin >> x >> y;
        cout << binary_search(x, y) << endl;
    }
    
    return 0;
}
P1102 A-B 数对 - 洛谷

由于顺序不影响最终结果,所以可以先把整个数组排序。
由A-B=C得:B=A-C,由于C是已知的数,我们可以从前往后枚举所有的A,然后去前⾯找有多少个符合要求的B,正好可以⽤⼆分快速查找出区间的⻓度。

STL的使⽤
  1. lower_bound :传⼊要查询区间的左右迭代器(注意是左闭右开的区间,如果是数组就是左右指针)以及要查询的值k,然后返回该数组中>=k的第⼀个位置;
  2. upper_bound:传⼊要查询区间的左右迭代器(注意是左闭右开的区间,如果是数组就是左右指针)以及要查询的值k,然后返回该数组中>k的第⼀个位置;
    ⽐如:a = [10, 20, 20, 20, 30, 40],设下标从1 开始计数,在整个数组中查询20 :
  3. lower_bound(a + 1, a + 1 + 6, 20) ,返回a + 2 位置的指针;
  4. upper_bound(a + 1, a + 1 + 6, 20) ,返回a + 5 位置的指针;
  5. 然后两个指针相减,就是包含20 这个数区间的⻓度
  • STL的使⽤范围很「局限」,查询「有序序列」的时候才有⽤,数组⽆序的时候就⽆法使
    ⽤。但是⼆分模板也能在「数组⽆序」的时候使⽤,只要有「⼆段性」即可
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N = 2e5 + 10;

LL n, c;
LL a[N];
unordered_map<int, int> mp;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> c;
    for (int i = 1; i <= n; i++) cin >> a[i];
    sort(a+1, a+1+n);
    
    LL ret = 0;
    for (int i = 2; i <= n; i++)
    {
        LL b = a[i] - c;
        ret += upper_bound(a+1, a+i, b) - lower_bound(a+1, a+i, b);
    }
    cout << ret << endl;
    
    return 0;
}
P1678 烦恼的高考志愿 - 洛谷

先把学校的录取分数「排序」,然后针对每⼀个学⽣的成绩b,在「录取分数」中⼆分出大于等于b的「第⼀个」位置pos,那么差值最⼩的结果要么在pos位置,要么在pos-1位置。
abs(a[pos] - b)abs(a[pos - 1] - b)两者的「最⼩值」即可。
细节问题:

  • 如果所有元素都⼤于b 的时候,pos - 1 会在0下标的位置,有可能结果出错;
  • 如果所有元素都⼩于b的时候,pos会在n的位置,此时结果倒不会出错,但是我们要想到这个细节问题,这道题不出错不代表下⼀道题不出错。
    加上两个左右护法,结果就不会出错了
    ![[Pasted image 20250403145459.png]]
a[mid] >= b  ->  right = mid;
a[mid] < b   ->  left = mid + 1;
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 1e5 + 10;
int n, m;
int a[N];

int find(LL b)
{
    int left = 1, right = m;
    while (left < right)
    {
        int mid = (left + right ) / 2;
        if (a[mid] >= b) right = mid;
        else left = mid + 1;
    }
    return left;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> m >> n;
    for (int i = 1; i <= m; i++) cin >> a[i];
    sort(a + 1, a + 1 + m);

    //加左右护法
    a[0] = -1e7 + 10;

    LL ret = 0;
    for (int i = 1; i <= n; i++)
    {
        LL b; cin >> b;        
        int pos = find(b);
        ret += min(abs(a[pos] - b), abs(a[pos-1] - b));
    }
    cout << ret << endl;
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值