【算法总结】7道例题帮你彻底搞懂二分

目录

定义

算法模版

无chesk函数版本

一般情况a: 寻找<=q的最后一个数 

一般情况b: 寻找>=q的第一个数

有chesk版本

算法在解题上的应用流程

例题

无chesk的题

有chesk的题


定义

二分查找本质就是在有序的序列中,找目标值,通过不断地找中间值,再通过比较目标值和中间值的大小关系,不断缩小查找范围,即每次都减少二分之一的搜索范围,从而找到目标值的算法

算法模版

模版有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的题

#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;
}
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,纯二分

AcWing 4080. 第k个数 - AcWing

如有帮助,可以点个赞,给我点正反馈

如有错误或疑问,欢迎指出,看到就会回复的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值