目录
定义
二分查找本质就是在有序的序列中,找目标值,通过不断地找中间值,再通过比较目标值和中间值的大小关系,不断缩小查找范围,即每次都减少二分之一的搜索范围,从而找到目标值的算法
算法模版
模版有3种类型,包括左开右开,左闭右闭,左闭右开
只需要掌握一种即可解决所有二分问题,这里只介绍左开右开
我把左开右开模版,又进行了一些细分,上模板
无chesk函数版本
一般情况a: 寻找<=q的最后一个数
int find(int q){}
int l = 0, r = n+1; // 假设我们要找的数的范围为[1,n],不一定是0和n+1,只要满足在[1,n]之外即可
while(l+1<r){
// 计算搜索范围的中间值mid
int mid = l + (r-l>>1); //+-优先级高于>>所以加括号。不写l+r>>1是为了防止数据溢出
// 比较搜索值和目标值的大小关系
// 若目标值在搜索值右边或相等
if (mid <= q) l = mid; // 抬左脚 踩到mid那里
else r = mid; // 否则抬右脚 踩到mid
}
// 由if (mid <= q)决定,mid与q相等时,抬哪只脚就返回哪只脚
return l; // mid=q时抬左脚 故返回左脚
}
解释一下为什么叫做左开右开:
因为l = 0,r = n+1中实际表达的意思是在(0,n+1)的范围内找目标值
一般情况b: 寻找>=q的第一个数
int find(int q){}
int l = 0, r = n+1;
while(l+1<r){
int mid = l + (r-l>>1);
// 若目标值在搜索值左边或相等
if (mid >= q) r = mid; // 抬右脚 踩到mid那里
else r = mid; // 否则抬左脚 踩到mid
}
return r; // mid=q时抬右脚 故返回右脚
}
PS1:
这里有很多种情况
比如找>=5的第一个数
数组有可能是
有5:
有重复数:1 2 3 4 5 5 5
无重复数:1 2 3 4 5
无5:
有重复数:1 2 3 4 4 4 6
无重复数:1 2 3 4 6
我试验过了,无论任何情况,模版依旧适用,直接用模版即可(比如这里,直接套模板a)。
PS2:
而如果题目要求找5,而不是>=5的第一个数,也不是<=5的最后一个数呢?这时候用a/b模版都可。
因为对于 1 2 3 4 5 6 7 来说,找5,既相当于找>=5的第一个数,也相当于找<=5的最后一个数
PS3:
当目标值不一定找得到,找不到时要求返回-1时
需要修改:在上述模版的基础上,引入res变量 (用res捕捉目标值,并最终返回res变量)
eg:
现在需要找数4
若有重复数,且要求有重复数时,只输出最后的数
这时相当于找<=4的最后一个数
直接在一般情况a模版基础修改
一般情况a模版:
原来版本:
int find(int q){}
int l = 0, r = n+1;
while(l+1<r){
int mid = l + (r-l>>1);
if (mid <= q) l = mid;
else r = mid;
}
return r;
}
改完版本:
int find(int q){
int l = 0, r = n+1,res = -1;
while(l+1<r){
int mid = l + (r-l>>1);
if (mid > q) r = mid;
else if(mid == q) {
res = mid;
l = mid;
}
else l = mid;
}
return res;
}
若无重复数
找单个数时,根据PS2,套用情况a/b模版都可以,在情况a/b模版基础改完之后:
int find(int q){
int l = 0, r = n+1,res = -1;
while(l+1<r){
int mid = l + (r-l>>1);
if (mid > q) r = mid;
else if(mid == q)
return mid; // 找到目标值直接return出去
else l = mid;
}
return -1; // 找不到就返回-1
}
PS4:
若查找的序列是浮点数,在情况a,b模版基础下,把while(l+1<r) 改成while(l+1e-7<r),int改成double,其他一样
有chesk版本
bool chesk(int mid){
xxx;
}
int find(int q){}
int l = 0, r = n+1;
while(l+1<r){
int mid = l + (r-l>>1);
// 若搜索值mid符合题意条件
if (chesk(mid)) r = mid; // 根据题意决定抬哪只脚,假设是抬右脚,踩到mid那里
else r = mid; // 否则抬左脚 踩到mid
}
// chesk(mid)==true时,抬哪只脚,那只脚就踩在目标值上,就返回哪只脚
return r;
}
算法在解题上的应用流程
- 看到:有序的一排数字中,找到目标数字
- 想到:二分查找算法
- 问自己几个问题:
- 1,数字是整数还是浮点数?
- 2,需不需要chesk?
- 3,若需要,套入有chesk的模版;若不需要,是找>=q的最后一个数,还是找<=q的最后一个数,还是直接找q? 套入对应的情况a/b模版
例题
无chesk的题
- 1、纯二分(有重复的数) P2249 【深基13.例1】查找 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
const int N = 1e6+5;
int n,m,a[N];
int find(int x){
int l = 0,r = n+1;
int res = -1;
while(l + 1 < r){
int mid = l + (r-l>>1);
if (x>a[mid]) l = mid;
else if (x == a[mid]){
res = mid;
r = mid;
}
else r = mid;
}
return res;
}
int main(){
IOS;
scanf("%d%d",&n,&m);
for (int i = 1;i <= n;i++) scanf("%d",&a[i]);
while(m--){
int x;
scanf("%d",&x);
printf("%d ",find(x));
}
return 0;
}
- 2、纯二分(没有重复的数) 704. 二分查找 - 力扣(LeetCode)
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = -1,right = nums.size();
while(left +1 < right){
int mid = left + (right-left>>1);
if(nums[mid]<target)
left = mid;
else if (nums[mid]==target)
return mid;
else right = mid;
}
return -1;
}
};
3、前缀和 + 二分
P1314 [NOIP 2011 提高组] 聪明的质监员 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
#include <cstring>
#include <iomanip>
#include <climits>
using namespace std;
const int N = 2e5 + 1;
typedef long long LL;
LL s, y; // 矿石的个数,区间的个数,标准值,检验结果
int n,m,w[N], v[N]; // 每个矿石的重量和价值
LL sw[N], sv[N];
int l[N], r[N]; // 每次输入的左右区间
int L=0, R=N, mid; // 二分的三要素
LL t = LLONG_MAX;
// 计算前缀和的函数
void calculate_prefix_sum(int mid) {
for (int i = 1; i <= n; i++) {
if (w[i] >= mid) {
sw[i] = sw[i - 1] + 1;
sv[i] = sv[i - 1] + v[i];
} else {
sv[i] = sv[i - 1];
sw[i] = sw[i - 1];
}
}
}
// 计算 y 值的函数
LL calculate_y() {
LL y = 0;
for (int i = 1; i <= m; i++) {
y += (sw[r[i]] - sw[l[i] - 1]) * (sv[r[i]] - sv[l[i] - 1]);
}
return y;
}
// 二分查找的函数
void binary_search() {
while (L+1<R) {
memset(sw, 0, sizeof(sw));
memset(sv, 0, sizeof(sv));
mid = L + ((R-L)>>1);
calculate_prefix_sum(mid);
y = calculate_y();
t = min(abs(y-s),t);
if(!t) break;
if (y <= s) {
R = mid;
} else {
L = mid;
}
}
}
int main() {
cin >> n >> m >> s;
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
for (int i = 1; i <= m; i++) {
cin >> l[i] >> r[i]; // 存放每次输入的区间
}
binary_search();
cout << t;
return 0;
}
4、贪心+二分
题目:
解题关键-贪心算法思想:
该问题可被分解成很多个小问题,每个子问题都去求最优解,最终能得到问题的最优解 套路:贪心算法
全局最优解是指什么?找到最长子序列长度。 局部最优解是指什么?对于每个num中的数,每次都接在尽可能小的数后面,同时更新最长子序列的长度。
然后由贪心算法思想到以下思路:
二分过程可视化:
代码:
#include <iostream>
using namespace std;
const int N = 100010;
int num[N],lenNum[N],n;
int main()
{
scanf("%d",&n);
for (int i = 1;i <= n;i++) scanf("%d",&num[i]);
int len = 0;
for (int i = 1;i <= n;i++){
int l = 0,r = len + 1; // 开区间
// 对于每个num[i],在0到最长的len值里面,寻找最大的小于num[i]的lenNum[下标]其对应的下标(A)
while(l + 1 < r){
int mid = l + (r-l>>1);
if(lenNum[mid] < num[i]) l = mid; // 想象二分查找的过程图,最终满足条件A的一定是l
else r = mid;
}
len = max(len,l+1); // 把a[i]接在lenNum[l]这个数后面,故更新最大序列长度为max(len,l+1)
lenNum[l+1] = num[i]; // 同时,更新长度为l+1的序列集合中最小的最后数为num[i]这个数
}
printf("%d",len);
return 0;
}
有chesk的题
1、纯二分
P1873 [COCI 2011/2012 #5] EKO / 砍树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
typedef long long ll;
const int N = 1e6+5;
int n,a[N];
ll m;
bool check(ll x){
ll sum = 0;
for (int i = 0;i < n;i++)
if(a[i]>x) sum += (ll)a[i]-x;
return sum >= m;
}
int find(){
int l = -1,r = 4e5+5;
while(l+1<r){
ll mid = l + (r-l)/2;
if(check(mid)) l = mid;
else r = mid;
}
return l;
}
int main(){
scanf("%d%lld",&n,&m);
for (int i = 0;i < n;i++)
scanf("%d",&a[i]);
cout << find();
return 0;
}
2、纯二分
P8647 [蓝桥杯 2017 省 AB] 分巧克力 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
#include <cstdio>
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
const int N = 1e5+5;
int n,k,h[N],w[N];
bool chesk(int m){
long long res = 0;
for (int i = 0;i < n;i++)
res += (h[i]/m) * (w[i]/m);
return res >= k;
}
int find(){
int l = -1,r = 1e5+1;
while(l+1<r){
int mid = l + (r-l>>1);
if(chesk(mid)) l = mid;
else r = mid;
}
return l;
}
int main(){
IOS;
scanf("%d%d",&n,&k);
for (int i = 0;i < n;i++) scanf("%d%d",&h[i],&w[i]);
printf("%d",find());
return 0;
}
3、bfs + 二分法(bfs函数作为二分法的chesk)
P1902 刺杀大使 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
const int N = 1010;
const int INF = 0x3f3f3f3f;
int n,m,p[N][N],vis[N][N];
int l = INF,r = -INF;
int dx[5] = {0,0,0,1,-1},dy[5] = {0,1,-1,0,0};
bool bfs(int x,int y,int maxp){
memset(vis,0,sizeof vis);
queue<pair<int,int>> q;
vis[x][y] = 1;
q.push({x,y});
while(q.size()){
int xx = q.front().first;
int yy = q.front().second;
q.pop();
for(int i = 1;i <= 4;i++){
int nx = xx + dx[i];
int ny = yy + dy[i];
if (nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[nx][ny]&&p[nx][ny]<=maxp){
vis[nx][ny] = 1;
if(nx == n) return 1;
else q.push({nx,ny});
}
}
}
return 0;
}
int find(){
int res;
while(l+1<r){
int mid = l + ((r-l)>>1);
if(bfs(1,1,mid)) {
res = mid;
r = mid;
}
else l = mid;
}
return res;
}
int main(){
cin >> n >> m;
for (int i = 1;i <= n;i++) {
for (int j = 1;j <= m;j++){
cin >> p[i][j];
l = min(l,p[i][j]);
r = max(r,p[i][j]);
}
}
l -=1,r += 1;
cout << find();
return 0;
}
4,纯二分
如有帮助,可以点个赞,给我点正反馈
如有错误或疑问,欢迎指出,看到就会回复的