二分

二分本质
给定一个具有单调性的区间 \([l,r]\),中间切一刀 \(mid=(l+r)/2;\) 可以将区间分为两个子区间,其合法的答案一定在其中的一个子区间中,继续对该区间进行二分。

按照上述原理,有一个问题:\(mid\) 这个位置分给左右那个区间?
(1)分给左区间,相当于:\([l,r] = [l,mid] + [mid+1,r]\); // 找左边界
(2)分给右区间,相当于:\([l,r] = [l,mid-1] + [mid,r]\); // 找右边界

image

【整数二分】起止位置

有 n 位同学按照年龄从小到大排好队。
王老师想要进行 q 次查询,年龄为 x 的同学,在队伍中首次出现的位置和最后一次出现的位置;
如果队伍中不存在年龄为 x 的同学,请输出 -1。

输入格式:
第一行包含整数 n 和 q,表示队伍中的总人数和询问个数。
第二行包含 n 个整数(均在1~10000范围内),表示队伍中每个人的年龄。
接下来 q 行,每行包含一个整数x,表示一次询问的值。

输出格式:
共 q 行,每行包含两个整数,表示所求年龄在队伍中的起始位置和终止位置。
如果数组中不存在该元素,则返回"-1 -1"。

数据范围: 1≤n≤100000, 1≤q, x≤10000。

输入样例 输出样例
6 3
1 2 2 2 3 3
2
1
8
2 4
1 1
-1 -1

分析: 单次询问要做到 \(log_2n\) 级别,考虑二分查找左右边界。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,x,a[N];
// 求 x 出现的左边界, [l,r] = [l,mid-1] + [mid,r]
int left(int l,int r,int x) {
    while(l<r) {
        int mid=l+r+1 >>1;
        if(a[mid]<=x) l=mid;
        else r=mid-1;
    }
    if(a[l]!=x) l=-1; return l;
}
// 求 x 出现的右边界, [l,r] = [l,mid] + [mid+1,r]
int right(int l,int r, int x) {
    while(l<r) {
        int mid=l+r>>1;
        if(a[mid]>=x) r=mid;
        else l=mid+1;
    }
    if(a[l]!=x) l=-1; return l;
}
int main() {
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++) scanf("%d",&a[i]);
    while(m--) {
        scanf("%d",&x);
        printf("%d %d\n",left(1,n,x),right(1,n,x));
    }
    return 0;
}

【小数二分】求算术平方根

给定 n,求 \(\sqrt{n}\),保留小数点后 6 位。

输入样例 输出样例
3.14
1.772005

分析: 算法平方根的求解本质上是在一个区间确定一个满足要求的值,那么如何快速确定这个值呢?

  • 牛顿迭代
    \(a×b=n,a=n\), 则 \(b=n/a;\)
    \(a_1 = (a+b)/2, b_1=n/a_1;\)
    \(a_2 = (a_1+b_1)/2, b_2=n/a_2;\)
    ..., 直到 \(|a_i-b_i|<eps\),则认为 \(a_i=b_i\)
    \(a_i×b_i={a_i}^2=n;\) 所以 \(\sqrt{n}=a_i\)

  • 小数二分

image

#include<bits/stdc++.h>
using namespace std;
const double eps=1e-8;// 一般比要保留的位数多 2
double sqr1(double n) { // 牛顿迭代
    double a=n;
    while(abs(a-n/a) > eps) {
        a = (a+n/a) / 2;
    }
    return a;
}
double sqr2(double n) { // 小数二分
    double l=0, r=n;
//    while(r-l > eps){  // 写法 1
    for(int i=1; i<=100; i++) { // 写法 2:直接枚举100次
        double mid = (l+r)/2;
        if(mid*mid >= n) r=mid;
        else l=mid;
    }
    return l;
}
int main() {
    double n=51;
    printf("%.6lf\n", sqrt(n));
    printf("%.6lf\n", sqr1(n));
    printf("%.6lf\n", sqr2(n));
}

【二分答案】切割绳子

有 n 条绳子,每条绳子的长度已知且均为正整数。绳子可以以任意正整数长度切割,但不可以连接。现在要从这些绳子中切割出 m 条长度相同的绳段,求绳段的最大长度是多少。

输入格式:
第一行是一个不超过 100 的正整数 n。
第二行是 n 个不超过 \(10^6\) 的正整数,表示每条绳子的长度。
第三行是一个不超过 \(10^8\) 的正整数 m。

输出格式: 绳段的最大长度,若无法切割,输出“Failed”。

输入样例 输出样例
3
5 10 8
2
8

分析: 可以切割的最大长度,二分答案 ------ 枚举答案,再检查答案是否合法。

  1. 确定答案区间 \([l,r]= [l,mid-1] + [mid,r]\)
  2. \(chk(x)\) 检查 绳段的长度为 \(x\) 的情况下是否合法:切割出来的绳子数量 s >= m。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e6+10;
int n,m,a[N];
// 检查 绳段的长度为 x 的情况下是否合法:切割出来的绳子数量 s >= m。
bool chk(int x){
    int s=0; // 切割出的绳子数量
    for(int i=1; i<=n; i++) s += a[i]/x;
    return s >= m;
}
int main() {
    scanf("%d",&n);
    for(int i=1; i<=n; i++) scanf("%d",&a[i]);
    scanf("%d",&m);
    int l=1, r=1e6;
    while(l < r){
        int mid = l+r+1 >> 1;
        if(chk(mid)) l=mid;
        else r=mid-1;
    }
    if(chk(l)) printf("%d\n",l);
    else printf("Failed\n");
    return 0;
}

练习题

同时出现的数

Hacker 同学拿到了 2 组数字,老师请你编程帮他找出,第 2 组数中的哪些数,在第 1 组数中出现了,从小到大输出所有满足条件的数。比如:
第 1 组数有:8 7 9 8 2 6 3
第 2 组数有:9 6 8 3 3 2 10
那么应该输出:2 3 3 6 8 9

输入格式:
第一行两个整数 n 和 m,分别代表 2 组数的数量,
第二行 n 个正整数,第三行 m 个正整数。

输出格式: 按照要求输出满足条件的数,数与数之间用空格隔开
数据范围: \(1≤n,m≤100000,每个数 ≤ 2×10^9\)

输入样例 输出样例
7 7
8 7 9 8 2 6 3
9 6 8 3 3 2 10
2 3 3 6 8 9

分析: 对两组数分别排序,再利用二分查找完成。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
int t,n,m,a[N],b[N];
int bs(int l,int r,int x){
    while(l < r){
        int mid = l+r>> 1;
        if(a[mid] >=x) r=mid;
        else l=mid+1;
    }
    if(a[l]!=x) l=-1;
    return l;
}
int main() {
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++) scanf("%d",&a[i]);
    for(int i=1; i<=m; i++) scanf("%d",&b[i]);
    sort(a+1,a+1+n);
    sort(b+1, b+1+m);
    for(int i=1; i<=m; i++){
        int x = bs(1,n,b[i]);
        if(x!=-1) printf("%d ",b[i]);
    }
    return 0;
}

一元三次方程求解

有形如:ax^3+bx^2+cx+d=0 这样的一个一元三次方程。给出该方程中各项的系数(a,b,c,d 均为实数),并约定该方程存在三个不同实根(根的范围在-100至100之间),且根与根之差的绝对值≥1。

要求由小到大依次在同一行输出这三个实根(根与根之间留有空格),并精确到小数点后2位。

输入格式: 一行,输入a, b, c, d
输出格式: 三个实根(根与根之间留有空格)

输入样例 输出样例
1 -5 -4 20
-2.00 2.00 5.00

分析: 本题需要明白一元三次方程及其求解方法(见下图),结合小数二分完成求解。
image

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
double a,b,c,d;
double f(double x){
    return a*pow(x,3) + b*pow(x,2) + c*x + d;
}
double bs(double l,double r){
    for(int i=1; i<=100; i++){
        double mid =(l+r)/2.0;
        if( f(l) * f(mid) <= 0) r=mid;
        else l=mid;
    }
    return l;
}
int main() {
    scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
    double A = 3*a, B=2*b, C=c;
    double t1 = (-B-sqrt(B*B-4*A*C)) / (2*A);
    double t2 = (-B+sqrt(B*B-4*A*C)) / (2*A);

    double x1 = bs(-100,t1);
    double x2 = bs(t1,t2);
    double x3 = bs(t2,100);

    printf("%.2lf %.2lf %.2lf\n",x1,x2,x3);
    return 0;
}

跳石头

一年一度的“跳石头”比赛又要开始了!
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。
为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。

输入格式:
第一行包含三个整数 L,N,M ,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。保证L≥1 且 N≥M≥0 。

接下来 N 行,每行一个整数,第 i 行的整数 Di( 0 < Di < L), 表示第 i 块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。

输出格式: 一个整数,即最短跳跃距离的最大值。
数据范围: \(0≤M≤N≤5×10^4,1≤L≤10^9\)

输入样例 输出样例
25 5 2
2
11
14
17
21
4

分析: 最短跳跃距离的最大值,二分答案 ------ 枚举答案,再检查答案是否合法。

  1. 确定答案区间,最短跳跃距离的最大值 \(\in [l,r]=[l,mid-1]+[mid,r]\)
  2. \(check(x)\) 函数:检查 当最短跳跃距离为 \(x\) 时需要移动的石头数量是否超标。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=5e4+10;
int L,n,m,a[N];
// 检查 当最短跳跃距离为 x 时需要移动的石头数量是否超标。
bool chk(int x){
    int s=0;
    for(int i=1,j=0; i<=n+1; i++){
        if(a[i]-j < x) s++;
        else j=a[i]; // 更新当前的位置
    }
    return s<=m;
}
int main() {
    scanf("%d%d%d",&L,&n,&m);
    for(int i=1; i<=n; i++) scanf("%d",&a[i]);
    a[0]=0, a[n+1]=L;

    int l=0, r=1e9;
    while(l<r){
        int mid=l+r+1 >>1;
        if(chk(mid)) l=mid;
        else r=mid-1;
    }
    printf("%d\n",l);
    return 0;
}
posted @ 2023-02-19 23:33  HelloHeBin  阅读(502)  评论(0编辑  收藏  举报