大家好,不同的时间,相同的地点,时隔多日我们又见面了。继上次的二分算法后,我们这次要来学习的是二分答案了。这个部分相较于前面的二分算法难度有相当的提升,希望大家有所准备。虽然难度增加了,但是博主还是会将其讲明白讲清楚的
let's go!
前言
二分算法虽然简单但是它是很重要的,在某些题目当中,我们以后还会遇到,希望大家能够有所重视。
1.二分答案
二分答案具体为:二分答案 + 判断
当做算法题的时候,如果遇到最大值最小以及最小值最大的问题,就可以思考着去使用二分答案来解决了。二分答案可以帮助解决绝大多数这类的问题,如果解空间在从小到大的变化过程中,判断答案的结果出现二段性,此时我们就可以去使用二分,去二分这个解空间,通过判断来找出最优解。
可能大家看这个概念还是比较抽象,跟着博主做完下面三道题后大家就会明白了。
2.题目
P1873 [COCI 2011/2012 #5] EKO / 砍树 - 洛谷
P2678 [NOIP 2015 提高组] 跳石头 - 洛谷
大家可以先行做一下这几道题,然后再看博主的思路。
3.木材加工
题目展示:
思路分析:
这道题可以说的上是二分答案的一道模版题目了,大家好好理解一下这道题:
大家看完题目后,应该有对题目有些理解了,那么我再来梳理一下这个题目吧。
这个题目会给出木头n个和需要切割出来的木头段数k,然后会给出n个木头的长度a[i]
我们思考一下也就知道,如果我们将木头切割成1cm的木头(最短)那么我们肯定是可以满足题目要求的段数,但是问题来了我们切割成1cm的小段不是能够切割出来的最长?
那么我们一种暴力的解法就是:
暴力解法思路:
枚举切割的长度从1到最长的木头,然后开始通过这个切割长度x来计算段数(t) t = a[i] / x
通过将段数最后计算出来的和拿来和要求的k来比较,如果总和大于k就可以算是待正确答案。
这样的话,我们可以将切割长度从最长的木头长度往1开始减少的枚举,这样当第一次出现大于k的情况那个切割长度就是我们需要的答案
对于这个思路大家有兴趣可以去实现一下,难度不大。
但是会出现超时的情况,我们就得用到二分答案的算法来解决这道题了。
二分答案思路:
如果我们所要的切割长度为ans,我们无非就是要在0~maxlen中找到这个答案ans
------------------------------------------
[0 ans maxlen]
------------------------------------------
当枚举的切割长度x在ans的左边时,切割出来的段数 t >= k
当枚举的切割长度x在ans的右边时,切割出来的段数 t < k
所以这道题答案是具有二段性的,所以可以通过二分来解决
left mid right
calc(mid)计算当前切割长度能切多少段
calc(mid) >= k left = mid;
calc(mid) < k right = mid - 1;
这时候后面的一个代码实现就变得简单很多了
代码实现:
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
LL n, k;
LL a[N];
LL calc(LL x)
{
//通过切割长度x计算切出的数量
LL ret = 0;
for (int i = 1; i <= n; i++)
{
ret += a[i] / x;
}
return ret;
}
int main()
{
cin >> n >> k;
LL r = 0;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
r = max(r, a[i]);
}
LL l = 0;
while(l < r)
{
LL mid = (l + r + 1) / 2;
if (calc(mid) >= k) l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}
4.砍树
题目展示:
思路分析:
这道题跟前面的那道题是很相似的大家看完前一道题后做这道题应该难度不会很大了。
但是现在还是跟着博主的思路开始分析一下这道题吧:
这道题给出了这一排树的数目n,所需的木材m两个数,接着给出了每棵树的高度a[i]
我们需要找到一个合适的高度既能够保护树,也能满足需求的木材。
那么我们的高度就只能是在0到最高的树的高度来枚举了
如果我们最后答案的树高为ans
----------------------------------------------
[0 ans maxh]
----------------------------------------------
当h小于ans时,获得的木材 l >= m
当h大于ans时,获得的木材 l < m
答案是具有二段性的,去思考使用二分答案来解决
left mid right
当枚举树高为mid,calc(mid)为此时获得的木材
当calc(mid) >= m时,l = mid
当calc(mid) < m时,r = mid - 1
代码实现:
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;
LL n, m;
LL a[N];
LL calc(LL x)
{
// 计算出x树高的时候,切割出来的木材数
LL ret = 0;
for (int i = 1; i <= n; i++)
{
if (a[i] >= x) ret += a[i] - x;
}
return ret;
}
int main()
{
cin >> n >> m;
LL r = 0;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
r = max(r, a[i]);
}
LL l = 0;
while(l < r)
{
LL mid = (l + r + 1) / 2;
if (calc(mid) >= m) l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}
5.跳石头
题目展示:
思路分析:
这道题题目可能刚看会有些不明白是怎么一回事,可以仔细看看
现在跟着博主的思路开始分析一下这道题目吧:
题目中会给出起点到终点的距离L,起点和终点之间的岩石数N,以及组委会至多移走的岩石数M
然后会给出N个值a[i],每个值a[i]代表第i个石头距离起点的距离。
题目要我们求的是什么呢?让我们求最短跳跃距离的最大值?
这是不是就是很符合我们二分答案吗?
那么什么叫做最短跳跃距离的最大值呢?
由于移走石头后选手会进行每块石头之间的跳跃,在所有跳跃中,会出现一个最短的跳跃距离
但是我们想要通过移走石头来使得选手这个最短跳跃距离达到最大
最终要求出这个最短跳跃距离。
对于最短跳跃距离x,和移走石头数c
当x增加,c也在增加
当x减小,c也在减少
-----------------------------------------------------------------
[1 ans L]
-----------------------------------------------------------------
当x <= ans时,移走的岩石小于等于 m
当x > ans时,移走的岩石c肯定大于 m (规定移走的岩石数)
left mid right
calc(mid)计算在最小跳跃距离为mid的时候,移走的石头数
calc(mid) <= m 在跳跃mid距离时,移走石头小于等于m left = mid
calc(mid) > m 在跳跃mid距离时,移走石头大于m right = mid - 1
那么问题来了?如何计算当最小跳跃距离为mid的时候,移走的石头数呢?
这个地方我们可以通过双指针的方法来实现它:
一个指针i指向前一个石头,另外一个指针j向后查找,通过a[j] - a[i] < mid就让j++
当出现a[j] - a[i] >= mid就停止向后查找,因为根据题目可以知道向后会越来越大
然后通过i和j的下标计算出来中间移走的石头数量 j - i - 1
最后更新i的大小,i = j - 1 (因为在for循环里面还有一个i++) 让i更新指向j的位置
然后继续执行逻辑,最后得出这种情况的移走石头数量。
这样我们就可以去实现我们的代码了!
代码实现:
#include <iostream>
using namespace std;
const int N = 5e4 + 10;
typedef long long LL;
LL l, n, m;
LL a[N];
LL calc(LL x)
{
//计算最短跳跃距离为x的时候,要移动的石头数
LL ret = 0;// 记录石头数
// 遍历石头
for (int i = 0; i < n; i++)
{
int j = i + 1;
while(j <= n && a[j] - a[i] < x) j++;
ret += j - i - 1;
i = j - 1;
}
return ret;
}
int main()
{
cin >> l >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i]; // 岩石距离起点距离
}
a[n + 1] = l;//避免计算的错误
n++;
LL left = 1, right = l;
while(left < right)
{
LL mid = (left + right + 1) / 2;
if (calc(mid) <= m) left = mid;
else right = mid - 1;
}
cout << left << endl;
return 0;
}
结语
很高兴大家能够看到这里,希望前面内容的讲解能够给大家带来收获,弄懂二分答案的原理和使用场景,对于最后这道题可能比较难以理解,但是大家可以仔细思考思考。
如果这篇文章对您有所收获的话,请多多点赞 + 收藏 + 关注!