算法训练简单二分专题

本文介绍了6道与数组和二分查找相关的编程题目,包括寻找缺失数字、数值等于下标的元素、旋转数组的最小数字、剪绳子、愤怒的牛和最佳牛围栏等问题,每道题都提供了题解思路和示例代码,主要利用二分查找优化时间复杂度。
摘要由CSDN通过智能技术生成

1. AcWing 68. 0到n-1中缺失的数字

题目链接

题目描述

一个长度为 n − 1 n − 1 n1 的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围 0 0 0 n − 1 n−1 n1 之内。

在范围 0 0 0 n − 1 n - 1 n1 n n n 个数字中有且只有一个数字不在该数组中,请找出这个数字。

样例

样例输入 #1

[0,1,2,4]

样例输出 #1

3

提示

【数据范围】

1≤n≤1000

题解思路

因为题目给出的数组是递增的而且数字都限定好了最大也才 1000 1000 1000,所以直接从头开始暴力也可以过,但如果卡数据的话这种 O ( n ) O(n) O(n)的复杂度是不行的。我们注意到这个数组根据数据特性其实可以分成两部分,前一个部分的数字都满足数字等于下标,而后一个部分都满足数字等于下标加一,要找的那个元素正好是这两部分子数组的分界线,所以可以用二分来找这个元素,优化后的时间复杂度是O(logn),可以参考y总画的图理解。
在这里插入图片描述

我们需要求的答案是在右部分数组,令 m i d mid mid ( l + r ) / 2 (l+r)/2 (l+r)/2 ,每次检查 m i d mid mid 如果不等于 n u m s [ m i d ] nums[mid] nums[mid] 的话就说明答案可能是 m i d mid mid 或在 m i d mid mid 的左边,所以更新右端点 r r r m i d mid mid ,反之答案应该在 m i d mid mid 的右边不包括 m i d mid mid ,所以这种情况更新左端点为 l + 1 l+1 l+1 ,需要注意的是如果数组为空或者缺失的正好是最后一个元素的特殊情况需要特判一下。

示例代码

class Solution {
public:
    int getMissingNumber(vector<int>& nums) {
        if (nums.empty()) return 0;
        int l = 0, r = nums.size() - 1;
        if (nums[r] == r) return r + 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] != mid) r = mid;
            else l = mid + 1;
        }
        return r;
    }
};

2. AcWing 69. 数组中数值和下标相等的元素

题目链接

题目描述

假设一个单调递增的数组里的每个元素都是整数并且是唯一的。

请编程实现一个函数找出数组中任意一个数值等于其下标的元素。

例如,在数组[−3,−1,1,3,5]中,数字3和它的下标相等。

样例 #1

样例输入 #1

[-3, -1, 1, 3, 5]

样例输出 #1

3

提示

数组长度 [1,100]

题解思路

在这里插入图片描述
参考画的图的结论我们可以发现答案应该是后面部分数组的第一个元素, m i d mid mid 如果满足 n u m s [ m i d ] > = m i d nums[mid] >= mid nums[mid]>=mid 则说明 m i d mid mid 在后面部分数组,答案就应该为 m i d mid mid 或者在 m i d mid mid 前面,所以更新右端点 r r r m i d mid mid ,反之答案应该在 m i d mid mid 的右边不包括 m i d mid mid ,所以这种情况更新左端点为 l + 1 l+1 l+1
在这里插入图片描述
拿样例演示就是第一次的 m i d mid mid 下标大于对应的数值,所以答案在 m i d mid mid 的右边,更新左端点 l l l m i d + 1 mid+1 mid+1 ,下一步 m i d mid mid 此时到了下标为 3 3 3 的位置满足后半部数组特性大于等于对应的数值,所以下一步更新右端点 r r r m i d mid mid,不能更新的时候就找到了正确答案, n u m s [ r ] 或 n u m s [ l ] nums[r]或nums[l] nums[r]nums[l] 都可以,没有正确答案返回 − 1 -1 1

示例代码

class Solution {
public:
    int getNumberSameAsIndex(vector<int>& nums) {
        int l = 0, r = nums.size() - 1;
        while(l < r) {
            int mid = l + r >> 1;
            if(nums[mid] >= mid) r = mid;
            else l = mid + 1 ;
        }
        if(nums[l] == l) return l;
        else return -1;
    }
};

3. AcWing 22. 旋转数组的最小数字

原题链接

题目描述

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

输入一个升序的数组的一个旋转,输出旋转数组的最小元素。

例如数组 {3,4,5,1,2} 为 {1,2,3,4,5} 的一个旋转,该数组的最小值为 1。

数组可能包含重复项。

注意:数组内所含元素非负,若数组大小为 0,请返回 −1。

样例 #1

样例输入 #1

nums = [2, 2, 2, 0, 1]

样例输出 #1

0

提示

数组长度 [0,90]

题解思路

这道题也是剑指offer上一道经典题,可以参考原文讲的很详细。

根据性质我们也可以把这个数组划分成两部分,前一部分数组里的数字都大于等于后一部分数组里面的数字,最小的那个数是后面数组的起始元素,不过需要注意的是如果前部分的起始等于后部分的末尾元素则无法二分,因为如果 m i d mid mid 为这个值我们不能确定它是在前部分还是后部分,所以将最后的元素删掉就行,如果删到最后发现最后的元素还是大于起始元素说明这个数组本来就是单调的直接返回起始元素,接下来二分的操作跟前两道题差不多不再赘述,放个y总的图帮助理解。

在这里插入图片描述

示例代码

class Solution {
public:
    int findMin(vector<int>& nums) {
        int n = nums.size() - 1;
        if (n < 0) return -1;// 数组为空返回-1
        // 如果最后的元素的等于起始元素就删除最后的元素
        while (n > 0 && nums[n] == nums[0]) n--;
        // 如果删到最后发现最后的元素还是大于起始元素说明这个数组本来就是单调的
        if (nums[n] >= nums[0]) return nums[0];
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] < nums[0]) r = mid;
            else l = mid + 1;
        }
        return nums[r];
    }
};

4. AcWing 680. 剪绳子

原题链接

题目描述

N N N 根绳子,第 i i i 根绳子长度为 L i Li Li,现在需要 M M M 根等长的绳子,你可以对 N N N 根绳子进行任意裁剪(不能拼接),请你帮忙计算出这 M M M 根绳子最长的长度是多少。

输入格式

第一行包含 2 2 2 个正整数 N 、 M N、M NM,表示原始绳子的数量和需求绳子的数量。

第二行包含 N N N 个整数,其中第 i i i 个整数 L i Li Li 表示第 i i i 根绳子的长度。

输出格式

输出一个数字,表示裁剪后最长的长度,保留两位小数。

数据范围

1≤N,M≤100000

0<Li<109

样例 #1

样例输入 #1

3 4
3 5 4

样例输出 #1

2.50

样例解释

第一根和第三根分别裁剪出一根 2.50 2.50 2.50 长度的绳子,第二根剪成 2 2 2 2.50 2.50 2.50 长度的绳子,刚好 4 4 4 根。

题解思路

看到这个题大家如果初学二分可能会很疑惑,这怎么跟二分有关系呢,如果我们直接去想这个最长的长度可能不好下手,但是不妨转变一下思维假设我们已经知道这个长度是多少去判断按照这个长度是否有足够 M M M 根的绳子,这样只需要找到在这些符合的长度中找到最大的即可,这样就可以二分来找这个长度了,可以把绳子长度分为两个部分,一部分满足能找够 M M M 根绳子,另一部分则不能。取 l l l 为数据范围的最小值, r r r 为最大值,因为绳子长度可以是不是整数,所以这道题用浮点数二分来做不用考虑边界情况, m i d mid mid 就是 l l l r r r 的二分之一,接着每次 c h e c k check check 函数判断是否有足够多的 M M M 根绳子。有的话证明长度还可以再增加比 m i d mid mid 更大即在 m i d mid mid 的右边,更新左端点为 m i d mid mid 反正更新右端点。

示例代码

#include <iostream>
using namespace std;

const int N = 100010;
int n, m;
int l[N];

bool check(double& mid) {
    int cnt = 0;
    for (int i = 1; i <= n; i++) {
        cnt += l[i] / mid;
    }
    if (cnt >= m) return true;
    else return false;
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> l[i];
    double l = 0, r = 1e9;
    while (r - l > 1e-3) {
        double mid = (l + r) / 2;
        if (check(mid)) l = mid;
        else r = mid;
    }
    printf("%.2f", l);
    return 0;
}

5. AcWing 4176. 愤怒的牛

原题链接

题目描述

农夫约翰建造了一座有 n n n 间牛舍的小屋,牛舍排在一条直线上,第 i i i 间牛舍在 x i xi xi 的位置,但是约翰的 m m m 头牛对小屋很不满意,因此经常互相攻击。

约翰为了防止牛之间互相伤害,因此决定把每头牛都放在离其它牛尽可能远的牛舍。

也就是要最大化最近的两头牛之间的距离。

牛们并不喜欢这种布局,而且几头牛放在一个隔间里,它们就要发生争斗。

为了不让牛互相伤害。

约翰决定自己给牛分配隔间,使任意两头牛之间的最小距离尽可能的大,那么,这个最大的最小距离是多少呢?

输入格式

第一行用空格分隔的两个整数 n n n m m m

第二行为 n n n 个用空格隔开的整数,表示位置 x i xi xi

输出格式

输出仅一个整数,表示最大的最小距离值。

数据范围

2 ≤ n ≤ 105

0 ≤ xi ≤ 109

2 ≤ m ≤n

样例 #1

样例输入 #1

5 3
1 2 8 4 9

样例输出 #1

3

题解思路

这道题的思路和上面那个剪绳子的思路很像,不过这个是整数二分要考虑边界情况。我们假设最大长度已知去找能否放下足够的牛,将长度分为两个特性,一种可以放下足够的牛,另一种不行,就可以用二分了。能放下说明长度还可以增加,答案在目前二分的长度的右边,所以更新左端点 l l l m i d mid mid,反之更新右端点为 m i d − 1 mid - 1 mid1 c h e c k check check 函数里面则为判断是否能放下足够多的牛,如果一个牛舍和前面要比较的牛舍的距离大于等于 m i d mid mid 的话说明这个牛舍可以放牛,则计数加一,并且要更新下次比较的牛舍的位置,最后计数大于等于 m m m 头牛的话满足返回 t r u e true true,反之 f a l s e false false

示例代码

#include<bits/stdc++.h>
using namespace std;

const int N = 100010;
int n, m;
int a[N];

bool check(int mid) {
    int cnt = 1, now = a[0];
    for (int i = 1; i < n; i++) {
        if (a[i] - now >= mid) {
            cnt++;
            now = a[i];
        }
    }
    if (cnt >= m) return true;
    else return false;
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i++) cin >> a[i];
    sort(a, a + n);
    int l = 0, r = 1e9;
    while (l < r) {
        int mid = (l + r + 1) / 2;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    cout << l;
    return 0;
}

6. AcWing 102. 最佳牛围栏

原题链接

题目描述

农夫约翰的农场由 N N N 块田地组成,每块地里都有一定数量的牛,其数量不会少于 1 1 1 头,也不会超过 2000 2000 2000 头。

约翰希望用围栏将一部分连续的田地围起来,并使得围起来的区域内每块地包含的牛的数量的平均值达到最大。

围起区域内至少需要包含 F F F 块地,其中 F F F 会在输入中给出。

在给定条件下,计算围起区域内每块地包含的牛的数量的平均值可能的最大值是多少。

输入格式

第一行输入整数 N N N F F F,数据间用空格隔开。

接下来 N N N 行,每行输入一个整数,第 i + 1 i+1 i+1 行输入的整数代表第 i i i 片区域内包含的牛的数目。

输出格式

输出一个整数,表示平均值的最大值乘以 1000 1000 1000向下取整 之后得到的结果。

数据范围

1 ≤ N ≤ 100000

1 ≤ F ≤ N

样例 #1

样例输入 #1

10 6
6 
4
2
10
3
8
5
9
4
1

样例输出 #1

6500

题解思路

这道题转化为一句话就是求一个正整数序列里面平均数最大并且长度不小于 F F F 的子段,所以这道题我们可以对这个平均值来二分去判断是否有长度不小于 F F F 的子段满足条件。这里满足的条件是这部分子段所有数的和的平均值大于等于这个二分出来的平均值,我们让每个数都减去这个二分出来的平均值就把问题转换成判断存不存在一个长度不小于 F F F 的子段,其中每个数减去这个平均值后的和大于等于零。我们可以计算出每个数减去二分的平均值之后的值的前缀和,这样就可以直接计算出一个区间内的值的和是否大于 0 0 0。参考大佬的双指针解法十分巧妙,我们用一个指针指向开头,另一个指针指向它后面 F F F 距离的位置,这样可以确保区间长度始终大于等于 F F F,后一个指针的值减去前一个指针的值即为这段区间的值的和。这样我们让第一个指针存储遇到的最小值,让第二个指针减去这个最小值跟 0 0 0 比较,一旦遇到大于等于 0 0 0 的时候就说明找到了一个满足条件的子段,返回 t r u e true true 即可。

可以参考AcWing上这位大佬的题解很详细

示例代码

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

const int N = 100005;
int cows[N];
double sum[N];
int n, m;

bool check(double avg) {
    for (int i = 1; i <= n; i++) {
        sum[i] = cows[i] + sum[i - 1] - avg;
    }
    double minv = 1e9; 
    for (int i = 0, j = m; j <= n; i++, j++) {
        minv = min(minv, sum[i]);
        if (sum[j] - minv >= 0) return true;
    }
    return false;
}
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> cows[i];
    double l = 0, r = 2000;
    while (r - l > 1e-5) {
        double mid = (l + r) / 2;
        if (check(mid)) l = mid;
        else r = mid;
    }
    cout << (int)(r * 1000);
    return 0; 
}

7. AcWing 1460. 我在哪?

原题链接

题目描述

农夫约翰出门沿着马路散步,但是他现在发现自己可能迷路了!

沿路有一排共 N N N 个农场。

不幸的是农场并没有编号,这使得约翰难以分辨他在这条路上所处的位置。

然而,每个农场都沿路设有一个彩色的邮箱,所以约翰希望能够通过查看最近的几个邮箱的颜色来唯一确定他所在的位置。

每个邮箱的颜色用 A . . Z A..Z A..Z 之间的一个字母来指定,所以沿着道路的 N N N 个邮箱的序列可以用一个长为 N N N 的由字母 A . . Z A..Z A..Z 组成的字符串来表示。

某些邮箱可能会有相同的颜色。

约翰想要知道最小的 K K K 的值,使得他查看任意连续 K K K 个邮箱序列,他都可以唯一确定这一序列在道路上的位置。

例如,假设沿路的邮箱序列为 A B C D A B C ABCDABC ABCDABC

约翰不能令 K = 3 K=3 K=3,因为如果他看到了 A B C ABC ABC,则沿路有两个这一连续颜色序列可能所在的位置。

最小可行的 K K K 的值为 K = 4 K=4 K=4,因为如果他查看任意连续 4 4 4 个邮箱,那么可得到的连续颜色序列可以唯一确定他在道路上的位置。

输入格式

输入的第一行包含 N N N,第二行包含一个由 N N N 个字符组成的字符串,每个字符均在 A . . Z A..Z A..Z 之内。

输出格式

输出一行,包含一个整数,为可以解决农夫约翰的问题的最小 K K K 值。

样例 #1

样例输入 #1

7
ABCDABC

样例输出 #1

4

题解思路

这道题我们可以直接4个for循环暴力枚举所有情况,第一重循环枚举K,第二重枚举第一个子串的起始位置,第三重枚举对照子串的起始位置,第四重比较两个子串的每一个字符是不是一样。这样复杂度最坏是将近n4,n最大是100所以也是可以跑过的。

根据前面几题的思路我们可以把一个求最优解的问题转化为判断问题,这道题就可以转化为给一个长度 K K K,判断一个字符串里存不存在的长度为K的不重复的子串。根据这个思路我们就可以考虑对长度 K K K 二分,一部分满足存在一个长度为K的不重复的子串,另一部分则不满足,满足二段性。如果这个二分出来的 m i d mid mid 满足条件就说明最小的 K K K 应该在这个 m i d mid mid 的左边也可能就是 m i d mid mid,所以我们更新右端点为 m i d mid mid,反之更新左端点为 m i d + 1 mid+1 mid+1,此时复杂度就可以优化为 n3logn。

考虑到查找子串的时候我们每次都是暴力匹配,会重复很多操作,所以我们可以考虑使用STL自带的哈希表,把当前长度 K K K 的所有子串一个个插入到哈希表中,插入时每个不同的子串对应的值都不一样。如果我们准备插入新的子串的时候发现这个串对应的值不为 0 0 0(默认值是 0 0 0)说明已经有了相同的子串return false,否则继续插入,如果最后也没有出现冲突的话说明不存在相同的子串return true,这样可以进一步优化为 n2logn。

这道题通过字符串的哈希(后续学习一下补上)和后缀数组(没听说过QAQ)还可以优化为 n l o g n nlogn nlogn

示例代码

  • n4
#include <iostream>
#include <algorithm>
#include <unordered_set>

using namespace std;

string s;
int n;

int main() {
    cin >> n >> s;
    for (int k = 1; k <= n; k++) {
        bool flag = false;// 表示当前是否存在相同子串
        for (int i = 0; i < n - k + 1; i++) {
            for (int j = i + 1; j < n - k + 1; j++) {
                bool same = true;// 表示是否存在相同子串
                for (int u = 0; u < k; u++) {
                    if (s[i + u] != s[j + u]) {// 始终不进这个if语句说明两个串相同same仍为true
                        same = false;
                        break;// 说明当前俩子串不相等提前break
                    }
                }
                if (same) {// 始终不进这个if语句说明此次K长度不存在相同子串,flag始终为false
                    flag = true;
                    break;// 存在相同子串提前break
                }
            }
            if (flag) break;// 存在相同子串提前break
        }
        if (!flag) {// 代表不存在相同子串,因为从小到大枚举所以第一次的K就是最小值
            cout << k;
            break;// 找到k可以直接结束break
        }
    }
    return 0;
}

  • nlogn
#include <iostream>
#include <algorithm>
#include <unordered_set>

using namespace std;

string s;
int n;

bool check(int mid) {
    unordered_set<string> hash;
    for (int i = 0; i + mid - 1 < n; i++) {
        string p = s.substr(i, mid);
        if (hash.count(p)) return false;
        hash.insert(p);
    }
    return true;
}

int main() {
    cin >> n >> s;
    int l = 1, r = n;
    while (l < r) {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    cout << r;
    return 0;
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值