二分答案的应用

二分答案的基本定义

二分答案(Binary Search on Answer) 是一种算法思想,它适用于一类答案具有单调性答案范围明确的问题。其核心思想是:不直接求解答案,而是在一个可能的答案区间内进行二分搜索,通过设计一个判定函数(check function) 来检验当前猜测的答案是否可行,从而将搜索范围减半,最终逼近或确定最优解。

核心要素

  1. 答案的单调性:这是二分答案能够成立的前提。它通常表现为:

    • 可行性单调:如果某个值 x 是可行的(满足题目条件),那么所有大于(或小于)x 的值也一定可行(或一定不可行)。
    • 最优性单调:我们寻找的最大(或最小)可行解,其可行性随 x 的变化是单调的。
  2. 明确的搜索区间:答案的可能范围 [left, right] 必须是已知且有限的。leftright 的初始值通常由题目条件或数据范围决定。

  3. 判定函数 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;
}

这三种模式分别对应:

  1. 精确查找:判断是否存在
  2. 左边界查找:寻找第一个满足条件的索引
  3. 右边界查找:寻找最后一个满足条件的索引

在二分答案中,check(mid) 函数的返回值(true/false)通常决定了搜索方向是向左收缩(right = mid - 1)还是向右收缩(left = mid + 1),这与上述模式中的判断逻辑一脉相承。

1 寻找峰值

1.1 题目描述

在这里插入图片描述

1.2 思路解析

本题要求时间复杂度为 O(log n),因此不能使用简单的 O(n) 遍历。我们可以利用二分答案的思想来寻找峰值。

核心思路

  1. 边界处理

    • 如果数组长度为 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] 内必然存在至少一个峰值。
  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] 同时大于左右邻居,即为峰值,记录答案并退出循环。
  3. 返回结果:最终记录的 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 内吃完所有香蕉的最小速度。由于速度与所需时间之间存在单调关系(速度越快,所需时间越少),我们可以使用二分答案来求解。

核心思路

  1. 确定搜索区间

    • 速度的最小可能值(左边界 l)为 1(每小时至少吃 1 根)。
    • 速度的最大可能值(右边界 r)为香蕉堆中的最大值(即一次吃完最大的一堆所需的速度)。
    • 这样,答案一定在区间 [1, max(piles)] 内。
  2. 设计判定函数 f(speed)

    • 函数功能:计算以给定速度 speed 吃完所有香蕉所需的总时间。
    • 关键计算:对于每一堆香蕉数量 a,吃完它需要的时间为 ⌈a / speed⌉(向上取整)。
    • 向上取整的实现:(a + speed - 1) / speedaspeed 均为正数)。
    • 返回所有堆的时间总和。
  3. 二分搜索(寻找最小可行速度)

    • 在区间 [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 思路解析

本题要求找到机器人能够成功完成所有跳跃的最小初始能量值。由于初始能量值越大,通关的可能性越大(单调性),我们可以使用二分答案来求解。

核心思路

  1. 确定搜索区间

    • 初始能量的最小可能值(左边界 l)为 1(至少需要 1 点能量)。
    • 初始能量的最大可能值(右边界 r)为所有建筑高度的最大值 max(如果初始能量等于最高建筑高度,则一定能通过)。
    • 这样,答案一定在区间 [1, max] 内。
  2. 设计判定函数 f(energy)

    • 函数功能:模拟机器人从给定的初始能量 energy 开始,依次经过每个建筑,判断能否成功到达终点。
    • 关键规则:当机器人到达第 i 个建筑(高度为 heights[i])时:
      • 如果当前能量 energy 大于等于建筑高度,则跳跃后剩余能量为 energy + (energy - heights[i]) = 2 * energy - heights[i]
      • 如果当前能量 energy 小于建筑高度,则跳跃后剩余能量为 energy - (heights[i] - energy) = 2 * energy - heights[i]
      • 重要发现:无论 energyheights[i] 的大小关系如何,跳跃后的能量计算公式统一为 energy = 2 * energy - heights[i]
    • 模拟过程中的剪枝优化:
      • 如果某次跳跃后 energy >= max(最大建筑高度),则后续无论怎样跳跃都一定能成功,可直接返回 true
      • 如果某次跳跃后 energy < 0,则能量耗尽,无法继续,返回 false
    • 如果成功模拟完所有建筑,返回 true
  3. 二分搜索(寻找最小可行初始能量)

    • 在区间 [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 个画匠,每个画匠只能画连续相邻的画,且所有画匠能力相同(单位时间完成的工作量相同)。目标是找到完成所有画作所需的最短时间(即最小化最大子数组和)。这是一个典型的“最小化最大值”问题,可以使用二分答案求解。

核心思路

  1. 确定搜索区间

    • 最短时间的下界(左边界 l)为 0(理论上可能为 0,但实际至少为 1)。
    • 最短时间的上界(右边界 r)为所有画作所需时间的总和 sum(即一个画匠完成所有画的时间)。
    • 这样,答案一定在区间 [0, sum] 内。
  2. 设计判定函数 f(aim)

    • 函数功能:给定一个最大子数组和上限 aim,计算最少需要多少个画匠(即最少分成多少段连续子数组)才能满足“每段子数组的和不超过 aim”的条件。
    • 关键逻辑:
      • 遍历数组,累加当前子数组的和 sum
      • 如果 sum + 当前画作时间 > aim,说明当前子数组已满,需要新开一个画匠(即新开一段),parts++,并将 sum 重置为当前画作时间。
      • 如果单个画作时间 num > aim,说明即使单独分配给一个画匠也无法在 aim 时间内完成,直接返回 Integer.MAX_VALUE 表示不可能。
    • 函数返回最少需要的画匠数量 parts
  3. 二分搜索(寻找最小可行时间)

    • 在区间 [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

核心思路

  1. 预处理:先将数组 nums 排序。排序后,数对距离 |nums[i] - nums[j]| 的计算和统计会更高效。
  2. 确定搜索区间
    • 最小可能距离(左边界 l)为 0(两个相同元素的距离)。
    • 最大可能距离(右边界 r)为 nums[n-1] - nums[0](排序后首尾元素的差值)。
    • 答案(第 k 小的距离)一定在区间 [0, nums[n-1] - nums[0]] 内。
  3. 设计判定函数 f(limit)
    • 函数功能:计算在排序后的数组中,有多少个数对的距离不超过 limit
    • 关键算法(双指针/滑动窗口):
      • 对于每个固定的左指针 l0 ≤ l < n),找到最大的右指针 rr ≥ l),使得 nums[r] - nums[l] ≤ limit
      • 由于数组已排序,当 l 右移时,nums[l] 增大,满足 nums[r] - nums[l] ≤ limitr 不会向左移动(单调性),因此可以用双指针在 O(n) 时间内计算。
      • 对于每个 l,满足条件的右端点 r 的范围是 [l+1, r],因此以 l 为左端点的、距离不超过 limit 的数对数量为 r - l
      • 累加所有 l 对应的 r - l,即为总数对数量。
  4. 二分搜索(寻找第 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 台电脑能够同时运行的最长时间。每台电脑需要持续供电,电池可以给任意电脑充电,但一旦开始给某台电脑充电,就不能中途切换给其他电脑。我们可以使用二分答案来求解。

核心思路

  1. 确定搜索区间

    • 最小可能时间(左边界 l)为 0(电脑运行时间为 0)。
    • 最大可能时间(右边界 r)为所有电池电量的总和 sum 除以电脑数量 n(即 sum / n),但实际计算中我们可以用电池中的最大电量 max 作为上界,因为单个电池最多只能给一台电脑供电。
    • 这样,答案一定在区间 [0, max] 内。
  2. 设计判定函数 f(time)

    • 函数功能:判断是否能让 n 台电脑同时运行 time 小时。
    • 关键策略:将电池分为两类处理:
      • 整体电池:电量 ≥ time 的电池。每个这样的电池可以单独让一台电脑运行完整的 time 小时。每分配一个整体电池,所需电脑数量减 1。
      • 碎片电池:电量 < time 的电池。这些电池不能单独支撑一台电脑运行 time 小时,但可以组合使用。
    • 判定条件:经过整体电池分配后,设剩余需要供电的电脑数量为 remain。如果所有碎片电池的总电量 ≥ remain × time,则说明可以用这些碎片电池组合起来为剩余的 remain 台电脑供电 time 小时,返回 true;否则返回 false
  3. 二分搜索(寻找最大可行时间)

    • 在区间 [l, r] 内进行二分搜索。
    • 对于中间时间 m,调用 f(m) 进行判定:
      • 如果 f(m) == true:说明当前时间 m 可行。为了寻找最大可行时间,我们尝试向右搜索更大的时间,即令 l = m + 1,并记录当前答案 ans = m
      • 如果 f(m) == false:说明当前时间 m 不可行,需要尝试更小的时间,即令 r = m - 1
    • 循环直到 l > r,最后记录的 ans 即为最大同时运行时间。
  4. 性能优化(剪枝)

    • 如果所有电池的总电量 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 回合内能够杀死怪兽。由于回合数越多,杀死怪兽的可能性越大(单调性),我们可以使用二分答案来求解。

核心思路

  1. 确定搜索区间

    • 最小可能回合数(左边界 l)为 1(至少需要 1 回合)。
    • 最大可能回合数(右边界 r)为 hp + 1(最坏情况下,每回合只能造成 1 点伤害)。
    • 这样,答案一定在区间 [1, hp + 1] 内。
  2. 设计判定函数 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
  3. 二分搜索(寻找最小可行回合数)

    • 在区间 [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();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值