知识点扫盲:二分答案

本条博客以及本系列用于记录算法学习中不熟悉或少见的知识点

一.关于二分答案的来源和应用

  1. 由于本大一 == 蒟蒻 == 没有经过系统性的学习,在一场牛客小白月赛中首次听说二分答案这一操作,十分震惊其效率与作用,遂写此篇来记录

  2. 关于二分答案有关的概念[二分法]"https://oi-wiki.org//basic/binary/#二分法"

  3. 简单来说:解题的时候往往会考虑枚举答案然后检验枚举的值是否正确。若满足单调性,则满足使用二分法的条件。把这里的枚举换成二分,就变成了「二分答案」。

  4. 需要注意:二分法的应用场景:满足单调性或需要最大值最小化。
    注意,这里的有序是广义的有序,如果一个数组中的左侧或者右侧都满足某一种条件,而另一侧都不满足这种条件,也可以看作是一种有序(如果把满足条件看做 1,不满足看做 0,至少对于这个条件的这一维度是有序的)。换言之,二分搜索法可以用来查找满足某种条件的最大(最小)的值。

要求满足某种条件的最大值的最小可能情况(最大值最小化),首先的想法是从小到大枚举这个作为答案的「最大值」,然后去判断是否合法。若答案单调,就可以使用二分搜索法来更快地找到答案。因此,要想使用二分搜索法来解这种「最大值最小化」的题目,需要满足以下/三个条件/:

1.答案在一个固定区间内;

2.可能查找一个符合条件的值不是很容易,但是要求能比较容易地判断某个值是否是符合条件的;

3.可行解对于区间满足一定的单调性。换言之,如果 x 是符合条件的,那么有 x + 1 或者 x - 1 也符合条件。(这样下来就满足了上面提到的单调性)
当然,最小值最大化是同理的。

二、经典例题(难度不大)

e.g1. [洛谷p2440木材加工]"https://www.luogu.com.cn/problem/P2440"

e.g2. [P4058 [Code+#1] 木材]"https://www.luogu.com.cn/problem/P4058"

[Code+#1] 木材

题目描述

有 $n$ 棵树,初始时每棵树的高度为 $H_i$,第 $i$ 棵树每月都会长高 $A_i$。现在有个木料长度总量为 $S$ 的订单,客户要求每块木料的长度不能小于 $L$,而且木料必须是整棵树(即不能为树的一部分)。现在问你最少需要等多少个月才能满足订单。

输入格式

第一行 $3$ 个用空格隔开的非负整数 $n,S,L$,表示树的数量、订单总量和单块木料长度限制。

第二行 $n$ 个用空格隔开的非负整数,依次为 $H_1,H_2, ... ,H_n$。

第三行 $n$ 个用空格隔开的非负整数,依次为 $A_1,A_2, ... ,A_n$。

输出格式

输出一行一个整数表示答案。

样例 #1

样例输入 #1

3 74 51
2 5 2
2 7 9

样例输出 #1

7

提示

对于样例,在六个月后,各棵树的高度分别为 $ 14,47,56 $,此时无法完成订单。

在七个月后,各棵树的高度分别为 $ 16,54,65 $,此时可以砍下第 $ 2 $ 和第 $ 3 $ 棵树完成订单了。

以下为eg2的代码及本人在2024.11.10因两个测试点数十次wonderful answer 后的经验与教训

#include<bits/stdc++.h>//需要对n==1时进行特判 #define N 200005 #define mod 998244353 #define int unsigned long long using namespace std; typedef long long ull; int n,s,l; //priority_queue<int,vector<int>,greater<int> >q; int a[N],b[N],c[N]; bool check(int x) { int b2 = x; for(int i = 1; i <= n;i++) { c[i] = x*b[i] + a[i]; } int sum = 0; for(int i = 1;i <= n;i++) { if(c[i] >= l) sum += c[i]; } if(sum >= s) return true; else return false; } void solve() { cin>>n>>s>>l; for(int i = 1;i <= n;i++) cin>>a[i]; for(int i = 1;i <= n;i++) cin>>b[i]; if(n == 1) { c[1] = a[1]; if(s <= a[1]) { cout<<0; } else { ull ans = 0; ans = (s-a[1])/b[1]; cout<<ans<<"\n"; } return ; } int l = 0,r = 1e18;//此处应注意,l不设置-1的原因是我使用了ull while(l < r) { int mid = (l+r)/2; if(check(mid)) r = mid; else l = mid+1; } cout<<l<<"\n"; } signed main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); solve(); return 0; }

image.png

image.png

但是关于此处,本人还是不太理解,为何将l<=r与r = mid-1 改为 l < r 与 r = mid就能将tle的#16过掉 (是宇宙射线

e.g3 [小红打怪]"https://ac.nowcoder.com/acm/contest/94879/C"

/本题为萌新较难看出来的二分答案其实是因为我太蒟蒻/

注意力不太集中的同学也可以观察到,因为本题单调性显然成立,或者说,满足

需要将最大值最小化,假设最少第x次可以将所有怪物击杀,那么第x+1显然能

将所有怪物击杀,因此可以使用二分答案的方式进行处理;

贪心有关的内容:先欠着

容易得到代码

#include<bits/stdc++.h> #define N 200005 #define mod 998244353 #define int long long//恶毒的写法,不要轻易使用 using namespace std; typedef long long ll; int n,a[N],b[N]; bool check(int x) { int b2 = x; for(int i = 1;i <= n;i++) { b[i] = max(a[i]-b2,0ll);//因为怪物血量不能为0 } int t = 0x3f3f3f; for(int i =1;i <= n;i++) { t = min(b[i],b[i+1]); b2 -= t; b[i] -=t; b[i+1] -=t; } b2 +=x;//易得,若相邻的次数有剩,则剩下的即为单体打击 for(int i =1 ;i <= n;i++) { b2 -=b[i]; } return b2>=0;//最后若为真,则说明目前的回合数位于x右侧 } void solve() { cin>>n; for(int i = 1;i <= n;i++) cin>>a[i]; int l=0,r = 1e9;//这里的1e9与数据范围有关 while(l <= r) { int mid = (l+r)/2; if(check(mid)) r = mid-1; else l = mid+1; } cout<<l<<"\n"; } signed main() { ios::sync_with_stdio(false);//关闭流同步 cin.tie(nullptr);//此处注意 cout.tie(nullptr);//若报错,则说明c++版本过低,~~建议更换c++20或c++23~~ solve(); return 0; }

posted @ 2024-11-10 23:06  graspppp  阅读(373)  评论(0)    收藏  举报