【基础算法】二分(二分查找 + 二分答案)
文章目录
- 一、二分查找
- 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)。二分算法的大致思路是:
- 定义一个
left
和right
,让left = 0
(数组开头),right = n - 1
(数组结尾)。 - 求出
left
和right
的中点位置mid
,用mid
指向的值与target
作比较。 - 如果
a[mid] < target
,则让left
移动到mid
的位置,反之让right
移动到mid
的位置。 - 重复 2、3 过程直到
left
和right
相遇。
虽然有了大致的思路,但是落实到代码上是有很多细节问题的,我们逐个来突破。
(2) 解题细节(important)
left
和right
如何移动?
当我们要查找 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
永远不会移动,会陷入死循环。
但是当我们需要查找最后一个位置时,则需要采用第二种方式。因为根据上面提到的 left
和 right
的移动方式可知,如果用第一种方式求中点,对于 [2, 2]
这种情况会陷入死循环。
还需要注意的是,当数据范围较大时,我们直接使用 left + right
这种写法可能会超出数据类型的范围。因此我们可以对它进行优化:
int mid = left + (right - left) / 2;
int mid = left + (right - left + 1) / 2;
这样写和上面两种写法效果一样,并且不会有超范围的风险。
结论:查找第一个位置用第一种方式,查找最后一个位置用第二种方式。
- 循环里的判断条件怎么写?
上面我们说到二分查找的结束条件是直到 left
和 right
相遇。那么 while
循环内的条件应该是 left < right
还是 left <= right
呢?思考一下,如果是 left <= right
,那么面临数组是 [2, 2]
,而我想要查找的元素是第一个 2 的情况,那么这个循环将永远不会结束!因为此时求出的中点在第一个 2 上,然后 right = mid
,接着求中点,然后会发现 left
和 right
就不会动了,但是循环不会结束。因此循环的结束条件应是 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
}// ⼆分结束之后可能需要判断是否存在结果
【说明】
- 不要死记硬背,算法原理搞清楚之后,在分析题目的时候自然而然就知道要怎么写二分的代码;
- 仅需记住一点,if/else 中出现 −1 的时候,求 mid 时 +1 就够了。
(5) STL 中的二分查找
在 algotithm
算法库中,有两个写好的二分查找算法。
lower_bound
:查找大于等于 x 的最小元素,返回的是迭代器;时间复杂度: O ( log n ) O(\operatorname{log}n) O(logn)。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 A−B=C 的数对的个数(不同位置的数字一样的数对算不同的数对)。
【输入格式】
输入共两行。
第一行,两个正整数 N , C N,C N,C。
第二行, N N N 个正整数,作为要求处理的那串数。
【输出格式】
一行,表示该串正整数中包含的满足 A − B = C A - B = C A−B=C 的数对的个数。
【示例一】
输入
4 1 1 1 2 3
输出
3
【说明/提示】
对于 75 % 75\% 75% 的数据, 1 ≤ N ≤ 2000 1 \leq N \leq 2000 1≤N≤2000。
对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 2 × 10 5 1 \leq N \leq 2 \times 10^5 1≤N≤2×105, 0 ≤ a i < 2 30 0 \leq a_i <2^{30} 0≤ai<230, 1 ≤ C < 2 30 1 \leq C < 2^{30} 1≤C<230。
2017/4/29 新添数据两组
(1) 解题思路
题目给定 C,让我们寻找 A 和 B,那么我们可以枚举 A,然后去寻找符合要求的 B,即寻找 A - C 的个数。
如何快速寻找 A - C 的个数?我们可以使用哈希表,这是最优的解法。但是同样我们也可以使用二分查找去寻找。
- 先将数组排序。
- 利用二分查找寻找到大于等于 B 的第一个位置
left
和大于 B 的第一个位置right
。 - 用
right - left
即可求出 B 的个数。
在二分查找时我们可以使用库中的 lower_bound
和 upper_bound
函数,分别查找 left
和 right
。
(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 1≤n,m≤103,估分和录取线 ≤ 10 4 \leq10^4 ≤104;
对于 100 % 100\% 100% 的数据, 1 ≤ n , m ≤ 10 5 1\leq n,m\leq10^5 1≤n,m≤105,估分和录取线 ≤ 10 6 \leq 10^6 ≤106 且均为非负整数。
(1) 解题思路
对于每一个学生的估分我们都需要找到与该分数 “最近” 的那个学校的分数,因此可以考虑排序 + 二分查找。
- 创建两个数组
sch[]
和stu[]
分别存储学校的分数线和学生的估分(下标从 1 开始)。 - 先将学校的分数排序。
- 对于每一个学生的估分
x
,用二分查找找出大于等于x
的最小的那个学校分数对应的下标pos
。 - 设置一个变量
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 1≤n≤105, 1 ≤ k ≤ 10 8 1\le k\le 10^8 1≤k≤108, 1 ≤ L i ≤ 10 8 ( i ∈ [ 1 , n ] ) 1\le L_i\le 10^8(i\in[1,n]) 1≤Li≤108(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 1≤N≤106, 1 ≤ M ≤ 2 × 10 9 1\le M\le2\times10^9 1≤M≤2×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 L≥1 且 N ≥ M ≥ 0 N \geq M \geq 0 N≥M≥0。
接下来 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 0≤M≤N≤10。
对于 50 % 50\% 50% 的数据, 0 ≤ M ≤ N ≤ 100 0 \le M \le N \le 100 0≤M≤N≤100。
对于 100 % 100\% 100% 的数据, 0 ≤ M ≤ N ≤ 50000 , 1 ≤ L ≤ 10 9 0 \le M \le N \le 50000,1 \le L \le 10^9 0≤M≤N≤50000,1≤L≤109。
(1) 解题思路
我们需要求得的是最短的跳跃距离的最大值,不难发现,当最短跳跃距离越大时,我们需要移走的石头数目就越多。也就是说,如果我们把这个最短跳跃距离的最大值设为 max_d
的话,那么在 [0, max_d]
范围内,我们需要移走的石头都是小于能移走的石头数目 M
的,而一旦超出了这个 max_d
,那么我们就无法移走那么多石头,不符合题意。因此为了找到这个 max_d
,我们可以利用二分去寻找。用二分去枚举最短跳跃距离 x
,检查这个最短跳跃距离对应所需要移走的数目是否小于等于 M
。
- 如何计算出对于每一个最短跳跃距离 x,需要移走的石头数目?
-
定义前后两个指针
i
和j
遍历整个数组,设i <= j
,每次j
从i
的位置往后移动; -
当发现
a[j] - a[i] >= x
时,说明[i + 1, j - 1]
之间的石头都可以移走; -
然后将
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;
}