当前位置: 首页 > news >正文

【基础算法】二分(二分查找 + 二分答案)

文章目录

  • 一、二分查找
    • 1. 【案例】在排序数组中查找元素的第一个和最后一个位置 ⭐
      • (1) 二分查找的引入
      • (2) 解题细节(important)
      • (3) 代码示例
      • (4) 【模板】二分查找
      • (5) STL 中的二分查找
    • 2. 牛可乐和封印魔法 ⭐⭐
      • (1) 解题思路
      • (2) 代码实现
    • 3. A-B 数对 ⭐
      • (1) 解题思路
      • (2) 代码实现
    • 4. 烦恼的高考志愿 ⭐⭐
      • (1) 解题思路
      • (2) 代码实现
  • 二、二分答案
    • 1. 什么是二分答案
    • 2. 木材加工 ⭐⭐
      • (1) 解题思路
        • 思路一:暴力解法
        • 思路二:二分答案
      • (2) 代码实现
    • 3. 砍树 ⭐⭐
      • (1) 解题思路
      • (2) 代码实现
    • 4. 跳石头 ⭐⭐
      • (1) 解题思路
      • (2) 代码实现

一、二分查找

1. 【案例】在排序数组中查找元素的第一个和最后一个位置 ⭐

【题目链接】

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

请添加图片描述

(1) 二分查找的引入

我们先来观察以下这个题目,显然,我们可以使用暴力解法去求解,即遍历数组每一个元素。但是这样的时间复杂度是 O ( n ) O(n) O(n),有没有更快的方法?答案是有的。暴力解法显然没有用到数组有序这样的特点,而通过观察我们可以发现,但当我们选中一个元素的时,我们会发现这个元素左边的元素都是小于这个数的,而右边的元素都是大于这个数的,因此我们就将数组划分为了两段当我们任意查找一个值 x 时如果发现它小于 target,那么我们就可以直接把 x 左边的区间所有数全部舍去,转而去右边的区间寻找 target

此时我们说这个数组具有 二段性。如果有二段性,我们就可以使用二分查找算法去解决这个问题,时间复杂度为 O ( log ⁡ n ) O(\operatorname{log}n) O(logn)。二分算法的大致思路是:

  1. 定义一个 leftright,让 left = 0(数组开头),right = n - 1(数组结尾)。
  2. 求出 leftright 的中点位置 mid,用 mid 指向的值与 target 作比较。
  3. 如果 a[mid] < target,则让 left 移动到 mid 的位置,反之让 right 移动到 mid 的位置。
  4. 重复 2、3 过程直到 leftright 相遇。

虽然有了大致的思路,但是落实到代码上是有很多细节问题的,我们逐个来突破。


(2) 解题细节(important)

  • leftright 如何移动?

当我们要查找 target 第一个出现的位置的时候,下面两种写法那种是正确的:

// 写法一:
if(nums[mid] < target) left = mid + 1;
else right = mid;// 写法二:
if(nums[mid] <= target) left = mid;
else right = mid - 1;

显然写法一是正确的。因为当 nums[mid] < target 的时候说面 mid 左边的区间(包括 mid)都不是我们想要的元素,因为 left 可以放心移动到 mid 的后一位。如果 nums[mid] >= target 的话,有可能 mid 所指的元素就是我们要查找的元素,因此 right 只能移动到 mid 的位置。

而如果我们想要查找的是元素最后一个出现的位置时,则采用写法二

结论:查找第一个位置用写法一,查找最后一个位置用写法二。


  • 求中点的方式如何选择?

求数组中的中间位置,也就是 mid,有两种方式:

int mid = (left + right) / 2;
int mid = (left + right + 1) / 2;

这两种方式对于数组元素个数为奇数时没有差别,但是数组元素个数是偶数的时候二者会有所不同。比如对于 [1, 2, 3, 4] 这样的数组而言,使用第一种方式求出的 mid 在 2 的位置(中间靠左),用第二种方式求的 mid 在 3 的位置(中间靠右)。

而当我们需要查找元素的第一个位置时,需要采用第一种方式。例如当数组是 [2, 2] 时,采用第二种方式求出的 mid 会在第二个 2 的位置处,这样的话 right 永远不会移动,会陷入死循环。

但是当我们需要查找最后一个位置时,则需要采用第二种方式。因为根据上面提到的 leftright 的移动方式可知,如果用第一种方式求中点,对于 [2, 2] 这种情况会陷入死循环。

还需要注意的是,当数据范围较大时,我们直接使用 left + right 这种写法可能会超出数据类型的范围。因此我们可以对它进行优化:

int mid = left + (right - left) / 2;
int mid = left + (right - left + 1) / 2;

这样写和上面两种写法效果一样,并且不会有超范围的风险。

结论:查找第一个位置用第一种方式,查找最后一个位置用第二种方式。


  • 循环里的判断条件怎么写?

上面我们说到二分查找的结束条件是直到 leftright 相遇。那么 while 循环内的条件应该是 left < right 还是 left <= right 呢?思考一下,如果是 left <= right,那么面临数组是 [2, 2],而我想要查找的元素是第一个 2 的情况,那么这个循环将永远不会结束!因为此时求出的中点在第一个 2 上,然后 right = mid,接着求中点,然后会发现 leftright 就不会动了,但是循环不会结束。因此循环的结束条件应是 left < right。查找最后一个位置同理可以说明当使用 left <= right 时会陷入死循环。

结论:循环条件都用 while(left < right)


(3) 代码示例

class Solution 
{
public:vector<int> searchRange(vector<int>& nums, int target) {if(nums.empty()) return {-1, -1};// 查找第一个位置int left = 0, right = nums.size() - 1;int ret1, ret2;while(left < right)  // 二分查找{int mid = left + (right - left) / 2;if(nums[mid] < target) left = mid + 1;else right = mid;}ret1 = nums[left] == target ? left : -1;  // 注意判断找到的元素是否是我们想要的// 查找最后一个位置left = 0, right = nums.size() - 1;while(left < right)  // 二分查找{int mid = left + (right - left + 1) / 2;if(nums[mid] <= target) left = mid;else right = mid - 1;}ret2 = nums[left] == target ? left : -1;return {ret1, ret2};}
};

(4) 【模板】二分查找

通过上面的案例,我们可以总结出以下两个模板。

// ⼆分查找区间左端点 int l = 0, r = n - 1;
// int l = 1, r = n;
while(l < r)
{int mid = l + (r - l) / 2;if(check(mid)) r = mid;else l = mid + 1;
}// ⼆分结束之后可能需要判断是否存在结果
// ⼆分查找区间右端点 int l = 1, r = n;
// int l = 1, r = n;
while(l < r)
{int mid = l + (r - l + 1) / 2;if(check(mid)) l = mid;else r = mid - 1;  // 小技巧:出现减 1,那么求 mid 时就加 1
}// ⼆分结束之后可能需要判断是否存在结果

【说明】

  1. 不要死记硬背,算法原理搞清楚之后,在分析题目的时候自然而然就知道要怎么写二分的代码;
  2. 仅需记住一点,if/else 中出现 −1 的时候,求 mid 时 +1 就够了。

(5) STL 中的二分查找

algotithm 算法库中,有两个写好的二分查找算法。

  1. lower_bound查找大于等于 x 的最小元素,返回的是迭代器;时间复杂度: O ( log ⁡ n ) O(\operatorname{log}n) O(logn)
  2. upper_bound查找大于 x 的最小元素,返回的是迭代器。时间复杂度: O ( log ⁡ n ) O(\operatorname{log}n) O(logn)。 二者均采用二分实现。但是 STL 中的二分查找只能适用于 “在有序的数组中查找”,如果是二分答案就不能使用。因此还是需要记忆二分模板。

2. 牛可乐和封印魔法 ⭐⭐

【题目链接】

牛可乐和魔法封印

请添加图片描述


(1) 解题思路

这道题其实就是案例那道题套了一个情景,本质是一模一样的。 唯一需要注意的是我们需要多考虑一下无效的情况。

当我们用二分查找找到大于等于 x 的第一个元素位置 left_num ,和小于等于 y 的第一个元素 right_num 时:

  • 如果 left_num == 0,说明此时所有元素均小于 x,结果需要输出 0。
  • 如果 right_num == 0,说明此时所有元素均大于 y,结果需要输出 0。

(2) 代码实现

#include<iostream>using namespace std;typedef long long LL;const int N = 1e5 + 10;
LL a[N];
int n, q;int lower_bound(LL target)
{int left = 1, right = n;while(left < right){int mid = left + (right - left) / 2;if(a[mid] < target) left = mid + 1;else right = mid;}if(a[left] < target) return 0;  // 如果所有元素都小于 x, 那么返回 0return left;
}int upper_bound(LL target)
{int left = 1, right = n;while(left < right){int mid = left + (right - left + 1) / 2;if(a[mid] <= target) left = mid;else right = mid - 1;}if(a[left] > target) return 0;  // 如果所有元素都大于 y, 那么返回 0return left;
}int main()
{cin >> n;for(int i = 1; i <= n; i++) cin >> a[i];cin >> q;while(q--){LL x, y;cin >> x >> y;int left_num = lower_bound(x);int right_num = upper_bound(y);if(left_num == 0 || right_num == 0) cout << 0 << endl;else cout << right_num - left_num + 1 << endl;}return 0;
}

3. A-B 数对 ⭐

【题目链接】

P1102 A-B 数对 - 洛谷

【题目描述】

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

【输入格式】

输入共两行。

第一行,两个正整数 N , C N,C N,C

第二行, N N N 个正整数,作为要求处理的那串数。

【输出格式】

一行,表示该串正整数中包含的满足 A − B = C A - B = C AB=C 的数对的个数。

【示例一】

输入

4 1
1 1 2 3

输出

3

【说明/提示】

对于 75 % 75\% 75% 的数据, 1 ≤ N ≤ 2000 1 \leq N \leq 2000 1N2000

对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 2 × 10 5 1 \leq N \leq 2 \times 10^5 1N2×105 0 ≤ a i < 2 30 0 \leq a_i <2^{30} 0ai<230 1 ≤ C < 2 30 1 \leq C < 2^{30} 1C<230

2017/4/29 新添数据两组


(1) 解题思路

题目给定 C,让我们寻找 A 和 B,那么我们可以枚举 A,然后去寻找符合要求的 B,即寻找 A - C 的个数。

如何快速寻找 A - C 的个数?我们可以使用哈希表,这是最优的解法。但是同样我们也可以使用二分查找去寻找。

  1. 先将数组排序
  2. 利用二分查找寻找到大于等于 B 的第一个位置 left大于 B 的第一个位置 right
  3. right - left 即可求出 B 的个数。

在二分查找时我们可以使用库中的 lower_boundupper_bound 函数,分别查找 leftright


(2) 代码实现

#include<iostream>
#include<algorithm>using namespace std;typedef long long LL;const int N = 2e5 + 10;
LL a[N];
LL n, c;int main()
{cin >> n >> c;for(int i = 1; i <= n; i++) cin >> a[i];sort(a + 1, a + 1 + n);LL cnt = 0;for(int i = 1; i <= n; i++){LL b = a[i] - c;auto left = lower_bound(a + 1, a + i, b);auto right = upper_bound(a + 1, a + i, b);cnt += right - left;}cout << cnt;return 0;
}

4. 烦恼的高考志愿 ⭐⭐

【题目链接】

P1678 烦恼的高考志愿 - 洛谷

【题目背景】

计算机竞赛小组的神牛 V 神终于结束了高考,然而作为班长的他还不能闲下来,班主任老 t 给了他一个艰巨的任务:帮同学找出最合理的大学填报方案。可是 v 神太忙了,身后还有一群小姑娘等着和他约会,于是他想到了同为计算机竞赛小组的你,请你帮他完成这个艰巨的任务。

【题目描述】

现有 m m m 所学校,每所学校预计分数线是 a i a_i ai。有 n n n 位学生,估分分别为 b i b_i bi

根据 n n n 位学生的估分情况,分别给每位学生推荐一所学校,要求学校的预计分数线和学生的估分相差最小(可高可低,毕竟是估分嘛),这个最小值为不满意度。求所有学生不满意度和的最小值。

【输入格式】

第一行读入两个整数 m , n m,n m,n

第二行共有 m m m 个数,表示 m m m 个学校的预计录取分数。

第三行有 n n n 个数,表示 n n n 个学生的估分成绩。

【输出格式】

输出一行,为最小的不满度之和。

【示例一】

输入

4 3
513 598 567 689
500 600 550

输出

32

【说明/提示】

数据范围:

对于 30 % 30\% 30% 的数据, 1 ≤ n , m ≤ 10 3 1\leq n,m\leq10^3 1n,m103,估分和录取线 ≤ 10 4 \leq10^4 104

对于 100 % 100\% 100% 的数据, 1 ≤ n , m ≤ 10 5 1\leq n,m\leq10^5 1n,m105,估分和录取线 ≤ 10 6 \leq 10^6 106 且均为非负整数。


(1) 解题思路

对于每一个学生的估分我们都需要找到与该分数 “最近” 的那个学校的分数,因此可以考虑排序 + 二分查找

  1. 创建两个数组 sch[]stu[] 分别存储学校的分数线和学生的估分(下标从 1 开始)。
  2. 先将学校的分数排序。
  3. 对于每一个学生的估分 x,用二分查找找出大于等于 x 的最小的那个学校分数对应的下标 pos
  4. 设置一个变量 sum 记录不满意度的总和。根据二分查找,可以得出对于每一个估分 x,都有 sum += min(abs(x - stu[pos]), abs(x - stu[pos - 1]))

对于上面的式子,做以下几点说明:

  • 如果找到的 pos 位于学校分数线数组的内部,那么显然公式成立。
  • 如果找到的 pos 位于 sch[] 数组的尾部,即所有学校的分数线都小于等于估分 x。那么一定有 abs(x - stu[pos]) < abs(x - stu[pos - 1]),显然成立。
  • 如果找到的 pos 位于 sch[] 数组的头部(pos == 1),即可能出现所有分数线都高于估分 x 的情况,那么此时只能取 abs(x - stu[pos])。为了保证不取到 abs(x - stu[pos - 1]),我们需要让 x - stu[0] 的值取绝对值是一个很大的数,这里的 stu[0] 可以称它为 “左右护法”。

(2) 代码实现

#include<iostream>
#include<algorithm>using namespace std;typedef long long LL;const int N = 1e5 + 10;
int sch[N], stu[N];
int m, n;// 查找大于等于 target 的最小元素的并返回其下标
int bisearch(int target)
{int left = 1, right = m;while (left < right){int mid = left + (right - left) / 2;if (sch[mid] < target) left = mid + 1;else right = mid;}return left;
}int main()
{cin >> m >> n;for (int i = 1; i <= m; i++) cin >> sch[i];for (int i = 1; i <= n; i++) cin >> stu[i];sort(sch + 1, sch + m + 1);sch[0] = -1e7;  // 加上左右护法LL sum = 0;for (int i = 1; i <= n; i++){int pos = bisearch(stu[i]);sum += min(abs(stu[i] - sch[pos]), abs(stu[i] - sch[pos - 1]));}cout << sum;return 0;
}

二、二分答案

1. 什么是二分答案

准确来说,应该是【二分答案 + 判断】。

当一个问题的答案在某一个区间上都符合要求的时候,如果问这些符合要求的答案的最大值(最小值)时,我们往往可以采用二分答案去求解。

二分答案可以处理大部分【最大值最小】以及【最小值最大】的问题。如果一个问题的解空间在从小到大变化的过程中,判断答案的结果出现二段性,此时我们就可以二分这个解空间,通过判断,找出最优解。


2. 木材加工 ⭐⭐

【题目链接】

P2440 木材加工 - 洛谷

【题目描述】

木材厂有 n n n 根原木,现在想把这些木头切割成 k k k 段长度 l l l 的小段木头(木头有可能有剩余)。

当然,我们希望得到的小段木头越长越好,请求出 l l l 的最大值。

木头长度的单位是 cm \text{cm} cm,原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。

例如有两根原木长度分别为 11 11 11 21 21 21,要求切割成等长的 6 6 6 段,很明显能切割出来的小段木头长度最长为 5 5 5

【输入格式】

第一行是两个正整数 n , k n,k n,k,分别表示原木的数量,需要得到的小段的数量。

接下来 n n n 行,每行一个正整数 L i L_i Li,表示一根原木的长度。

【输出格式】

仅一行,即 l l l 的最大值。

如果连 1cm \text{1cm} 1cm 长的小段都切不出来,输出 0

【示例一】

输入

3 7
232
124
456

输出

114

【说明/提示】

对于 100 % 100\% 100% 的数据,有 1 ≤ n ≤ 10 5 1\le n\le 10^5 1n105 1 ≤ k ≤ 10 8 1\le k\le 10^8 1k108 1 ≤ L i ≤ 10 8 ( i ∈ [ 1 , n ] ) 1\le L_i\le 10^8(i\in[1,n]) 1Li108(i[1,n])


(1) 解题思路

思路一:暴力解法

我们首先会想到暴力解法,即从小到大依次枚举这个 l,看看如果分成长度为 l 能不能满足要求,如果 l 足够小,那么它就可以把木材分成很多段(段数 > k)从而符合要求,但是我们要的是最大的 l ,也就是说我们要不断从小到大枚举直到找到最后一个符合要求l 才是我们要的 l

显然,这会超时。

思路二:二分答案

通过观察我们会发现,假设 l 的最大值我们记为 max_l,木材长度的最大值记为 max_len。那么在 [1, max_l] 的这段区间内都是能够使切出的段数 >= k 的。而一旦在 (max_l, max_len] 这个范围内就不符合要求了。也就是说这个 max_l 将问题的答案分成了两段,一段符合要求,一段不符合要求,我们要找的就是符合要求的最大值

接下来就可以采用二分答案来思考问题,既然这个问题的答案具有二段性,那么我们就没必要一个一个枚举 l 了,我们可以让 left = 1, right = max_len,在这个区间内采用二分算法,如果 mid 指向的值(切割出的木材长度)符合要求,那么就让 left 移动到 mid 的位置;否则让 right 移动到 mid - 1 的位置,然后继续二分去寻找答案。


(2) 代码实现

#include<iostream>using namespace std;typedef long long LL;const int N = 1e5 + 10;
LL wood[N];
LL n, k;// 判断如果切割为长度 x 能不能满足题意 (段数 >= k)
bool check(LL x)
{LL c = 0;  // 记录段数for(int i = 1; i <= n; i++){c += wood[i] / x;}return c >= k;
}int main()
{cin >> n >> k;LL max_len = 0;for(int i = 1; i <= n; i++){cin >> wood[i];max_len = max(max_len, wood[i]);}LL left = 1, right = max_len;// 二分while(left < right){LL mid = left + (right - left + 1) / 2;if(check(mid)) left = mid;  // 如果切割成长度 mid 能满足条件else right = mid - 1;   // 否则}// 结束时记得判断这个值是否符合条件,因为有可能 k 过大,导致怎么切都无法到 k 段,这时输出 0if(check(left)) cout << left;else cout << 0;return 0;
}

3. 砍树 ⭐⭐

【题目链接】

[P1873 COCI 2011/2012 #5] EKO / 砍树 - 洛谷

【题目描述】

伐木工人 Mirko 需要砍 M M M 米长的木材。对 Mirko 来说这是很简单的工作,因为他有一个漂亮的新伐木机,可以如野火一般砍伐森林。不过,Mirko 只被允许砍伐一排树。

Mirko 的伐木机工作流程如下:Mirko 设置一个高度参数 H H H(米),伐木机升起一个巨大的锯片到高度 H H H,并锯掉所有树比 H H H 高的部分(当然,树木不高于 H H H 米的部分保持不变)。Mirko 就得到树木被锯下的部分。例如,如果一排树的高度分别为 20 , 15 , 10 20,15,10 20,15,10 17 17 17,Mirko 把锯片升到 15 15 15 米的高度,切割后树木剩下的高度将是 15 , 15 , 10 15,15,10 15,15,10 15 15 15,而 Mirko 将从第 1 1 1 棵树得到 5 5 5 米,从第 4 4 4 棵树得到 2 2 2 米,共得到 7 7 7 米木材。

Mirko 非常关注生态保护,所以他不会砍掉过多的木材。这也是他尽可能高地设定伐木机锯片的原因。请帮助 Mirko 找到伐木机锯片的最大的整数高度 H H H,使得他能得到的木材至少为 M M M 米。换句话说,如果再升高 1 1 1 米,他将得不到 M M M 米木材。

【输入格式】

1 1 1 2 2 2 个整数 N N N M M M N N N 表示树木的数量, M M M 表示需要的木材总长度。

2 2 2 N N N 个整数表示每棵树的高度。

【输出格式】

1 1 1 个整数,表示锯片的最高高度。

【示例一】

输入

4 7
20 15 10 17

输出

15

【示例二】

输入

5 20
4 42 40 26 46

输出

36

【说明/提示】

对于 100 % 100\% 100% 的测试数据, 1 ≤ N ≤ 10 6 1\le N\le10^6 1N106 1 ≤ M ≤ 2 × 10 9 1\le M\le2\times10^9 1M2×109,树的高度 ≤ 4 × 10 5 \le 4\times 10^5 4×105,所有树的高度总和 > M >M >M


(1) 解题思路

和上一道题十分类似,我们会发现,假设最高高度记为 max_h,那么当锯片高度为 [0, max_h] 时我们都能获得 >= M 米的木材,而一旦高度大于 max_h,我们将无法获得那么多木材。因此,为了找到这个最高高度 max_h,可以采用二分来寻找。


(2) 代码实现

#include<iostream>using namespace std;typedef long long LL;const int N = 1e6 + 10;
LL tree[N];
LL n, m;// 如果锯片的高度为 h,检查其能否得到 >= M 米的木材
bool check(int h)
{LL sum = 0;  // 能收获的木材总长度for(int i = 1; i <= n; i++){if(tree[i] > h) sum += tree[i] - h;}return sum >= m;
}int main()
{cin >> n >> m;for(int i = 1; i <= n; i++) cin >> tree[i];// 在[0, 1e5]区间内二分,寻找符合条件的高度int left = 0, right = 4e5;while(left < right){int mid = left + (right - left + 1) / 2;if(check(mid)) left = mid;  // 如果 mid 高度时能符合条件else right = mid - 1;  // 否则 }cout << left;return 0;
}

4. 跳石头 ⭐⭐

【题目链接】

[P2678 NOIP 2015 提高组] 跳石头 - 洛谷

【题目描述】

一年一度的“跳石头”比赛又要开始了!

这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N N N 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。

为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M M M 块岩石(不能移走起点和终点的岩石)。

【输入格式】

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

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

【输出格式】

一个整数,即最短跳跃距离的最大值。

【示例一】

输入

25 5 2 
2
11
14
17 
21

输出

4

【说明/提示】

输入输出样例 1 说明

将与起点距离为 2 2 2 14 14 14 的两个岩石移走后,最短的跳跃距离为 4 4 4(从与起点距离 17 17 17 的岩石跳到距离 21 21 21 的岩石,或者从距离 21 21 21 的岩石跳到终点)。

数据规模与约定

对于 20 % 20\% 20%的数据, 0 ≤ M ≤ N ≤ 10 0 \le M \le N \le 10 0MN10
对于 50 % 50\% 50% 的数据, 0 ≤ M ≤ N ≤ 100 0 \le M \le N \le 100 0MN100
对于 100 % 100\% 100% 的数据, 0 ≤ M ≤ N ≤ 50000 , 1 ≤ L ≤ 10 9 0 \le M \le N \le 50000,1 \le L \le 10^9 0MN50000,1L109


(1) 解题思路

我们需要求得的是最短的跳跃距离的最大值,不难发现,当最短跳跃距离越大时,我们需要移走的石头数目就越多。也就是说,如果我们把这个最短跳跃距离的最大值设为 max_d 的话,那么在 [0, max_d] 范围内,我们需要移走的石头都是小于能移走的石头数目 M 的,而一旦超出了这个 max_d,那么我们就无法移走那么多石头,不符合题意。因此为了找到这个 max_d,我们可以利用二分去寻找。用二分去枚举最短跳跃距离 x,检查这个最短跳跃距离对应所需要移走的数目是否小于等于 M

  • 如何计算出对于每一个最短跳跃距离 x,需要移走的石头数目?
  1. 定义前后两个指针 ij 遍历整个数组,设 i <= j,每次 ji 的位置往后移动;

  2. 当发现 a[j] - a[i] >= x 时,说明 [i + 1, j - 1] 之间的石头都可以移走;

  3. 然后将 i 更新到 j 的位置,继续重复 1、2 两步。


(2) 代码实现

#include<iostream>using namespace std;typedef long long LL;const int N = 5e4 + 10;
LL dist[N];
LL l, n, m;// 检查如果最短跳跃距离为 x 时,需要移走的岩石数是否 <= M
bool check(LL x)
{LL cnt = 0;  // 记录移走的岩石数for(int i = 0; i <= n; i++){int j = i + 1;while(j <= n && dist[j] - dist[i] < x) j++;cnt += j - i - 1;i = j - 1;}return cnt <= m;
}int main()
{cin >> l >> n >> m;for(int i = 1; i <= n; i++) cin >> dist[i];dist[n + 1] = l;  // 最后一个岩石的距离(终点)n++;  // 注意添加了最后一个岩石的距离之后要把 n 加上一LL left = 0, right = l;while(left < right){LL mid = left + (right - left + 1) / 2;if(check(mid)) left = mid;else right = mid - 1;}cout << left;return 0;
}
http://www.lryc.cn/news/573257.html

相关文章:

  • 华为云Flexus+DeepSeek征文|体验华为云ModelArts快速搭建Dify-LLM应用开发平台并创建b站视频总结大模型
  • Vue3 + TypeScript 中 let data: any[] = [] 与 let data = [] 的区别
  • C++ 内存分配器的作用
  • AI+预测3D新模型百十个定位预测+胆码预测+去和尾2025年6月21日第115弹
  • 【舞蹈】编排:如何对齐拍子并让小节倍数随BPM递减
  • 56-Oracle SQL Tuning Advisor(STA)
  • hot100——第六周
  • MagnTek MT6816-ACD 一款基于各向异性磁阻(AMR)技术的磁性角度传感器 IC
  • wordpress外贸独立站常用留言表单插件 contact form 7
  • 探索 Oracle Database 23ai 中的 SQL 功能
  • 小程序右上角○关闭事件
  • 基于深度学习的侧信道分析(DLSCA)Python实现(带测试)
  • RNN工作原理和架构
  • `teleport` 传送 API 的使用:在 Vue 3 中的最佳实践
  • Thrift 服务端的完整示例
  • 【设计模式】4.代理模式
  • 分组交换比报文交换的传输时延更低
  • PHP语法基础篇(五):流程控制
  • Occt几何内核快速入门
  • 力扣网C语言编程题:多数元素
  • OPENPPP2传输层控制算法剖析及漏洞修复对抗建议
  • 5.3 VSCode使用FFmpeg库
  • Git 使用手册:从入门到精通
  • 基于Qt的UDP主从服务器设计与实现
  • 【Linux第四章】gcc、makefile、git、GDB
  • 从需求到落地:充电桩APP开发的定制化流程与核心优势
  • 免费1000套编程教学视频资料视频(涉及Java、python、C C++、R语言、PHP C# HTML GO)
  • Python subprocess 模块详解
  • 60-Oracle 10046事件-实操
  • Java面试复习指南:JVM原理、并发编程与Spring框架