专题二:二分法
目录
前言
本文主要介绍二分法是什么,二分法的原理,二分法的模板以及使用场景,并且会举一些例题和蓝桥杯真题辅助理解。
是什么
官方版:二分查找是一个时间效率极高的算法,尤其是面对大量的数据时,其查找效率是极高,时间复杂度是log(n)。
主要思想就是不断的对半折叠,每次查找都能除去一半的数据量,直到最后将所有不符合条件的结果都去除,只剩下一个符合条件的结果。
精简版:二分区间快速高效找到结果。
模板
查找大于等于/大于key的第一个元素
这种通常题目描述为满足某种情况的最小的元素。
int left = 1,right = n;
while(left < right)
{
//这里不需要加1。我们考虑如下的情况,最后只剩下A[i],A[i + 1]。
//首先mid = i,如果A[mid] > key,那么right = left = i,跳出循环,如果A[mid] < key,left = right = i + 1跳出循环,所有不会死循环。
int mid = (left + right) / 2;
if(A[mid] > key)//如果要求大于等于可以加上等于,也可以是check(A[mid])
right = mid;
//因为找的是大于key的第一个元素,那么比A[mid]大的元素肯定不是第一个大于key的元素,因为A[mid]已经大于key了,所以把mid+1到后面的排除
else
left = mid + 1;
//如果A[mid]小于key的话,那么A[mid]以及比A[mid]小的数都需要排除,因为他们都小于key。不可能是第一个大于等于key的元素,
}
查找小于等于/小于key的最后一个元素
这种通常题目描述为满足某种情况的最大的元素.
int left = 1, right = n;
while(left < right)
{
//这里mid = (left + right + 1) / 2;
//考虑如下一种情况,最后只剩下A[i],A[i + 1],如果不加1,那么mid = i,如果A[mid] < key,执行更新操作后,left = mid,right = mid + 1,就会是死循环。
//加上1后,mid = i + 1,如果A[mid] < key,那么left = right = mid + 1,跳出循环。如果A[mid] > key,left = mid = i,跳出循环。
int mid = (left + right + 1) / 2;
if(A[mid] < key)
left = mid;//如果A[mid]小于key,说明比A[mid]更小的数肯定不是小于key的最大的元素了,所以要排除mid之前的所有元素
else
right = mid - 1;//如果A[mid]大于key,那么说明A[mid]以及比A[mid]还要大的数都不可能小于key,所以排除A[mid]及其之后的元素。
}
判断二分的特性
- 单调性:数组具有明显的单调性
- 二段性:答案在一段区间内,二分答案
分类
- 二分查找:在一个已知的有序数据集上进行二分地查找
- 二分答案:答案有一个区间,在这个区间中二分,直到找到最优答案
二分思考顺序
首先通过题目背景和check(mid)函数的逻辑,判断答案落在左半区间还是右半区间。
左右半区间的划分方式一共有两种:
中点mid属于左半区间,则左半区间是[l, mid],右半区间是[mid+1, r],更新方
式是r = mid;或者 l = mid + 1;,此时用第一个模板;
中点mid属于右半区间,则左半区间是[l, mid-1],右半区间是[mid, r],更新方
式是r = mid - 1;或者 l = mid;,此时用第二个模板
(第一个找符合要求的最小值,第二个找符合要求的最大值)
力扣704:二分查找
代码
class Solution {
public:
int search(vector<int>& nums, int target) {
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 l;
return -1;
}
};
题解
本题就是对模板的理解以及运用,属于是二分查找类型,因此用模板1或者模板2都是可以的,并不涉及最大值与最小值的问题。
数的范围
代码
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int q[N];
int n,m;
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++) cin>>q[i];
while(m--)
{
int x;
cin>>x;
int l=0,r=n-1;
while(l<r)
{
int mid=l+r >>1;
if(q[mid]>=x) r=mid;
else l=mid+1;
}
if(q[l]!=x) cout<<"-1 -1"<<endl;
else{
cout<<l<<" ";
int l=0,r=n-1;
while(l<r)
{
int mid=l+r+1>>1;
if(q[mid]<=x) l=mid;
else r=mid-1;
}
cout<<l<<endl;
}
}
}
题解
本题中的“起始位置”以及“最终位置”就分别对应最小值和最大值,因此对模板的理解要到位,第一个模板更新r向左边找的是找出最小值,第二个则是最大值,当然有可能是相等的情况以及找不到的情况。
我在哪?
代码
#include<bits/stdc++.h>
using namespace std;
int n;
string str;
unordered_set<string> S;
//找到任何两个长度为k的子串都不互相同
//知识点:判断某个串是否只出现一次可以用哈希表
bool check(int mid)
{
for(int i=0;i+mid-1<n;i++)//区间大小为mid
{
string s=str.substr(i,mid);//取出子串
if(S.count(s)) return false;
S.insert(s);
}
return true;
}
int main()
{
cin>>n;
cin>>str;
int l=1,r=n;//答案的区间
while(l<r)
{
int mid=l+r >> 1;
if(check(mid)) r=mid;
else l=mid+1;
}
cout<<l<<endl;
}
题解
注意到本题属于是二分答案类型,答案是在一个区间里的,再根据题意要求出最小值选用第一个模板即可,
本题需要掌握一个哈希表的知识点,可以用来快速判重,下次出一个专门的stl使用技巧,总结出所有常用的库以及函数。
思维点:找到任何两个长度为k的子串都不互相同
分巧克力
代码
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int w[N], h[N];//存储长、宽
int n,k;
//因为答案在一个区间内:具有二段性,因此可以用二分答案法。
bool check(int a){
int num=0;//切出的数量
for(int i=0;i<n;i++)
{
num+=(w[i]/a)*(h[i]/a);//推出的公式
if(num>=k) return true;
}
return false;
}
int main()
{
cin>>n>>k;
for(int i=0;i<n;i++) cin>>h[i]>>w[i];
int l=1,r=1e5;//答案的区间
while(l<r)
{
int mid=(l+r+1) >>1;
if(check(mid)) l=mid;//因为求最大值
else r=mid-1;
}
cout<<r<<endl;
return 0;
}
题解
本题注意到随着边长的增加,分的块数就会减小,满足单调递减的性质,考虑用二分出最大的边长。
且答案在一个区间内,因此可以用二分答案的方法,再根据题目求最大值来选出用第二个模板。
本题还需要推出一个可以切出巧克力块数的公式。
机器人跳跃问题
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int h[N];
bool check(int e)
{
for (int i = 1; i <= n; i ++ )
{
e + = e - h[i];//每次失去或者得到能量
if (e >= 1e5) return true;//需要注意超过能量最大值的特判
if (e < 0) return false;
}
return true;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
int l = 0, r = 1e5;//
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n", r);
return 0;
}
题解
本题还是有一定难度的,首先你需要想到可以用二分答案法来写,因为答案在一个区间内,并且不管你是得到还是失去能量都可以用一个表达式来表示,只不过可以是正也可以负值罢了,再去判断是否有一步走了会小于0,如果小于0了就不符合。本题还需要注意不能超出能量最大值。
总结
本文主要介绍了二分法,重点需要掌握二分的两个模板和理解方法,灵活运用到题目中还需大家多做题,增加自己的理解力和敏锐度。预祝各位考出好成绩!