文章目录
二分答案的基本定义
二分答案(Binary Search on Answer) 是一种算法思想,它适用于一类答案具有单调性且答案范围明确的问题。其核心思想是:不直接求解答案,而是在一个可能的答案区间内进行二分搜索,通过设计一个判定函数(check function) 来检验当前猜测的答案是否可行,从而将搜索范围减半,最终逼近或确定最优解。
核心要素
-
答案的单调性:这是二分答案能够成立的前提。它通常表现为:
- 可行性单调:如果某个值
x是可行的(满足题目条件),那么所有大于(或小于)x的值也一定可行(或一定不可行)。 - 最优性单调:我们寻找的最大(或最小)可行解,其可行性随
x的变化是单调的。
- 可行性单调:如果某个值
-
明确的搜索区间:答案的可能范围
[left, right]必须是已知且有限的。left和right的初始值通常由题目条件或数据范围决定。 -
判定函数
check(mid):这是二分答案的灵魂。对于当前猜测的中间值mid,该函数需要能在多项式时间内判断mid是否满足题目的约束条件,并返回true(可行)或false(不可行)。判定结果将指导搜索方向的收缩。
算法框架(寻找最小可行解为例)
初始化 left, right
while (left <= right) {
mid = left + (right - left) / 2
if (check(mid) == true) {
// mid 可行,答案可能在 [left, mid] 中,尝试寻找更小的可行解
right = mid - 1
ans = mid // 记录当前可行解
} else {
// mid 不可行,答案只可能在 [mid + 1, right] 中
left = mid + 1
}
}
返回 ans
与普通二分搜索的区别
| 特性 | 普通二分搜索 | 二分答案 |
|---|---|---|
| 搜索对象 | 有序数组中的具体元素 | 满足条件的最优数值解 |
| 判定依据 | 直接与数组元素比较大小 | 调用自定义的、可能复杂的 check(mid) 函数 |
| 应用场景 | 查找、边界查找 | 最优化问题(如“最小最大值”、“最大最小值”)、可行性问题 |
典型应用场景
- “最小化最大值”问题:例如,将数组分成 k 段,使得最大段的和最小。
- “最大化最小值”问题:例如,在数轴上放置 k 个点,使得任意两点间的最小距离最大。
- 在单调函数中找根。
- 满足某种条件的最小/最大整数。
简单来说,二分答案是将二分搜索的“比较大小”升级为“检验一个复杂的条件是否成立”,从而解决更广泛的最优化问题。
基础二分搜索回顾(三种经典模式)
在理解二分答案之前,需要熟练掌握以下三种基础二分搜索模式,它们是二分答案中 check(mid) 函数判断逻辑的基础。
1. 在有序数组中查找目标值是否存在
// 前提:数组是升序的
public static int find(int[] arr, int target) {
int n = arr.length;
int l = 0, r = n - 1;
while (l <= r) {
int mid = l + (r - l) / 2; // 防溢出
if (arr[mid] > target) {
r = mid - 1; // 目标在左半部分
} else if (arr[mid] < target) {
l = mid + 1; // 目标在右半部分
} else {
return mid; // 找到目标
}
}
return -1; // 不存在
}
2. 在有序数组中查找大于等于 target 的最左位置
// 前提:数组是升序的
public static int findLeft(int[] arr, int target) {
int n = arr.length;
int l = 0, r = n - 1;
int ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (arr[mid] >= target) {
// 记录一个答案,继续向左搜索更左的位置
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
return ans;
}
3. 在有序数组中查找小于等于 target 的最右位置
// 前提:数组是升序的
public static int findRight(int[] arr, int target) {
int n = arr.length;
int ans = -1;
int l = 0, r = n - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (arr[mid] <= target) {
// 记录一个答案,继续向右搜索更右的位置
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return ans;
}
这三种模式分别对应:
- 精确查找:判断是否存在
- 左边界查找:寻找第一个满足条件的索引
- 右边界查找:寻找最后一个满足条件的索引
在二分答案中,check(mid) 函数的返回值(true/false)通常决定了搜索方向是向左收缩(right = mid - 1)还是向右收缩(left = mid + 1),这与上述模式中的判断逻辑一脉相承。
1 寻找峰值
1.1 题目描述

1.2 思路解析
本题要求时间复杂度为 O(log n),因此不能使用简单的 O(n) 遍历。我们可以利用二分答案的思想来寻找峰值。
核心思路:
-
边界处理:
- 如果数组长度为 1,直接返回下标 0。
- 检查下标 0:如果
arr[0] > arr[1],则 0 就是峰值,直接返回。 - 检查下标 n-1:如果
arr[n-1] > arr[n-2],则 n-1 就是峰值,直接返回。 - 经过以上检查,可以确定:
arr[0] < arr[1](左边上扬),arr[n-1] < arr[n-2](右边下降)。根据数学性质,在区间 [1, n-2] 内必然存在至少一个峰值。
-
二分搜索(在区间 [1, n-2] 内):
- 计算中点
m。 - 如果
arr[m-1] > arr[m]:- 说明在
m左侧形成了“左上右下”的趋势(即arr[m-1]是高点,arr[m]是低点),峰值一定在左侧区间[l, m-1]中,令r = m-1。
- 说明在
- 否则如果
arr[m] < arr[m+1]:- 说明在
m右侧形成了“左上右下”的趋势(即arr[m]是低点,arr[m+1]是高点),峰值一定在右侧区间[m+1, r]中,令l = m+1。
- 说明在
- 否则(即
arr[m-1] < arr[m]且arr[m] > arr[m+1]):arr[m]同时大于左右邻居,即为峰值,记录答案并退出循环。
- 计算中点
-
返回结果:最终记录的
ans即为峰值下标。
题目地址:寻找峰值
1.3 代码实现
class Solution {
public static int findPeakElement(int[] arr){
int n = arr.length;
if(n==1){
return 0;
}
//验证0位置
if(arr[0]>arr[1]){
return 0;
}
//验证n-1位置
if(arr[n-1]>arr[n-2]){
return n-1;
}
//左边上扬 右边下降 中间必有
int l = 1,r = n-2,m=0,ans = -1;
while(l<=r){
m = l +(r-l)/2;
//只搜一遍 左边
if(arr[m-1]>arr[m]){
r = m-1;
}
//右边
else if(arr[m]<arr[m+1]){
l = m+1;
}
else{
ans = m;
break;
}
}
return ans;
}
}
2 爱吃香蕉的珂珂
2.1 题目描述

2.2 思路解析
本题要求找到珂珂在规定时间 h 内吃完所有香蕉的最小速度。由于速度与所需时间之间存在单调关系(速度越快,所需时间越少),我们可以使用二分答案来求解。
核心思路:
-
确定搜索区间:
- 速度的最小可能值(左边界
l)为 1(每小时至少吃 1 根)。 - 速度的最大可能值(右边界
r)为香蕉堆中的最大值(即一次吃完最大的一堆所需的速度)。 - 这样,答案一定在区间
[1, max(piles)]内。
- 速度的最小可能值(左边界
-
设计判定函数
f(speed):- 函数功能:计算以给定速度
speed吃完所有香蕉所需的总时间。 - 关键计算:对于每一堆香蕉数量
a,吃完它需要的时间为⌈a / speed⌉(向上取整)。 - 向上取整的实现:
(a + speed - 1) / speed(a和speed均为正数)。 - 返回所有堆的时间总和。
- 函数功能:计算以给定速度
-
二分搜索(寻找最小可行速度):
- 在区间
[l, r]内进行二分搜索。 - 对于中间速度
m,计算f(m):- 如果
f(m) <= h:说明当前速度m可行(能在规定时间内吃完)。为了寻找最小可行速度,我们尝试向左搜索更小的速度,即令r = m - 1,并记录当前答案ans = m。 - 如果
f(m) > h:说明当前速度太慢,无法在规定时间内吃完。需要尝试更大的速度,即令l = m + 1。
- 如果
- 循环直到
l > r,最后记录的ans即为最小可行速度。
- 在区间
单调性保证:速度 speed 越大,所需时间 f(speed) 越小(或不变),因此 f(speed) <= h 这个条件关于 speed 是单调的(存在一个分界点,左侧不可行,右侧可行),这正是二分答案能够应用的前提。
题目地址:爱吃香蕉的珂珂
2.3 代码实现
class Solution {
public int minEatingSpeed(int[] piles, int h) {
int n = piles.length;
// 先求l r的可能的区间
int l = 1;
int r = 0;
for(int a : piles){
r = Math.max(r,a);
}
int ans = 0;
int m = 0;
while(l<=r){
m = l+(r-l)/2;
//速度满足的最左位置 速度的单调性 speed越大 h花费越小
if(f(piles,m)<=h){
ans = m;
r = m-1;
}
else{
l = m+1;
}
}
return ans;
}
//时间向上取整
//返回吃完香蕉的时间
public static long f(int[] piles, int speed){
long ans = 0;
for(int a: piles){
//向上取整a/b a b 都是正数 (a+b-1)/b
ans += (a+speed-1)/speed;
}
return ans;
}
}
3机器人跳跃问题
3.1 题目描述

3.2 思路解析
本题要求找到机器人能够成功完成所有跳跃的最小初始能量值。由于初始能量值越大,通关的可能性越大(单调性),我们可以使用二分答案来求解。
核心思路:
-
确定搜索区间:
- 初始能量的最小可能值(左边界
l)为 1(至少需要 1 点能量)。 - 初始能量的最大可能值(右边界
r)为所有建筑高度的最大值max(如果初始能量等于最高建筑高度,则一定能通过)。 - 这样,答案一定在区间
[1, max]内。
- 初始能量的最小可能值(左边界
-
设计判定函数
f(energy):- 函数功能:模拟机器人从给定的初始能量
energy开始,依次经过每个建筑,判断能否成功到达终点。 - 关键规则:当机器人到达第
i个建筑(高度为heights[i])时:- 如果当前能量
energy大于等于建筑高度,则跳跃后剩余能量为energy + (energy - heights[i]) = 2 * energy - heights[i]。 - 如果当前能量
energy小于建筑高度,则跳跃后剩余能量为energy - (heights[i] - energy) = 2 * energy - heights[i]。 - 重要发现:无论
energy与heights[i]的大小关系如何,跳跃后的能量计算公式统一为energy = 2 * energy - heights[i]。
- 如果当前能量
- 模拟过程中的剪枝优化:
- 如果某次跳跃后
energy >= max(最大建筑高度),则后续无论怎样跳跃都一定能成功,可直接返回true。 - 如果某次跳跃后
energy < 0,则能量耗尽,无法继续,返回false。
- 如果某次跳跃后
- 如果成功模拟完所有建筑,返回
true。
- 函数功能:模拟机器人从给定的初始能量
-
二分搜索(寻找最小可行初始能量):
- 在区间
[l, r]内进行二分搜索。 - 对于中间能量值
m,调用f(m)进行判定:- 如果
f(m) == true:说明当前能量m可行。为了寻找最小可行能量,我们尝试向左搜索更小的能量,即令r = m - 1,并记录当前答案ans = m。 - 如果
f(m) == false:说明当前能量不足,需要尝试更大的能量,即令l = m + 1。
- 如果
- 循环直到
l > r,最后记录的ans即为最小可行初始能量。
- 在区间
单调性分析:初始能量值 energy 越大,机器人通过所有建筑的可能性越大(或不变),因此 f(energy) 函数关于 energy 是单调的(存在一个分界点,左侧不可行,右侧可行)。这满足了二分答案的应用条件。
题目地址:机器人跳跃问题
3.3 代码实现
import java.io.*;
public class Main {
public static int[] heights;
public static int n,l,r,ans,max;
public static int compute(){
ans = -1;
while(l<=r){
int m = l+(r-l)/2;
if(f(m)){
ans = m;
r = m-1;
}
else {
l = m+1;
}
}
return ans;
}
public static boolean f(int energy){
for(int i=0;i<n;i++){
/* if(energy<=arr[i]){
energy-=(arr[i]-energy);
}
else {
energy+=(energy-arr[i]);
}
if(energy>=max){
return true;
}*/
energy = 2*energy- heights[i];
if(energy>=max){
return true;
}
else if(energy<0){
return false;
}
}
return true;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer st = new StreamTokenizer(br);
PrintWriter pw = new PrintWriter(System.out);
st.nextToken(); n = (int) st.nval;
heights = new int[n];
max = Integer.MIN_VALUE;
for(int i=0;i<n;i++){
st.nextToken();
heights[i] = (int) st.nval;
max = Math.max(max, heights[i]);
}
l=1;
r=max;
pw.print(compute());
pw.flush();
pw.close();
br.close();
}
}
4 画匠问题
4.1题目描述
有k个画匠 有n幅画 每幅画需要的画arr[i] 小时 画家只能选择相邻的画 画匠的能力一样 问把画全部画完需要的最短时间

4.2 思路解析
本题要求将 n 幅画分配给 k 个画匠,每个画匠只能画连续相邻的画,且所有画匠能力相同(单位时间完成的工作量相同)。目标是找到完成所有画作所需的最短时间(即最小化最大子数组和)。这是一个典型的“最小化最大值”问题,可以使用二分答案求解。
核心思路:
-
确定搜索区间:
- 最短时间的下界(左边界
l)为 0(理论上可能为 0,但实际至少为 1)。 - 最短时间的上界(右边界
r)为所有画作所需时间的总和sum(即一个画匠完成所有画的时间)。 - 这样,答案一定在区间
[0, sum]内。
- 最短时间的下界(左边界
-
设计判定函数
f(aim):- 函数功能:给定一个最大子数组和上限
aim,计算最少需要多少个画匠(即最少分成多少段连续子数组)才能满足“每段子数组的和不超过aim”的条件。 - 关键逻辑:
- 遍历数组,累加当前子数组的和
sum。 - 如果
sum + 当前画作时间 > aim,说明当前子数组已满,需要新开一个画匠(即新开一段),parts++,并将sum重置为当前画作时间。 - 如果单个画作时间
num > aim,说明即使单独分配给一个画匠也无法在aim时间内完成,直接返回Integer.MAX_VALUE表示不可能。
- 遍历数组,累加当前子数组的和
- 函数返回最少需要的画匠数量
parts。
- 函数功能:给定一个最大子数组和上限
-
二分搜索(寻找最小可行时间):
- 在区间
[l, r]内进行二分搜索。 - 对于中间时间
m,调用f(m)计算所需的最少画匠数量need:- 如果
need <= k:说明当前时间m可行(k个画匠足够完成)。为了寻找最小可行时间,我们尝试向左搜索更小的时间,即令r = m - 1,并记录当前答案ans = m。 - 如果
need > k:说明当前时间m太小,需要更多画匠才能完成,即时间不足。需要尝试更大的时间,即令l = m + 1。
- 如果
- 循环直到
l > r,最后记录的ans即为最短完成时间。
- 在区间
单调性分析:最大子数组和上限 aim 越大,所需的最少画匠数量 f(aim) 越少(或不变),因此 f(aim) <= k 这个条件关于 aim 是单调的(存在一个分界点,左侧不可行,右侧可行)。这满足了二分答案的应用条件。
题目地址:画匠问题
4.3 代码实现
public static int splitArray(int[] nums, int k) {
int sum = 0;
for(int a: nums){
sum+=a;
}
int ans = 0;
for(int l=0,r=sum,m,need;l<=r;){
m = l+(r-l)/2;
need = f2(nums,m);
//没超过 记录答案 向左搜
if(need<=k){
ans = m;
r = m-1;
}
else{
l = m+1;
}
}
return ans;
}
//固定数组和不能超过aim 的数量
public static int f2(int[] nums, int aim){
int parts = 1;
int sum = 0;
for(int num: nums){
//单个超过 不可能
if(num>aim){
return Integer.MAX_VALUE;
}
// 超过part++ sum下一个重新赋值
if(sum+num>aim){
parts++;
sum = num;
}
//没超 进来
else {
sum += num;
}
}
return parts;
}
5 找出第 K 小的数对距离
5.1 题目描述

5.2 思路解析
本题要求找出所有数对距离中第 k 小的距离。数对距离定义为 |nums[i] - nums[j]|(其中 i < j)。直接计算所有 n*(n-1)/2 个数对的距离并排序会超时(O(n² log n))。我们可以利用二分答案的思想,将问题转化为:寻找一个距离 limit,使得距离不超过 limit 的数对数量恰好大于等于 k 的最左 limit。
核心思路:
- 预处理:先将数组
nums排序。排序后,数对距离|nums[i] - nums[j]|的计算和统计会更高效。 - 确定搜索区间:
- 最小可能距离(左边界
l)为 0(两个相同元素的距离)。 - 最大可能距离(右边界
r)为nums[n-1] - nums[0](排序后首尾元素的差值)。 - 答案(第
k小的距离)一定在区间[0, nums[n-1] - nums[0]]内。
- 最小可能距离(左边界
- 设计判定函数
f(limit):- 函数功能:计算在排序后的数组中,有多少个数对的距离不超过
limit。 - 关键算法(双指针/滑动窗口):
- 对于每个固定的左指针
l(0 ≤ l < n),找到最大的右指针r(r ≥ l),使得nums[r] - nums[l] ≤ limit。 - 由于数组已排序,当
l右移时,nums[l]增大,满足nums[r] - nums[l] ≤ limit的r不会向左移动(单调性),因此可以用双指针在O(n)时间内计算。 - 对于每个
l,满足条件的右端点r的范围是[l+1, r],因此以l为左端点的、距离不超过limit的数对数量为r - l。 - 累加所有
l对应的r - l,即为总数对数量。
- 对于每个固定的左指针
- 函数功能:计算在排序后的数组中,有多少个数对的距离不超过
- 二分搜索(寻找第 k 小的距离):
- 在区间
[l, r]内进行二分搜索。 - 对于中间距离
m,调用f(m)计算距离不超过m的数对数量cnt:- 如果
cnt >= k:说明距离m可能是答案(因为我们要找的是第k小的距离,而cnt表示不超过m的数对数量已经达到k个,所以第k小的距离一定 ≤m)。为了寻找最小的满足条件的m(即最左位置),我们尝试向左搜索更小的距离,即令r = m - 1,并记录当前答案ans = m。 - 如果
cnt < k:说明距离m太小,不超过m的数对数量不足k个,第k小的距离一定大于m。需要尝试更大的距离,即令l = m + 1。
- 如果
- 循环直到
l > r,最后记录的ans即为第k小的数对距离。
- 在区间
单调性分析:距离上限 limit 越大,满足 距离 ≤ limit 的数对数量 f(limit) 越多(或不变),因此 f(limit) >= k 这个条件关于 limit 是单调的(存在一个分界点,左侧不满足,右侧满足)。这满足了二分答案的应用条件。
题目地址:找出第 K 小的数对距离
5.3 代码实现
class Solution {
public static int smallestDistancePair(int[] nums, int k) {
int n = nums.length;
Arrays.sort(nums);
//在0 max-min 上二分
int ans = 0;
for(int l=0,r = nums[n-1]-nums[0],m,cnt;l<=r;){
m = l+(r-l)/2;
cnt = f3(nums,m);
if(cnt>=k){
ans = m;
r = m-1;
}
else {
l = m+1;
}
}
return ans;
}
//arr数组中任意的差值小limit 的数对的数量
public static int f3(int[] nums, int limit){
int ans = 0;
for(int l=0,r =0;l<nums.length;l++){
while(r+1<nums.length&&nums[r+1]<=nums[l]+limit){
r++;
}
ans+=r-l;
}
return ans;
}
}
6 同时运行 N 台电脑的最长时间
6.1 题目描述

6.2 思路解析
本题要求找到 n 台电脑能够同时运行的最长时间。每台电脑需要持续供电,电池可以给任意电脑充电,但一旦开始给某台电脑充电,就不能中途切换给其他电脑。我们可以使用二分答案来求解。
核心思路:
-
确定搜索区间:
- 最小可能时间(左边界
l)为 0(电脑运行时间为 0)。 - 最大可能时间(右边界
r)为所有电池电量的总和sum除以电脑数量n(即sum / n),但实际计算中我们可以用电池中的最大电量max作为上界,因为单个电池最多只能给一台电脑供电。 - 这样,答案一定在区间
[0, max]内。
- 最小可能时间(左边界
-
设计判定函数
f(time):- 函数功能:判断是否能让
n台电脑同时运行time小时。 - 关键策略:将电池分为两类处理:
- 整体电池:电量
≥ time的电池。每个这样的电池可以单独让一台电脑运行完整的time小时。每分配一个整体电池,所需电脑数量减 1。 - 碎片电池:电量
< time的电池。这些电池不能单独支撑一台电脑运行time小时,但可以组合使用。
- 整体电池:电量
- 判定条件:经过整体电池分配后,设剩余需要供电的电脑数量为
remain。如果所有碎片电池的总电量≥ remain × time,则说明可以用这些碎片电池组合起来为剩余的remain台电脑供电time小时,返回true;否则返回false。
- 函数功能:判断是否能让
-
二分搜索(寻找最大可行时间):
- 在区间
[l, r]内进行二分搜索。 - 对于中间时间
m,调用f(m)进行判定:- 如果
f(m) == true:说明当前时间m可行。为了寻找最大可行时间,我们尝试向右搜索更大的时间,即令l = m + 1,并记录当前答案ans = m。 - 如果
f(m) == false:说明当前时间m不可行,需要尝试更小的时间,即令r = m - 1。
- 如果
- 循环直到
l > r,最后记录的ans即为最大同时运行时间。
- 在区间
-
性能优化(剪枝):
- 如果所有电池的总电量
sum > (long) max * n,那么最大运行时间可以直接计算为sum / n。这是因为即使最大的电池也只能给一台电脑供电,但总电量足够平均分配给所有电脑更长时间。
- 如果所有电池的总电量
单调性分析:运行时间 time 越大,满足 f(time) == true 的难度越大(或不变),因此 f(time) 函数关于 time 是单调的(存在一个分界点,左侧可行,右侧不可行)。这满足了二分答案的应用条件。
题目地址:同时运行 N 台电脑的最长时间
6.3 代码实现
class Solution {
public static long maxRunTime(int n, int[] batteries) {
long ans = 0,sum = 0;
int max = 0;
for(int a: batteries){
max = Math.max(a,max);
sum+=a;
}
if(sum>(long)max*n){
return sum/n;
}
for(int l=0,r = max,m;l<=r;){
m = l+(r-l)/2;
if(f4(batteries,n,m)){
ans = m;
l = m+1;
}
else{
r = m-1;
}
}
return ans;
}
//让n太电脑同时运行 time
public static boolean f4(int[] nums, int n,int time){
//碎片时间的总和
long sum = 0;
for(int a: nums){
if(a>=time){
n--;
}
else{
sum+=a;
}
}
if(sum>=(long)time*n){
return true;
}
return false;
}
}
7 刀砍毒杀怪兽
7.1题目描述

7.2 思路解析
本题要求找到杀死怪兽所需的最少回合数。怪兽有初始血量 hp,我们有 n 种攻击方式,第 i 种攻击方式有两种效果:
- 刀砍:立即造成
cut[i]点伤害 - 毒杀:在当前回合不造成伤害,但在后续每个回合造成
poisons[i]点伤害(持续效果)
我们需要找到最小的回合数 m,使得在 m 回合内能够杀死怪兽。由于回合数越多,杀死怪兽的可能性越大(单调性),我们可以使用二分答案来求解。
核心思路:
-
确定搜索区间:
- 最小可能回合数(左边界
l)为 1(至少需要 1 回合)。 - 最大可能回合数(右边界
r)为hp + 1(最坏情况下,每回合只能造成 1 点伤害)。 - 这样,答案一定在区间
[1, hp + 1]内。
- 最小可能回合数(左边界
-
设计判定函数
f(limit):- 函数功能:判断是否能在
limit回合内杀死怪兽。 - 关键策略:
- 我们最多只能使用
min(n, limit)种攻击方式(因为每回合只能使用一种攻击方式,且总回合数为limit)。 - 对于第
i种攻击方式(i从 0 开始),在第j回合使用(j从 1 开始到limit):- 刀砍效果:立即造成
cut[i]点伤害 - 毒杀效果:在后续
(limit - j)个回合中,每回合造成poisons[i]点伤害,总伤害为(limit - j) * poisons[i]
- 刀砍效果:立即造成
- 对于每个攻击方式,我们选择刀砍和毒杀效果中的较大值作为该攻击方式在
limit回合内的总伤害贡献。 - 计算所有攻击方式的总伤害,判断是否 ≥
hp。
- 我们最多只能使用
- 函数功能:判断是否能在
-
二分搜索(寻找最小可行回合数):
- 在区间
[l, r]内进行二分搜索。 - 对于中间回合数
m,调用f(m)进行判定:- 如果
f(m) == true:说明当前回合数m可行(能在m回合内杀死怪兽)。为了寻找最小可行回合数,我们尝试向左搜索更小的回合数,即令r = m - 1,并记录当前答案ans = m。 - 如果
f(m) == false:说明当前回合数m不可行,需要尝试更大的回合数,即令l = m + 1。
- 如果
- 循环直到
l > r,最后记录的ans即为最少所需回合数。
- 在区间
单调性分析:回合数 limit 越大,能够造成的总伤害越多(或不变),因此 f(limit) == true(能在 limit 回合内杀死怪兽)这个条件关于 limit 是单调的(存在一个分界点,左侧不可行,右侧可行)。这满足了二分答案的应用条件。
题目地址:刀砍毒杀怪兽
7.3 代码实现
import java.io.*;
public class Main {
//刀砍cut 毒杀p 数组 n 怪兽的血量hp 刀砍直接减 毒杀当前回合不会减 后内回合减去毒杀的血量 放回至少多少个回合怪兽会死
//f函数不超过 m回合看怪兽能不能死
// 1<=n<=10^5 1<=hp<=10^9 1<=cut[i],poisons[i]<=10^9
public static int hp,n;
public static int[] cut,poisons;
public static int fast2(int[]cuts,int[] poisons,int hp){
int ans = Integer.MAX_VALUE;
for(int l=1,r = hp+1,m;l<=r;){
m = l+(r-l)/2;
if(f6(cuts,poisons,hp,m)){
ans = m;
r = m-1;
}
else {
l = m+1;
}
}
return ans;
}
//cut poisons 数组 刀砍 毒杀的效果
//hp怪兽的血量 limit 回合的轮数
public static boolean f6(int[] cuts,int[] poisons,long hp,int limit){
int n = Math.min(cuts.length,limit);
for(int i=0,j=1;i<n;i++,j++){
//回合确定 看谁大
hp-=Math.max((long)cuts[i],(long)(limit-j)*poisons[i]);
if(hp<=0){
return true;
}
}
return false;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer st = new StreamTokenizer(br);
PrintWriter pw = new PrintWriter(System.out);
st.nextToken(); hp = (int) st.nval;
st.nextToken(); n = (int) st.nval;
cut = new int[n];
poisons = new int[n];
for(int i=0;i<n;i++){
st.nextToken();cut[i] = (int) st.nval;
st.nextToken();poisons[i] = (int) st.nval;
}
pw.println(fast2(cut,poisons,hp));
pw.flush();
pw.close();
br.close();
}
}
698

被折叠的 条评论
为什么被折叠?



