专题二 二分法

2025ACM寒假集训

专题二 二分法

102400226 石华波 Vjudge账号:DustLi

问题一 二分查找

问题陈述

输入一个整数 n和 n个整数,保证这 n个整数已经按照从小到大进行排序。

然后输入一个整数 q( q≤100000 )代表 q次查询。接下来 q行,每行含有一个整数 m,代表一次查询。对于每次查询,使用二分查找判断 m是否在之前输入的 n个整数中出现过。如果出现,输出一行 “Yes”,否则输出 “No 。

解题思路

基本的二分查找,直接套板即可。

AC代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<iostream>

using namespace std;

int main(){
    int n;
    cin>>n;
    int array[n];
    for(int i=0;i<n;i++) {
        cin>>array[i];
    }
    int q;
    cin>>q;
    while(q--) {
        int m;
        cin>>m;
        int beg=0,end=n-1,mid=0;
        while(beg<=end) {
            mid=(beg+end)/2;
            if(array[mid]==m) break;
            if(array[mid]<m) beg=mid+1;
            else end=mid-1;
        }
        if(array[mid]==m) cout<<"YES"<<endl;
        else cout<<"NO"<<endl;
    }
    return 0;
}

问题二 A-B数对

问题陈述

给出一串正整数数列以及一个正整数 C,要求计算出所有满足 A−B=C的数对的个数(不同位置的数字一样的数对算不同的数对)。

解题思路

解题时并未联系到二分查找,而是使用Hash表,遍历数组,当HASH表中存在A=B+C时,答案加上A出现的次数。

AC代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
#include<unordered_map>

using namespace std;

int main(){
    int N,C;
    int tag;
    long long ans=0;
    cin>>N>>C;
    int nums[N];
    unordered_map<int,int> numcnt;
    for(int i=0;i<N;i++) {
        cin>>nums[i];
        numcnt[nums[i]]++;
    }
    for(int i=0;i<N;i++) {
        tag=nums[i]+C;
        ans+=numcnt[tag];
    }
    cout<<ans<<endl;
    return 0;
}

问题三 分巧克力

问题陈述

儿童节那天有 K 位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。

小明一共有 NN 块巧克力,其中第 i 块是 Hi×Wi的方格组成的长方形。

为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。切出的巧克力需要满足:

  1. 形状是正方形,边长是整数。
  2. 大小相同。

例如一块 6×5的巧克力可以切出 6 块 2×2的巧克力或者 2块 3×3的巧克力。

当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?

解题思路

题目要求每块巧克力大小相同,而对于一块H[i]*W[i]的巧克力,最多能切出边长为i的巧克力(H[i]/i)*(W[i]/i)份。

使用二分法,由题意可知最大边长不会超过已知Hi、Wi最大值中较小的那个,故以该值为上界,1为下界,当可切出的块数大于等于K时更新下界,否则更新上界。

AC代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<iostream>
#include<climits>
using namespace std;

int main(){
    int N,K;
    cin>>N>>K;
    int H[N],W[N];
    int Max=INT_MIN;
    for(int i=0;i<N;i++) {
        cin>>H[i]>>W[i];
        Max=max(Max,min(H[i],W[i]));
    }
    int Min=1;
    int mid;
    int cnt;
    while(Min<Max) {
        mid=(Min+Max+1)/2;
        cnt=0;
        for(int i=0;i<N;i++) {
            cnt+=(H[i]/mid)*(W[i]/mid);
        }
        if(cnt>=K) Min=mid;
        else Max=mid-1;
    }
    cout<<Min<<endl;
    return 0;
}

问题4 卡牌

问题陈述

这天,小明在整理他的卡牌。

他一共有 n 种卡牌,第 ii 种卡牌上印有正整数数 i(i∈[1,n]), 且第 i种卡牌现有 ai张。

而如果有 n 张卡牌,其中每种卡牌各一张,那么这 n 张卡牌可以被称为一套牌。小明为了凑出尽可能多套牌,拿出了 m 张空白牌, 他可以在上面写上数 i,将其当做第 i 种牌来凑出套牌。然而小明觉得手写的牌不太美观,决定第 i种牌最多手写 bi张。

请问小明最多能凑出多少套牌?

解题思路

首先,十年OI一场空,不开LongLong见祖宗!

同样是用二分法查找最多能凑出的牌数,此处上界可以是max(Right,b[i]+a[i]),但对结果影响不大,故没有进行修改,使用了与上一题不同的二分板子。

以1为下界,一个足够大的数1e7为上界,Check函数用于检查是否能够凑出mid套牌,如果mid-a[i]>b[i],说明无论如何此种牌都无法凑到mid张,可直接return 0,循环结束后将sum与m比较,如若sum小于等于m则说明可以凑出mid套牌,反之不然。

AC代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<iostream>
#include<climits>
#define ll long long
using namespace std;

const ll N=2e5+10;
ll n,m;
ll a[N],b[N];

ll Check(ll x);

int main() {
    cin>>n>>m;
    for(ll i=0;i<n;i++) cin>>a[i];
    for(ll i=0;i<n;i++) cin>>b[i];
    ll Left=0,Right=1e7;
    while(Left<Right) {
        ll mid=(Right+Left+1)/2;
        if(Check(mid)) Left=mid;
        else Right=mid-1;
    }
    cout<<Right<<endl;
    return 0;
}

ll Check(ll x) {
    ll sum=0;
    for(ll i=0;i<n;i++) {
        if(x-a[i]>b[i]) return 0;
        sum+=x-a[i]>0?x-a[i]:0;
    }
    return sum<=m;
}

问题五 书的复制

问题陈述

现在要把 m 本有顺序的书分给 k个人复制(抄写),每一个人的抄写速度都一样,一本书不允许给两个(或以上)的人抄写,分给每一个人的书,必须是连续的,比如不能把第一、第三、第四本书给同一个人抄写。

现在请你设计一种方案,使得复制时间最短。复制时间为抄写页数最多的人用去的时间。

解题思路

二分法此处不表,与上文类似。

复制时间即抄写页数最多的人所抄页数,该值最大为所有书页码之和,最小为页码数最多那本书的页码数,以此为上下界。

Check函数中,检查抄的页数加上当前书的页数是否超过mid(所求复制时间),是则换人,否则继续,最后比较所需人数与实际人数,若所需人数小于等于实际人数则可行,反之不然。

在输出答案时,选择从后往前遍历,即让后面的人多抄以达到前面的人少抄的效果,记录下每个人的结束书号,第一个人的起始书号为1,最后一个人的结束书号为n,当前人的起始书号为上一个人的结束书号+1。

AC代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<iostream>
#include<climits>
#define ll long long
using namespace std;

int Check(int upp);

int book,people;
int books[502];

int main() {
    cin>>book>>people;
    int left=0,right=0;
    for(int i=1;i<=book;i++) {
        cin>>books[i];
        right+=books[i];
        left=max(left,books[i]);
    }
    int mid=0;
    while(left+1<right) {
        mid=(left+right)/2;
        if(Check(mid)) right=mid;
        else left=mid;
    }
    int last[505],cnt=1;
    last[1]=book+1;
    last[people+1]=1;
    int pages=0;
    for(int i=book;i>0;i--) {
        if(pages+books[i]>right) {
            pages=0;
            last[++cnt]=i+1;
        }
        pages+=books[i];
    }
    for(int i=people;i>0;i--) {
        cout<<last[i+1]<<" "<<last[i]-1<<endl;
    }
    return 0;
}

int Check(int upp) {
    int pcnt=1,pages=0;
    for(int i=book;i>0;i--) {
        if(pages+books[i]>upp) pages=0,pcnt++;
        pages+=books[i];
    }
    return pcnt<=people;
}

问题六 青蛙过河

小青蛙住在一条河边,它想到河对岸的学校去学习。小青蛙打算经过河里的石头跳到对岸。

河里的石头排成了一条直线,小青蛙每次跳跃必须落在一块石头或者岸上。不过,每块石头有一个高度,每次小青蛙从一块石头起跳,这块石头的高度就会下降 1,当石头的高度下降到 0时小青蛙不能再跳到这块石头上(某次跳跃后使石头高度下降到 0是允许的)。

小青蛙一共需要去学校上 x天课,所以它需要往返 2x次。当小青蛙具有一个跳跃能力 y时,它能跳不超过 y的距离。

请问小青蛙的跳跃能力至少是多少才能用这些石头上完 x次课。

解题思路

一只青蛙往返2x次<=>一只青蛙单向过河2x次<=>2x只青蛙单向过河1次。

在第i次跳越中,这些青蛙必将落在[i,y+i-1]的区间中,故每个这样的区间中石头高度之和必须大于等于2x,此为必要性。

接下来说明充分性:当青蛙们在[i,y+i-1]的区间内上落脚时,令落在石头i上的青蛙向前跳跃[1,y]格,则此时所有的青蛙都处于[i+1,y+1]区间中,由于每个区间的石头高度之和均大于等于2x,所以青蛙均能顺利落脚。显然,这2x只青蛙可以在[1,y]上落脚,那么以此类推,他们就可以在[2,y+1]、[3,y+2]……上落脚,故所有青蛙均能过河。

但如果每次都以H[i]+…+H[y+i-1]的方式计算区间高度和,用时就太久了,故计算从石头1到石头n的高度和Sum[i],每次只需令区间端点的高度和相减即可。

AC代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<iostream>
#include<climits>
#define ll long long
using namespace std;

ll Check(ll y);

const ll N=1e5+1;

ll width,days;
ll H[N],Sum[N]={0};

int main() {
    cin>>width>>days;
    days*=2;
    for(ll i=1;i<width;i++) {
        cin>>H[i];
        Sum[i]=Sum[i-1]+H[i];
    }
    ll left=1,right=width;
    while(left<right) {
        ll mid=(left+right)/2;
        if(Check(mid)) right=mid;
        else left=mid+1;
    }
    cout<<left<<endl;
    return 0;
}

ll Check(ll y) {
    for(ll i=y;i<width;i++) {
        if(Sum[i]-Sum[i-y]<days) return 0;
    }
    return 1;
}

学习总结

本专题内容主要为二分法的应用,通过学习,解决了两道绿题,提高了自己的能力。

Licensed under CC BY-NC-SA 4.0
2025寒假ACM新生线上集训
使用 Hugo 构建
主题 StackJimmy 设计