Skip to content

二分答案法


解题思路

核心点:分析单调性、建立 f 函数

估计最终答案可能的范围是什么,可以定的粗略,反正二分不了几次

分析问题的答案给定条件之间的单调性,大部分时候只需要用到自然智慧

建立一个 f 函数,当答案固定的情况下,判断给定的条件是否达标

最终答案可能的范围上不断二分搜索,每次用 f 函数判断,直到二分结束,找到最合适的答案

⚠️ 注意

这个技巧常用且重要,一定要引起重视,非常的美、精妙!以后的课还会经常见到

爱吃香蕉的珂珂

题目链接

https://leetcode.cn/problems/koko-eating-bananas/

思路分析

首先答案范围是可以确定的,且有单调性,吃的速度越快,花的实践越少,建立 f 函数判断每个答案是否合法,在答案的范围中二分,找到本题的答案返回即可

⭐ 向上取整的写法;(a + b - 1)/ b

代码实现

java
class Solution {
    // 时间复杂度 O(n * log(max)),额外空间复杂度 O(1)
    public int minEatingSpeed(int[] piles, int h) {
        // 定义答案范围 [l,r]
        // l 最小且达标的速度
        // r 最大且达标的速度
        int l = 1;
        int r = 0;
        for (int pile : piles) {
            r = Math.max(r, pile);
        }
        // 在答案范围内不停二分
        int ans = 0;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            // 如果当前速度满足,尝试更小的速度
            // 记答案,往左二分
            if (f(piles, mid) <= h) {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return ans;
    }

    // 香蕉重量都在 piles 中,速度定成 speed
    // 返回吃完所有的香蕉,耗费的最小时数量
    public long f(int[] piles, int speed) {
        long ans = 0;
        for (int pile : piles) {
            // a / b 向上取整可以写成 (a + b - 1) / b
            ans += (pile + speed - 1) / speed;
        }
        return ans;
    }
}

分割数组的最大值

题目链接

https://leetcode.cn/problems/split-array-largest-sum/

思路分析

本题是画匠问题,将一个数组划分成 k 份且连续,有很多不同的划分方法,不同的画匠分别负责相邻的划分部分且不能跨越,最后完成工作的结果取决于在这么多划分方法中,两部分累加和较大的取尽可能小的值

例如将数组 [ 6,4,5,5 ],划分成两份,以下仅举出两个例子

第一种划分:[ 6 ]、[ 4,5,5 ],累加和为 6,14

第二种划分:[ 6,4 ]、[ 5,5 ],累加和为 10,10

目的是两部分累加和较大的取尽可能小的值,所以第二种划分方式是符合要求的

本题采用逆向思维,根据累加和来判定能否划分成 k 个,找到答案那个累加和

定于 f 函数,给定 limit,表示每个划分部分的累加和不超过 limit,返回需要划分成几个部分

单调性体现在当 limit 变大,即每一部分划分的累加和变多了,那需要划分的部分就变少

二分逻辑:当前给定的 limit,如果返回需要划分部分的数量小于 k,则说明每个划分部分的累加和太大了,导致了最后需要划分部分的数量变少,此时需要缩小每个划分部分的累加和,使得需要划分部分的数量增大,以达到题目要求的 k 值

反之就增大 limit,使得每个划分部分的累加和变大,缩小需要划分部分的数量

代码实现

java
class Solution {
    // 时间复杂度 O(n * log(sum)),额外空间复杂度 O(1)
    public int splitArray(int[] nums, int k) {
        long sum = 0;
        for (int num : nums) {
            sum += num;
        }
        // 定义二分范围 [0, sum],题目给的数据量偏大,采用 long 类型
        // 从答案的角度出发,寻找符合题目要求的答案
        long ans = 0;
        long l = 0;
        long r = sum;
        while (l <= r) {
            long mid = l + ((r - l) >> 1);
            // 每部分累加和为 mid 的条件下,需要划分成几部分
            long need = f(nums, mid);
            // 此情况说明每部分的累加和太大了,才导致需要划分的部分太少
            // 记答案,缩小每部分的累加和,增大需要划分的部分,往左二分
            if (need <= k) {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return (int) ans;
    }

    // 给定 limit 表示每个划分部分的累加必须 <= limit
    // 返回整个数组需要划分成几个部分才能满足这个条件
    public int f(int[] arr, long limit) {
        int parts = 1;
        int sum = 0;
        for (int num : arr) {
            // 返回整数最大值表示无法划分
            if (num > limit) {
                return Integer.MAX_VALUE;
            }
            if (sum + num > limit) {
                parts++;
                sum = num;
            } else {
                sum += num;
            }
        }
        return parts;
    }
}

机器人跳跃问题

题目链接

https://www.nowcoder.com/practice/7037a3d57bbd4336856b8e16a9cafd71

思路分析

题意:当前能量比下一个柱子的高度大,就加上差值,否则减去差值

初始能量越大越可能通关,因为一路都是加下去的,反而越小越吃亏,即初始能量有范围,在上面二分答案

定义 f 函数,给定一个能量和数组的最大值,判断该能量是否能通关,如果能量还有剩余,说明可以通关

⚠️ 需要注意的是如果数组全是 1,则每次都会累加 2n,增长过快以致于可能会在 long 类型的情况下溢出,加上判断,如果当前能量大于数组的最大值,结束累加,此时是可以通关的

代码实现

java
import java.io.*;

public class Main {

    public static int MAXN = 100001;

    public static int[] arr = new int[MAXN];

    public static int n;

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
        while (in.nextToken() != StreamTokenizer.TT_EOF) {
            n = (int) in.nval;
            int l = 0;
            int r = 0;
            for (int i = 1; i <= n; i++) {
                in.nextToken();
                arr[i] = (int) in.nval;
                r = Math.max(r, arr[i]);
            }
            out.println(compute(l, r, r));
        }
        out.flush();
        out.close();
        br.close();
    }

    // 时间复杂度 O(n * log(sum)),额外空间复杂度 O(1)
    // [l,r] 范围内的初始能量,找到可以通关的初始能量
    public static int compute(int l, int r, int max) {
        int ans = -1;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            // 当前的初始能量可以通关,记答案
            // 尝试寻找更小的初始能量,往左二分
            if (f(mid, max)) {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return ans;
    }

    // 给定初始能量值 energy,判断能否通关
    public static boolean f(int energy, int max) {
        // 如果数组都是 1,则每次累加 2^n ,可能会导致溢出
        // 这也是传入 max 的原因,如果能量大于 max,结束累加
        for (int i = 1; i <= n; i++) {
            if (energy <= arr[i]) {
                energy -= arr[i] - energy;
            } else {
                energy += energy - arr[i];
            }
            // 特殊判断,能量都超过了最大值,肯定通关
            if (energy >= max) {
                return true;
            }
            if (energy < 0) {
                return false;
            }
        }
        return true;
    }
}

找出第 K 小的数对距离

题目链接

https://leetcode.cn/problems/find-k-th-smallest-pair-distance/

思路分析

首先差值的范围是 [ 0,最大 - 最小 ],所以首先要对数组排序,然而第 k 小的距离必然在这个范围上

定义 f 函数,传入 limit,表示差值,返回差值为 limit 的数对有几对,在差值范围上不断二分,最终一定能找到差值为某个值,而这样的数对有 k 个

代码实现

java
class Solution {
    // 时间复杂度 O(n * log(n) + log(max-min) * n),额外空间复杂度 O(1)
    public int smallestDistancePair(int[] nums, int k) {
        int n = nums.length;
        Arrays.sort(nums);
        int ans = 0;
        // 在 [0,最大 -最小] 范围上不断二分
        int l = 0;
        int r = nums[n - 1] - nums[0];
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            // 差值为 mid 的数对有几对
            int cnt = f(nums, mid);
            // 够 k 个,则 mid 右边的差值可定也符合条件
            // 往左二分,看是否还有符合 k 的差值,继续二分
            if (cnt >= k) {
                ans = mid;
                r = mid - 1;
            } else {
                // 不够 k 个,说明差值定小了,往右二分
                l = mid + 1;
            }
        }
        return ans;
    }

    // 给定差值 limit,返回差值 <= limit 的数字对有几对
    public int f(int[] arr, int limit) {
        int ans = 0;
        for (int l = 0, r = 0; l < arr.length; l++) {
            // 滑动窗口技巧,每次以 l 位置为基准
            // 判断后面的数与 l 位置的数的差值是否 <= limit
            // 如果 > limit,停止扩大窗口,计算以 l 位置为基准
            // 符合条件的数对有几对,累加答案
            while (r + 1 < arr.length && arr[r + 1] - arr[l] <= limit) {
                r++;
            }
            // 例如数组 arr[0...3],差值对数如下
            // (0,1)位置一对、(0,2)位置一对,(0,3)位置一对
            // 1 开头的哪些等 l++ 之后才计算
            ans += r - l;
        }
        return ans;
    }
}

同时运行 N 台电脑的最长时间

题目链接

https://leetcode.cn/problems/maximum-running-time-of-n-computers/

思路分析

定义碎片电池:电池的电量小于要求的运行时间

⭐ 结论:如果全是碎片电池,碎片电池的电量总和 >= 要求的运行时间 * 电脑台数,则一定可以达成要求的运行时间

对于电池电量 > 要求的运行时间的电池,可以先不考虑,排除了这些不考虑的电池后,如果剩余的电池都能达成条件,那这些电池就能达成电脑要求的运行时间

单调性的体现:让 n 台电脑运行 x 分钟更易,运行 x + 1 分钟更难

定义 f 函数,只考虑碎片电池的总电量能否使得电脑运行 x 分钟

二分的逻辑:如果这些电池的总电量足以实现要求运行 x 分钟,那么看看运行 x + 1 分钟及更多分钟能否达成,不断调整二分的区间,用答案来验证是否满足条件,满足条件的一定是答案

代码实现

java
class Solution {
    // 原来这里是 int n, int[] batteries
    // 这里改成 int num, int[] arr
    // 时间复杂度 O(n * log(sum)),额外空间复杂度 O(1)
    public long maxRunTime(int num, int[] arr) {
        long sum = 0;
        for (int x : arr) {
            sum += x;
        }
        long ans = 0;
        // 在 [0,sum] 范围上二分
        long l = 0;
        long r = sum;
        while (l <= r) {
            long mid = l + ((r - l) >> 1);
            // 如果电量足以让 num 台电脑运行 mid 的时间
            // 往右二分,看看 mid + 1及更多的时间能否运行
            if (f(arr, num, mid)) {
                ans = mid;
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return ans;
    }

    // 只关注碎片电池,不考虑电池电量 > 要求运行时间的电池
    // 碎片电池的总电量能否使得 num 台电脑运行 time 的时间
    public boolean f(int[] arr, int num, long time) {
        // 碎片电池电量总和
        long sum = 0;
        for (int x : arr) {
            if (x > time) {
                num--;
            } else {
                // x <= time,是碎片电池
                sum += x;
            }
            // 核心结论的运用
            if (sum >= (long) num * time) {
                return true;
            }
        }
        return false;
    }

}

优化实现

贪心优化,找到最大的电池电量 max,如果 sum > max * num,对于 num 台电脑,每台电脑需要供电 max 分钟,需要的总电量都 < sum,则说明供电时间一定 >= max

既然 max <= 需要的供电时间,那说明所有电池都是碎片电池,那供电时间不就是 sum / num

此时二分范围从 [ 0,sum ] 优化到了 [ 0,max ],二分跑的更快

java
class Solution {
    // 原来这里是 int n, int[] batteries
    // 这里改成 int num, int[] arr
    // 时间复杂度 O(n * log(sum)),额外空间复杂度 O(1)
    public long maxRunTime(int num, int[] arr) {
        int max = 0;
        long sum = 0;
        for (int x : arr) {
            max = Math.max(max, x);
            sum += x;
        }
        // 贪心优化
        if (sum > (long) max * num) {
            // 所有电池的最大电量是max
            // 如果此时sum > (long) max * num,
            // 说明 : 最终的供电时间一定在 >= max,而如果最终的供电时间 >= max
            // 说明 : 对于最终的答案X来说,所有电池都是课上讲的"碎片拼接"的概念
            // 那么寻找 ? * num <= sum 的情况中,尽量大的 ? 即可
            // 即sum / num
            return sum / num;
        }

        // 此时二分范围从 [0,sum] 优化到了 [0,max],跑的更快
        int ans = 0;
        int l = 0;
        int r = max;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            if (f(arr, num, mid)) {
                ans = mid;
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return ans;
    }

    public boolean f(int[] arr, int num, int time) {
        // 碎片电池电量总和
        long sum = 0;
        for (int x : arr) {
            if (x > time) {
                num--;
            } else {
                sum += x;
            }
            if (sum >= (long) num * time) {
                return true;
            }
        }
        return false;
    }
}

计算等位时间

题目描述

给定一个数组 arr 长度为 n,表示 n 个服务员,每服务一个人的时间

给定一个正数 m,表示有 m 个人等位,如果你是刚来的人,每个客人都遵循有空位就上的原则,请问你需要等多久?

假设 m 远远大于 n,比如 n <= 103,m <= 109,该怎么做是最优解?

谷歌的面试,这个题连考了 2 个月

思路分析

假设所有服务员都服务 x 的时间,看能否服务 m + 1 个人(自己是第 m + 1 位),等到自己,可能没开始服务,但是要求能够服务到自己

设定服务的时间范围:[ 0, min * n ],假设所有人都选择服务时间较短的服务员,则自己至少要等 min * n 的时间,这个范围可以很粗,也二分不了几次

在给定的时间范围上二分,找到可以满足服务 m + 1 个人的时间

代码实现

java
class Solution {
    public int waitingTime(int[] arr, int w) {
        int min = Integer.MAX_VALUE;
        for (int x : arr) {
            min = Math.min(min, x);
        }
        int ans = 0;
        int l = 0;
        // 至少等这么久才轮到自己
        int r = min * w;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            // 当前时间足以服务 w + 1 个人
            // 往左二分,寻找更短的等待时间
            if (f(arr, mid) >= w + 1) {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return ans;
    }

    // 如果每个服务员都服务 time 的时间,可以接待
    // 几位客人(结束的,开始的客人都要算)
    public int f(int[] arr, int time) {
        int ans = 0;
        for (int num : arr) {
            // 要算上自己,所以 +1
            ans += (time / num) + 1;
        }
        return ans;
    }
}

对数器验证

java
package class051;

import java.util.PriorityQueue;

// 计算等位时间
// 给定一个数组arr长度为n,表示n个服务员,每服务一个人的时间
// 给定一个正数m,表示有m个人等位,如果你是刚来的人,请问你需要等多久?
// 假设m远远大于n,比如n <= 10^3, m <= 10^9,该怎么做是最优解?
// 谷歌的面试,这个题连考了2个月
// 找不到测试链接,所以用对数器验证
public class Code06_WaitingTime {

	// 堆模拟
	// 验证方法,不是重点
	// 如果m很大,该方法会超时
	// 时间复杂度O(m * log(n)),额外空间复杂度O(n)
	public static int waitingTime1(int[] arr, int m) {
		// 一个一个对象int[]
		// [醒来时间,服务一个客人要多久]
		PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> (a[0] - b[0]));
		int n = arr.length;
		for (int i = 0; i < n; i++) {
			heap.add(new int[] { 0, arr[i] });
		}
		for (int i = 0; i < m; i++) {
			int[] cur = heap.poll();
			cur[0] += cur[1];
			heap.add(cur);
		}
		return heap.peek()[0];
	}

	// 二分答案法
	// 最优解
	// 时间复杂度O(n * log(min * w)),额外空间复杂度O(1)
	public static int waitingTime2(int[] arr, int w) {
		int min = Integer.MAX_VALUE;
		for (int x : arr) {
			min = Math.min(min, x);
		}
		int ans = 0;
		for (int l = 0, r = min * w, m; l <= r;) {
			// m中点,表示一定要让服务员工作的时间!
			m = l + ((r - l) >> 1);
			// 能够给几个客人提供服务
			if (f(arr, m) >= w + 1) {
				ans = m;
				r = m - 1;
			} else {
				l = m + 1;
			}
		}
		return ans;
	}

	// 如果每个服务员工作time,可以接待几位客人(结束的、开始的客人都算)
	public static int f(int[] arr, int time) {
		int ans = 0;
		for (int num : arr) {
			ans += (time / num) + 1;
		}
		return ans;
	}

	// 对数器测试
	public static void main(String[] args) {
		System.out.println("测试开始");
		int N = 50;
		int V = 30;
		int M = 3000;
		int testTime = 20000;
		for (int i = 0; i < testTime; i++) {
			int n = (int) (Math.random() * N) + 1;
			int[] arr = randomArray(n, V);
			int m = (int) (Math.random() * M);
			int ans1 = waitingTime1(arr, m);
			int ans2 = waitingTime2(arr, m);
			if (ans1 != ans2) {
				System.out.println("出错了!");
			}
		}
		System.out.println("测试结束");
	}

	// 对数器测试
	public static int[] randomArray(int n, int v) {
		int[] arr = new int[n];
		for (int i = 0; i < n; i++) {
			arr[i] = (int) (Math.random() * v) + 1;
		}
		return arr;
	}

}

测试链接

https://leetcode.cn/problems/minimum-time-to-complete-trips/

本题要求的是需要完成 w 趟旅行,没有到自己的概念,所以不需要 +1 处理

在数据上有些变化,需要改成 long 类型,除此以外与再无区别

java
class Solution {
    // 原来是 int[] time, int totalTrips
    // 这里改成 int[] arr, int w
    public long minimumTime(int[] arr, int w) {
        int min = Integer.MAX_VALUE;
        for (int x : arr) {
            min = Math.min(min, x);
        }
        long ans = 0;
        long l = 0;
        // 至少等这么久才轮到自己
        long r = (long)min * w;
        while (l <= r) {
            long mid = l + ((r - l) >> 1);
            // 要求完成多少趟旅行,不需要 +1
            // 往左二分,寻找更短的时间花费
            if (f(arr, mid) >= w) {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return ans;
    }

    // 如果每个服务员都服务 time 的时间,可以接待
    // 几位客人(结束的,开始的客人都要算)
    public long f(int[] arr, long time) {
        long ans = 0;
        for (int num : arr) {
            // 要求完成多少趟旅行,不需要 +1
            ans += (time / num);
        }
        return ans;
    }
}

刀砍毒杀怪兽问题

题目描述

怪兽的初始血量是一个整数 hp,给出每一回合刀砍和毒杀的数值 cuts 和 poisons

第 i 回合如果用刀砍,怪兽在这回合会直接损失 cuts [ i ] 的血,不再有后续效果

第 i 回合如果用毒杀,怪兽在这回合不会损失血量,但是之后每回合都损失 poisons [ i ] 的血量

并且你选择的所有毒杀效果,在之后的回合都会叠加

两个数组 cuts、poisons,长度都是 n,代表你一共可以进行 n 回合

每一回合你只能选择刀砍或者毒杀中的一个动作

如果你在 n 个回合内没有直接杀死怪兽,意味着你已经无法有新的行动了

但是怪兽如果有中毒效果的话,那么怪兽依然会在血量耗尽的那回合死掉

返回至少多少回合,怪兽会死掉

数据范围如下

1 <= n <= 105

1 <= hp <= 109

1 <= cuts [ i ]、poisons [ i ] <= 109

本题来自真实大厂笔试,找不到测试链接,所以用对数器验证

思路分析

正着求不好求,对于每一回合选择刀砍还是毒杀不好抉择

单调性的体现:回合数越多,怪兽死的可能性越大

逆向思维二分答案,要求 n 回合怪兽必须死,那对于抉择刀砍还是毒杀就很好选了

例如当前是第 10 回合, 要求第 12 回合怪兽必须死,当然是选择刀砍,因为只有 2 回合的机会,要使怪兽扣的血量尽可能的多

代码实现

java
class Solution {
    // 时间复杂度:O(n * log(hp)),额外空间复杂度:O(1)
    public int fast(int[] cuts, int[] poison, int hp) {
        int ans = Integer.MAX_VALUE;
        int l = 1;
        int r = hp + 1;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            // 如果 mid 回合能让怪兽死,那更多的回合肯定可以
            // 记答案,往左二分,看更少的回合能否将怪兽杀死
            if (f(cuts, poison, hp, mid)) {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return ans;
    }

    // cuts :刀砍伤害,poisons :毒啥伤害
    // hp :怪兽血量,limit :回合限制
    // 给定 limit 回合限制,返回能不能将怪兽杀死
    public boolean f(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++) {
            // 如果采用毒杀伤害,本轮不会生效,limit - j 才是生效的回合
            // 在之后的每一轮都会生效,伤害需要累加,所以采用相乘关系
            hp -= Math.max((long) cuts[i], (long) (limit - j) * (long) poisons[i]);
            if (hp <= 0) {
                return true;
            }
        }
        return false;
    }
}

对数器验证

java
package class051;

// 刀砍毒杀怪兽问题
// 怪兽的初始血量是一个整数hp,给出每一回合刀砍和毒杀的数值cuts和poisons
// 第i回合如果用刀砍,怪兽在这回合会直接损失cuts[i]的血,不再有后续效果
// 第i回合如果用毒杀,怪兽在这回合不会损失血量,但是之后每回合都损失poisons[i]的血量
// 并且你选择的所有毒杀效果,在之后的回合都会叠加
// 两个数组cuts、poisons,长度都是n,代表你一共可以进行n回合
// 每一回合你只能选择刀砍或者毒杀中的一个动作
// 如果你在n个回合内没有直接杀死怪兽,意味着你已经无法有新的行动了
// 但是怪兽如果有中毒效果的话,那么怪兽依然会在血量耗尽的那回合死掉
// 返回至少多少回合,怪兽会死掉
// 数据范围 :
// 1 <= n <= 10^5
// 1 <= hp <= 10^9
// 1 <= cuts[i]、poisons[i] <= 10^9
// 本题来自真实大厂笔试,找不到测试链接,所以用对数器验证
public class Code07_CutOrPoison {

	// 动态规划方法(只是为了验证)
	// 目前没有讲动态规划,所以不需要理解这个函数
	// 这个函数只是为了验证二分答案的方法是否正确的
	// 纯粹为了写对数器验证才设计的方法,血量比较大的时候会超时
	// 这个方法不做要求,此时并不需要理解,可以在学习完动态规划章节之后来看看这个函数
	public static int fast1(int[] cuts, int[] poisons, int hp) {
		int sum = 0;
		for (int num : poisons) {
			sum += num;
		}
		int[][][] dp = new int[cuts.length][hp + 1][sum + 1];
		return f1(cuts, poisons, 0, hp, 0, dp);
	}

	// 不做要求
	public static int f1(int[] cuts, int[] poisons, int i, int r, int p, int[][][] dp) {
		r -= p;
		if (r <= 0) {
			return i + 1;
		}
		if (i == cuts.length) {
			if (p == 0) {
				return Integer.MAX_VALUE;
			} else {
				return cuts.length + 1 + (r + p - 1) / p;
			}
		}
		if (dp[i][r][p] != 0) {
			return dp[i][r][p];
		}
		int p1 = r <= cuts[i] ? (i + 1) : f1(cuts, poisons, i + 1, r - cuts[i], p, dp);
		int p2 = f1(cuts, poisons, i + 1, r, p + poisons[i], dp);
		int ans = Math.min(p1, p2);
		dp[i][r][p] = ans;
		return ans;
	}

	// 二分答案法
	// 最优解
	// 时间复杂度O(n * log(hp)),额外空间复杂度O(1)
	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中点,一定要让怪兽在m回合内死掉,更多回合无意义
			m = l + ((r - l) >> 1);
			if (f(cuts, poisons, hp, m)) {
				ans = m;
				r = m - 1;
			} else {
				l = m + 1;
			}
		}
		return ans;
	}

	// cuts、posions,每一回合刀砍、毒杀的效果
	// hp:怪兽血量
	// limit:回合的限制
	public static boolean f(int[] cuts, int[] posions, 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) * (long) posions[i]);
			if (hp <= 0) {
				return true;
			}
		}
		return false;
	}

	// 对数器测试
	public static void main(String[] args) {
		// 随机测试的数据量不大
		// 因为数据量大了,fast1方法会超时
		// 所以在数据量不大的情况下,验证fast2方法功能正确即可
		// fast2方法在大数据量的情况下一定也能通过
		// 因为时间复杂度就是最优的
		System.out.println("测试开始");
		int N = 30;
		int V = 20;
		int H = 300;
		int testTimes = 10000;
		for (int i = 0; i < testTimes; i++) {
			int n = (int) (Math.random() * N) + 1;
			int[] cuts = randomArray(n, V);
			int[] posions = randomArray(n, V);
			int hp = (int) (Math.random() * H) + 1;
			int ans1 = fast1(cuts, posions, hp);
			int ans2 = fast2(cuts, posions, hp);
			if (ans1 != ans2) {
				System.out.println("出错了!");
			}
		}
		System.out.println("测试结束");
	}

	// 对数器测试
	public static int[] randomArray(int n, int v) {
		int[] ans = new int[n];
		for (int i = 0; i < n; i++) {
			ans[i] = (int) (Math.random() * v) + 1;
		}
		return ans;
	}

}