二分算法 && 二分的特殊情况 && 小数的二分 && 二分答案

二分算法

一、 sort()排序函数

1、要点:

  • 头文件

    #include <algorithm> // algorithm是算法的意思
    
  • sort(num + first, num + last) // last是数组的长度 +first,比如sort(num, num + n)表示从num[0] 排序到num[n - 1], 默认是升序的

  • 降序排序

    bool cmp(const int &a, const int &b) {//默认写法
        return a > b;//降序排序
    }
    
    sort(num, num + 8, cmp);
    
  • 自定义类型的排序

    1、整形的
    bool cmp(const int &a, const int &b) {
        return a > b;//不能写a >= b, 这里严格执行
    }
    
    2.类的排序
    
    #include <iostream>
    #include <algorithm>
    using namespace std;
    
    struct node {
        int x, y;
        //直接下面的重定向,就不需要下面的cmp_node函数了,直接sort(t, t + 3);
        bool operator< (const node &b) const {
            return this->x > b.x;
        }
    };
    
    bool cmp_node(const node &a, const node &b) {
         if (a.x == b.x) {
             return a.y > b.y;
         }
        return a.x > b.x;
    }
    int main() {
        node t[10];
        t[0].x = 1, t[0].y = 2;
        t[1].x = 3, t[1].y = 5;
        t[2].x = 3, t[2].y = 1;
        sort(t, t + 2, cmp_node);
    }
    无法直接排序
    

2、代码演示

#include <iostream>
using namespace std;

bool cmp(const int &a, const int &b) {
    return a > b;//降序
}

int main() {
    int num[105] = {99, 3, -1, 7, 2, 54, 5l, 5};//
    sotr(num, num + 8);//num[0]~num[7] 升序排序
    sort(num + 2, num + 5 cmp);// num[2]~num[4]按照cmp的规则排序,这里是降序
    for (int i = 0; i < 8; i++) {
        cout << num[i] << " ";
    }
    cout << endl;
    return 0;
}


二、二分算法

对于整数的二分

while (l <= r) {
    int mid = (l + r) >> 2;
    if (key[mid] == target) return mid;
    else if (key[mid] > target) {
        r = mid  - 1;
    } else {
        l = mid + 1;
    }
}

三、二分的特殊情况

1、情况一:

查找第一个满足情况的,比如 0000011111这一串数字中,查找第一个1,

用法与模板:
while(l != r) {
    int mid = (l + r) / 2;
    if (num[mid] != target) {
        l = mid + 1;
    } else {
        r = mid;
    }
}
分析
  • 左边界的移动: l = mid + 1
  • 右边界的移动: r = mid

比如下面的,我们在数字 000011中寻找第一个1

首先,mid = 0, 因为二分查是线性排序的,那么mid所指向的这个0和mid前面的所有0就找不符合条件了,所以答案在mid下一个的区间,所以我们是 l = mid + 1;

第二步中mid = 1, 那么这个1有可能是结果,不然就是在这个1的前面,所以的尾指针变为mid , 即 r = mid

在这里插入图片描述

例题

某地总共有 M 堆瓜,第 i堆瓜的数量为 Xi。现有 N 组群众现在想要吃瓜,第 i组群众想要吃的瓜的数量为 Yi。现在对于每组想吃瓜的群众,需要在 M堆瓜中查找大于等于需要数量的第一堆瓜,并输出那堆瓜的编号,若所有瓜堆的数量均小于需要数量,则输出 0。

输入

输入共 3 行。

第一行两个整数 M,N。

第二行 M 个整数分别表示 X1,X2…XM。(保证各不相同)

第三行 N 个整数分别表示 Y1,Y2…YN。(保证各不相同)

输出

对于每个 Yi输出一行一个整数为大于等于需要数量的第一堆瓜的编号,若所有瓜堆的数量均小于需要数量,则输出 0。

样例输入

5 5
1 3 26 7 15
27 10 3 4 2

样例输出

0
5
2
4
2

数据规模与约定

时间限制:1 s

内存限制:256 M

100% 的数据保证 1≤M,N≤100,000,1≤Xi,Yi≤1,000,000,0001≤M,N≤100,000,1≤Xi,Yi≤1,000,000,000


**分析:**题目中的意思,观众想吃瓜的数量为a, 那么我们寻找第一个数量大于等于a的瓜堆,没有的话就输出0, 我们可以把前面所有小于a的瓜堆看为0,大于等于的瓜堆看为1,那么就是000011111这样子求第一个1的特殊二分情况了。

代码实现

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;

struct node {
    int num, cnt;
};

bool cmp(const node &a, const node &b) {//瓜堆按照小到大排序
    return a.num < b.num;
}

node wm[100005];
int n, m;
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &wm[i].num);
        wm[i].cnt = i;
    }
    wm[0].num = 2000000000;
    wm[0].cnt = 0;//设置哨兵,当没有符合的数据时,这个0号元素,就是符合大于想要吃瓜的数量的最小数量,输出0.
    sort(wm, wm + n + 1, cmp);
    for (int i = 0; i < m; i++) {
        int t;
        int l = 0, r = n;
        scanf("%d", &t);
        while(l != r) {
            int mid = (l + r) / 2;
            if (wm[mid].num >= t) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        printf("%d\n", wm[l].cnt);
    }
    return 0;
}

OJ-391

题目描述

对于给定的一个长度为 N 的正整数数列 Ai,现要将其分成 M(M≤N)段,并要求每段连续,且每段和的最大值最小。

关于最大值最小:

例如一数列 4 2 4 5 1 要分成 3 段

将其如下分段:

[4 2] [4 5] [1]

第 1 段和为 6,第 2 段和为 9,第 3段和为 1,和最大值为 9。

将其如下分段:

[4] [2 4] [5 1]

第 1 段和为 4,第 2 段和为 6,第 3 段和为 6,和最大值为 6。

并且无论如何分段,最大值不会小于 66。

所以可以得到,要将数列 4 2 4 5 1分成 3 段,每段和的最大值最小为 6。

输入

第一行两个整数 N,MN,M。(1≤M≤N≤100,000)

接下来 NN 行,每行一个数,表示 Ai。(1≤Ai≤100,000,000)

输出

一个正整数,即每段和最大值最小为多少。

样例输入

5 3
4
2
4
5
1

样例输出

6

数据规模与约定

时间限制:1 s

内存限制:256 M

100% 的数据保证 1≤M≤N≤100,000,1≤Ai≤100,000,0001≤M≤N≤100,000,1≤Ai≤100,000,000

#include<iostream>
#include<cstdio>
using namespace std;

long long n, m, num[100005];
long long l, r;

long long func(long long x) {
    long long cnt = 0, now = 0;
    for (int i = 0; i < n; i++) {
        if (now + num[i] == x) {
            cnt++;
            now = 0;
        } else if (now + num[i] > x) {
            cnt++;
            now = num[i];
        } else {
            now += num[i];
        }
    }
    if(now) cnt++;
    return cnt;
}

long long bs() {
    while (l != r) {
        long long mid = (l + r) / 2;
        long long t = func(mid);
        if (t > m) {
            l = mid + 1;
        } else {
            r = mid;
        }
    }
    return r;
}

int main() {
    scanf("%lld%lld", &n, &m);
    for (int i = 0; i < n; i++) {
        scanf("%lld", &num[i]);
        l = max(l, num[i]);
        r += num[i];
    }
    printf("%lld\n", bs());
    return 0;
}


2、情况二:

查找最后一个满足情况的,比如 111110000这一串数字中,查找最后一个1,

用法与模板:
while(l != r) {
    int mid = (l + r + 1) / 2;
    if (num[mid] != target) {
        l = mid;
    } else {
        r = mid - 1;
    }
}
分析
  • 左边界的移动: l = mid
  • 右边界的移动: r = mid - 1

比如下面的,我们在数字 1110000中寻找最后一个1

首先,mid = 0, 因为二分查是线性排序的,那么mid所指向的这个0和mid后面的所有0就找不符合条件了,所以答案在mid的钱前面,所以我们是 r = mid - 1;

第二步中mid = 1, 那么这个1有可能是结果,或者答案在它后面,所以的首指针变为mid , 即 l= mid
在这里插入图片描述

  • mid = (l + r + 1) / 2

    为什么这里是这个呢?q其实是为了避免死循环问题, 比如在10中查找1,

    对于mid = (l + r ) / 2 的话, mid 指向的1, 那么 l = mid , 就会发生死循环, 但如果是 mid = (l + r + 1) / 2, 那么mid 指向0, r = mid - 1, 就不会发生死循环了


例题(特殊情况,二分答案)
  1. 题目描述

某林业局现在 N根原木,长度分别为 Xi,为了便于运输,需要将他们切割成长度相等的 M 根小段原木(只能切割成整数长度,可以有剩余),小段原木的长度越大越好,现求小段原木的最大长度。例如,有 33 根原木长度分别为 6,15,226,15,22,现在需要切成 8 段,那么最大长度为 5。

输入

第一行两个整数 N,M。(1≤N≤100,000,1≤M≤100,000,000)

接下来 N 行,每行一个数,表示原木的长度 Xi。(1≤Xi≤100,000,000)

输出

输出小段原木的最大长度, 保证可以切出 MM 段。

样例输入

3 8
6
15
22

样例输出

5

数据规模与约定

时间限制:1 s

内存限制:256 M

100% 的数据保证 1≤N≤100,000,1≤M≤100,000,000,1≤Xi≤100,000,000


分析

对样例给出的三块木头进行分析:

切成的长度12345678910
可以切成的段数4321149865333

我们可以看出,随着可以切成的段数的减小,目标切成的长度是增加的,所以我们可以对所求的答案进行二分,简称—二分答案

  • 首先,我们的下边界 l = 1, 上边界 r = max(num[i]),就是最长的木头的长度,因为我们需要切成的木头的长度不可能大于最长的木头的长度
  • mid 的含义,mid 作为一个临时的答案,也就是切成的的木头的长度
  • 举个例子, 当我们切的长度为4.9, 那么其实我们也可以切成8段的, 当切成5.1 ,就是7, 也就是满足1111100000寻找最后一个1的特殊情况
#include<iostream>
#include<cstdio>
using namespace std;

int n, m, l = 1, r, num[100005];

int func(int x) {//当目标长度是x时,求可以切成的段数是多少
    int  t = 0;
    for (int i = 1; i <= n; i++) {
        t += num[i] / x;//每段可以切成的长度相加
    }
    return t;//总段数
}

int bs() {
    while(l != r) {
        int mid = ((long long)l + r + 1) / 2;//mid是临时答案,就是需要切成的木头的长度
        int t = func(mid);//在mid这个长度下,可以切成多少段
        if (t >= m) {
            l = mid;
        } else {
            r = mid -1 ;
        }
    }
    return r;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &num[i]);
        r = max(r, num[i]);//右边界是最长的木头的长度,因为我们需要切的木头的长度肯定小于最长的木头
    }
    printf("%d\n", bs());
    return 0;
}


  1. 例题2暴躁的程序猿

某公司的程序猿每天都很暴躁,因为他们每个人都认为其他程序猿和自己风格不同,无法一同工作,当他们的工位的编号距离太近时,他们可能会发生语言甚至肢体冲突,为了尽量避免这种情况发生,现在公司打算重新安排工位,因为有些关系户的工位是固定的,现在只有一部分工位空了出来,现在有 N 个程序猿需要分配在 M 个工位中,第 i个工位的编号为 Xi,工位编号各不相同,现在要求距离最近的两个程序猿之间的距离最大,求这个最大距离是多少。Xi和 Xj 工位之间距离为|Xi−Xj|。

输入

输入共 M+1 行。

第一行两个整数 M,N。(2≤N≤M≤100,000)(

接下来 M 行,每行一个数,表示剩余的工位的编号。

输出

输出距离最近的两个程序猿之间的最大距离。

样例输入

5 3
1
2
8
4
9

样例输出

3

数据规模与约定

时间限制:1 s

内存限制:256 M

100% 的数据保证 2≤N≤M≤100,000,1≤Xi≤1,000,000,000

分析:

题目的意思就是,比如我们有1 3 5 7 8 这几个位置,需要安排3个人的话, 那么安排这三个人之间的两个人的之间的距离最大为多大

以样例为例子:

座位号 1 2 4 8 9 (排序后的座位号)

如果距离为1,那么每一个作为都可以坐一个人,那么就需要5个人,如果距离为2, 可以安排人的位置分别为1号位、4号位、8号位,因为它们之间的距离都大于等于2; 如果距离为3, 可以安排的位置分别为 1号、 4号、 8号; 如果距离为4,可以安排的位置号分别为1号、 8号

画出表格如下:

安排的距离12345678
可以安排的程序员的人数53322222

可以看出,随着我们的安排的程序猿的人数增加,我们的答案(安排的距离)是递减的,所以我们可以使用二分答案法,再观察一下,是符合11110000的里面找最后一个1的特殊请情况的

#include<iostream>
#include <algorithm>
#include <cstdio>
using namespace std;

int n, m, num[100005];

int func(int x) {
    int ans = 1;//可以安排的人数
    int last = num[0];//上一次的安排位置
    for (int i = 0; i < n; i++) {
        if (num[i] - last >= x){//距离够的话,我们安排一个程序员
            ans++;
            last = num[i];
        }
    }
    return ans;
}

int bs() {
    int l = 1, r = num[n - 1] - num[0];//最远的距离是最远两个位置之间的距离
    while (l != r) {
        int mid = (l + r + 1) / 2;
        int ans = func(mid);//在这个距离下,可以安排多少个程序员
        if (ans >= m) {
            l = mid;
        } else {
            r = mid - 1;
        }
    }
    return r;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i <n; i++) {
        scanf("%d",&num[i]);
    }
    sort(num, num + n);
    printf("%d\n", bs());

    return 0;
}


例题三:丢瓶盖

题目描述

小明小时候很贪玩,在他童年时期的某一天,他在地上丢了 A 个瓶盖,为了简化问题,我们可以当作这 A 个瓶盖丢在一条直线上,现在他想从这些瓶盖里找出 B 个,使得距离最近的 2 个距离最大,他想知道,最大可以到多少呢?

输入

第一行两个整数 A,B(2≤B≤A≤100,000)

接下来 A 行,每行一个数,表示瓶盖的位置坐标 Ai。(1≤Ai≤100,000,000)

输出

一个正整数,相邻的两个瓶盖的最大距离。

样例输入

5 3
1
2
3
4
5

样例输出

2

数据规模与约定

时间限制:1 s

内存限制:256 M

100% 的数据保证 2≤B≤A≤100,000,1≤Ai≤100,000,000

#include<iostream>
#include<algorithm>
using namespace std;

int a, b;
int l, r, num[100005];


int  func(int  x) {
    int  cnt = 1,  last = num[0];
    for (int i = 1; i < a; i++) {
        if(num[i] - last >= x) {
            cnt++;
            last = num[i];
        }
    }
    return cnt;
}

int bs() {
    l = 1;
    while (l != r) {
        int mid = (l + r + 1) / 2;
        int t = func(mid);
        if ( t >= b ) {
            l = mid;
        } else {
            r = mid - 1;
        }
    }
    return l;

}
 
int main() {
    cin >> a >> b;
    for (int i = 0; i < a; i++) {
        cin >> num[i];
    }
    sort(num, num + a);
    r = num[a - 1] - num[0];
    cout << bs() << endl;


    return 0;
}


四、对于小数的二分

  • 循环条件

    while(r - l > 0.00001) { //差值到达一定程度,默认两者相等了
        l = mid;
        r = mid;
        //对于小数来说,+1的话,区间就会变得非常大,所以这里直接等于mid
    }
    
  • 如何保留两位小数

    1. 首先, 如果我们直接printf("%.2f\n", ans); 比如说 1.2365,然后会自己默认进位为1.24,而不是我们想要得到的1.23
    2. 有两种方法可以解决
      • ans = i(int )(ans * 100) / 100.00, 对于(int )(1.2365 * 100) --> 123 --> 123 / 100.00 = 1.230000 – > 再取两位, 就是1.23了
      • printf("%.2f\n", ans - 0.005); ans - 0.005 = 1.2314 这样子就不会进位了,如果保留三位,就是ans -0.0005

例题:

题目描述

有 N 条绳子,它们的长度分别为 Li。如果从它们中切割出 K 条长度相同的绳子,这 KK 条绳子每条最长能有多长?答案保留到小数点后 2 位(直接舍掉 2 位后的小数)。

输入

第一行两个整数 N 和 K,接下来 N 行,描述了每条绳子的长度 Li。

输出

切割后每条绳子的最大长度,保证答案大于零。

样例输入

4 11
8.02
7.43
4.57
5.39

样例输出

2.00

数据规模与约定

时间限制:1 s

内存限制:256 M

100% 的数据保证 1≤n≤k≤10,000,0<Li≤100,000


这一题的思路和上面的思路是一样的,使用二分答案法,随着可以切分的木头的长度增加,可以切成的段数是减少的,可以使用二分查找的方法,同时符合111110000这样子的特殊情况。

#include<iostream>
using namespace std;


int  n, k;
double num[10005], maxr;

//统计切成长度为x的木头时,总共可以切成的段数
int func(double x) {
    int t = 0;
    for (int i = 0; i < n; i++) {
        t += num[i] / x;
    }
    return t;
}

double bs() {
    double l = 0, r = maxr;
    while (r - l > 0.000001) {
        double mid = (l + r) / 2;
        int t =func(mid);//切成的段数
        if ( t >= k ) {//题目需要切成的k段
           l = mid;
        } else{
            r = mid;
        }
    }
    return l;
}

int main() {
    cin >> n >> k;
    for (int i = 0; i < n; i++) {
        cin >> num[i];
        maxr = max(maxr, num[i]);
    }
    double ans = bs();
    printf("%.2f\n", ans - 0.005);

    return 0;
}

写于2021-07 -11

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值