系列文章目录
如果你对二分查找/二分答案有疑问,请看这篇博客
【算法笔记】二分查找 && 二分答案 (超详细解析,一篇让你搞懂二分)
话不多说,直接上题解
文章目录
- 系列文章目录
- [P1678 烦恼的高考志愿](https://www.luogu.com.cn/problem/P1678)
- [P1163 银行贷款](https://www.luogu.com.cn/problem/P1163)
- [P8647 [蓝桥杯 2017 省 AB] 分巧克力](https://www.luogu.com.cn/problem/P8647)
- [P1024 [NOIP2001 提高组] 一元三次方程求解](https://www.luogu.com.cn/problem/P1024)
- [P2440 木材加工](https://www.luogu.com.cn/problem/P2440)
- [P1577 切绳子](https://www.luogu.com.cn/problem/P1577)
- [P2678 [NOIP2015 提高组] 跳石头](https://www.luogu.com.cn/problem/P2678)
- [1460. 我在哪?](https://www.acwing.com/problem/content/1462/)
- [102. 最佳牛围栏](https://www.acwing.com/problem/content/description/104/)
P1678 烦恼的高考志愿
二分查找典题,将志愿中的学校分数排序,然后循环遍历每一位学生的分数,找到第一个>=其分数的学校,然后分别计算该学校和该学校的前一个学校与分数的差,取min即可,注意要特判一下,如果该学生的分数比志愿中分最低的学校还要低,就直接输出差值,否则答案会错
还有,注意,最后的和res会超过int的范围,要开long long
每天一遍:
不开long long
见祖宗
ACcode
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
using LL = long long;
const int N = 1e5 + 10;
int m, n; //m:学校数 n:学生数
int a[N], b[N]; //a[]:学校录取分数线 b[]:学生估分
int main(){
cin >> m >> n;
for (int i = 1; i <= m; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
cin >> b[i];
sort(a + 1, a + m + 1); // 二分查找之前一定不要忘了排序
LL res = 0; //不满意度总和
for (int i = 1; i <= n; i++){ // 遍历每个学生的估分
int l = 0, r = m + 1; // 初始化二分查找的左右边界,l 为 0,r 为 m + 1
while(l < r){
int mid = l + r >> 1;
if(a[mid] <= b[i]) // 如果中间位置的分数线小于等于当前学生的估分
l = mid + 1; // 将左边界移动到 mid + 1
else // 否则
r = mid; // 将右边界移动到 mid
}
if(b[i] <= a[1]) // 如果当前学生的估分小于等于最小的分数线
res += a[1] - b[i]; // 将不满意度累加为最小分数线与估分的差值
else
res += min(abs(a[l - 1] - b[i]), abs(a[l] - b[i])); // 计算并累加最小的不满意度(此时l==r,两者可以替换)
}
cout << res;
return 0;
}
P1163 银行贷款
题目大意:给出
n,m,k
,求贷款者向银行支付的利率p
,使得∑ i = 1 k m ( 1 + p ) i = n \begin{equation} \sum_{i=1}^{k} \frac{m}{(1+p)^i} = n \end{equation} i=1∑k(1+p)im=n
按百分比形输出,精确到小数点后一位
运用浮点二分的模板,二分答案即可
ACcode
#include<iostream>
using namespace std;
int w0, w, m;// 分别表示初始金额,每月支付金额和月数
double sum;// 用于记录每月后的剩余金额
int check(double mid)
{
sum = w0;// 用初始金额初始化sum
for (int i = 1; i <= m; i++) // 循环 m 次,模拟 m 个月的还款过程
{
sum = sum + sum * mid - w;
}
if (sum > 0) // 如果最后金额大于 0,说明利率太高,无法在 m 个月内还清
return true;
return false;
}
int main()
{
cin >> w0 >> w >> m;
double l = 0, r = 500; // 答案范围尽量开大些
while (r - l > 1e-5) // 精度保证
{
double mid = (l + r) / 2;
if (check(mid))
r = mid;
else
l = mid;
}
printf("%.1f", l * 100);//保留小数用printf
return 0;
}
P8647 [蓝桥杯 2017 省 AB] 分巧克力
由于答案具有单调性,因此考虑使用二分答案。
根据题目描可以得到:若当前的边长为x
,呢么对第i
块巧克力,可以切出
⌊ h i x ⌋ × ⌊ w i x ⌋ \left\lfloor \frac{h_i}{x} \right\rfloor \times \left\lfloor \frac{w_i}{x} \right\rfloor ⌊xhi⌋×⌊xwi⌋
块边长为x
的巧克力,因此可以进行二分答案,用总共可以切出的巧克力个数作为check
,
如果>=k
,则可行,否则不可行。
ACcode
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, k; // n表示巧克力块数,k表示小朋友人数
int h[N]; // 高度
int w[N]; // 宽度
// 检查是否可以切出至少k块边长为x的正方形巧克力
bool check(int x)
{
int sum = 0; // 定义变量sum,统计可以切出的正方形巧克力数
// 遍历每块巧克力
for (int i = 1; i <= n; i++)
// 计算每块巧克力可以切出的边长为x的正方形数目,并累加到sum
sum += (h[i] / x) * (w[i] / x);
// 如果总数sum大于或等于k,返回true;否则返回false
if (sum >= k)
return true;
else
return false;
}
int main()
{
cin >> n >> k;
for (int i = 1; i <= n; i++)
cin >> h[i] >> w[i];
int l = 1, r = N; // 左右边界
// 二分答案
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid))
l = mid;
else
r = mid - 1;
}
cout << l;
return 0;
}
P1024 [NOIP2001 提高组] 一元三次方程求解
很恶心也很无脑的一道题,数据范围甚至可以暴力枚举…当然如果你知道那个什么
著名的卡尔丹公式,你也可以直接套公式。这里给出二分解法–也就是找函数的零点
和一次函数求零点相似,不过三次函数有可能有三个零点,因此我们对[-100, 100]内200个长度为1的区间进行二分答案
ACcode
#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
// 系数
double a, b, c, d;
// 计算f(x)函数值
double f(double x) {
return a * x * x * x + b * x * x + c * x + d;
}
int main() {
cin >> a >> b >> c >> d;
// 从 -100 到 99 遍历每一个整数作为区间的左端点
for (int i = -100; i <= 99; i++) {
double l = i, r = i + 1;
// 如果左端点的函数值为零,则输出这个根
if (f(l) == 0)
printf("%.2lf ", l);
// 检查区间两端的函数值是否有符号变化
if (f(l) * f(r) >= 0)
continue; // 没有符号变化则跳过
// 使用二分法在区间 [l, r] 内找根
while (r - l >= 0.001) {
double mid = (l + r) / 2; // 计算中点
if (f(l) * f(mid) <= 0)
r = mid; // 根在左半区间
else
l = mid; // 根在右半区间
}
// 输出找到的根,精确到小数点后两位
printf("%.2lf ", l);
}
return 0;
}
P2440 木材加工
二分答案,对于
check
,我们依次要判断a
中的每一个数并计算出能切出多少个mid
,用一个变量res
储存他们,如果res
比k
多或者正好等于,返回true
。如果是小于k
,返回false
。
ACcode
#include <iostream>
#include <algorithm>
using namespace std;
using LL = long long;
const int N = 1e6 + 10;
LL n, k; //n:原木数量 k:需要的小段数量
LL a[N]; // 存储每根原木的长度
// 检查能否通过切割长度为 x 的小段得到至少 k 段
bool check(LL x) {
LL res = 0; // 定义一个变量 res,记录可以切割得到的小段数量
for (int i = 1; i <= n; i++) { // 遍历每一根原木
res += a[i] / x; // 将当前原木能够切割出的长度为 x 的小段数量累加到 res
}
return res >= k; // 返回是否可以得到至少 k 段长度为 x 的小段
}
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++)
cin >> a[i];
LL l = 0, r = 100000001; // 定义二分查找的左右边界,l 为 0,r 为 100000001(比 10^8 大 1)
while (l < r) { // 当左边界小于右边界时
LL mid = l + r + 1 >> 1;
if (check(mid)) // 如果长度为 mid 的小段可以得到至少 k 段
l = mid; // 更新左边界为 mid
else
r = mid - 1; // 否则更新右边界为 mid - 1
}
cout << l << endl; // 输出最大的可以得到的长度
return 0;
}
P1577 切绳子
这题和上一道题思路很像,就是把砍树换成了切绳子,也是二分答案,不同的的是这题数据是浮点数,可以直接用double类型进行二分,但有时会遇到精度的问题,比较麻烦,这里我用另一种方法–
将浮点数*100转化成整数,然后对整数进行二分
ACcode
#include <iostream>
#include <algorithm>
using namespace std;
using LL = long long;
const int N = 10005;
LL n, k, s[N], sum;
double a[N], res; //a:每条绳子的长度 s:每条绳子的长度*100
// 检查给定的长度mid是否能切割出至少k条绳子
bool check(LL mid)
{
LL ans = 0; // 初始化计数器ans
for (int i = 1; i <= n; i++) // 遍历每条绳子
ans = ans + s[i] / mid; // 累加可以切割出的绳子数量
return ans >= k; // 返回是否可以切割出至少k条绳子
}
int main()
{
cin >> n >> k;
for (int i = 1; i <= n; i++){
cin >> a[i];
s[i] = a[i] * 100; // 转换为整数存储,乘以100以避免小数
sum += s[i]; // 累加总长度
}
LL l = 0, r = sum / k + 1; // 初始化二分查找的左右边界
while (l < r)
{
LL mid = l + r + 1 >> 1;
if (check(mid)) //可以切割mid条,将l右移
l = mid;
else
r = mid - 1; //否则将r左移
}
res = r * 1.00 / 100; // 别忘了把结果转换回双精度浮点数,不然就WA
printf("%.2lf\n", res); // 输出结果,保留两位小数
return 0;
}
P2678 [NOIP2015 提高组] 跳石头
二分答案很经典的一道题,求最大的最小跳跃距离,和上篇文章的砍树类似,我记得今年蓝桥杯国赛B组就出了一道和这道题类似的典题。
我们对于一个长度x,想看看它是否可以符合删除石头数小于等于k,按照贪心的思想:可以这样做:
从位置的小到大扫遍所有石头,用一个变量存储上一个跳到的点。第一个与这上一个点的距离大于等于k的石头即是下一个跳到的点。如果不能跳到下一个石头,则移除当前石头,并增加移除计数。
贪心证明:
- 假设我们不跳到第一个符合条件的石头,而是跳到更远的某个石头,这意味着我们跳过了一些中间的石头。
- 跳过这些中间的石头意味着这些石头与前一个石头的距离都小于 D,所以我们必须移除这些石头。
- 如果我们跳到更远的石头,可能会导致移除的石头数量超过允许的最大移除数M
- 这样做会导致无法保证最小跳跃距离 D,从而无法得到正确的答案。
ACcode
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 50010;
int a[N]; // 存储石头位置
int s, n, m, ans; // s:起点到终点的距离,n:石头数量,m:最多可以移除的石头数,ans:答案
// 检查是否可以在移除不超过m块石头的情况下,确保最小跳跃距离至少为k
int check(int k)
{
int last = 0; // 上一个石头的位置,初始化为起点位置0
int cnt = 0; // 记录移除的石头数量
for (int i = 1; i <= n; ++i)
{
if (a[i] - last < k) // 如果当前石头和上一个石头之间的距离小于k,则移除当前石头
cnt++;
else
last = a[i]; // 否则更新上一个石头的位置
}
return cnt <= m; // 返回是否能够在移除不超过m块石头的情况下确保最小跳跃距离至少为k
}
int main()
{
cin >> s >> n >> m;
for (int i = 1; i <= n; ++i)
cin >> a[i];
n++; // 将终点作为最后一个石头
a[n] = s; // 将终点的位置放入数组中
int l = 0, r = s; // 二分查找的左右边界
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) // 检查是否可以在移除不超过m块石头的情况下,确保最小跳跃距离至少为mid
l = mid; // 如果可以,更新左边界
else
r = mid - 1; // 否则,更新右边界
}
cout << l; // 输出结果,即最大的最小跳跃距离
return 0;
}
1460. 我在哪?
这题难度比较高,但是到很好的题,解决方法很多,可以用字符串哈希加二分求解,如果小白没学过一些算法可以先跳过,等修得正果再回来看
因为解决方式很多,我这里就不一一列了,详情请看 题解传送门
102. 最佳牛围栏
通过二分查找最大平均牛数,然后用前缀和来判断是否存在一个连续的至少包含F块田地的子数组,其牛的数量的平均值大于或等于当前中间值。
这里还要强调一个概念:子数组
对于萌新,子数组和子序列的概念应该比较模糊,这里做一下区分
首先,看定义
子数组:数组中的一个连续部分,这部分中的元素必须是连续的
子序列:从数组中按顺序提取的元素,这些元素不必须是连续的
其次,举个栗子
对于数组a[]={1,3,2,10,4,7,11}
它的子数组可以是{1,3,2}
{2,10,4,7}
{1,3,2,10,4,7}
但不可一是{1,2,10}
它的子序列可以是{1,3,2} {2,10,4,7}
也可以是{1,2,10} {1,4,11}
,但不可以是{3,1,2} {7,2,1}
序列序列,当然要按顺序
很显然,只要他是子数组,他就一定是子序列;但如果他是子序列,它不一定是子数组------由此可见:子数组是子序列的子集
ACcode
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, F; // n:田地数量,F:至少围起的田地数量
double a[N], s[N]; // a数组存储每块田地的牛数量,s数组用于存储前缀和减去平均值后的值
// 定义check函数,用于判断是否存在一个长度至少为F的子数组,其平均值大于等于x
bool check(double x)
{
// 计算前缀和,减去x
for (int i = 1; i <= n; i++)
s[i] = s[i - 1] + a[i] - x;
double mins = 0; // 初始化最小前缀和为0
// 遍历所有长度至少为F的子数组
for (int k = F; k <= n; k++)
{
mins = min(mins, s[k - F]); // 更新最小前缀和
// 如果当前前缀和大于等于最小前缀和,说明存在一个子数组平均值大于等于x
if (s[k] >= mins)
return true;
}
return false; // 否则,不存在这样的子数组
}
int main()
{
// 读取输入的n和F
scanf("%d%d", &n, &F);
double l = 0, r = 0; // 初始化二分查找的左右边界
for (int i = 1; i <= n; i++)
{
// 读取每块田地的牛数量
scanf("%lf", &a[i]);
// 更新右边界为最大牛数量
r = max(r, a[i]);
}
// 二分查找
while (r - l > 1e-5)
{
double mid = (l + r) / 2; // 计算中间值
// 检查是否存在子数组平均值大于等于mid
if (check(mid))
l = mid; // 如果存在,更新左边界
else
r = mid; // 否则,更新右边界
}
// 输出结果,最大平均值乘以1000并向下取整
printf("%d\n", (int)(r * 1000));
return 0;
}
其实只要搞清楚那几个细节(见上篇文章),记好板子,做几道题,你就会发现,二分其实真的不难。毕竟这是基础算法 仅靠这几道题还不够,去刷题吧,伟大的OIer 和ACMer!
传送门:洛谷 ACwing 牛客 codeforces …