二分答案
二分答案区间,最优化问题转换为判定性问题。
「二分」的本质是二段性,并非单调性。说白了就是答案在一个区间,二分区间,直到找到最优答案。
TreeSet 的方法:
E floor(E e) 返回此 set 中小于或等于给定元素的最大元素,如果没有这样的元素,则 null 。
E ceiling(E e) 返回此 set 中大于或等于给定元素的最小元素,如果没有这样的元素,则 null 。

左查找 返回 set 中 ≥ 给定目标的最小元素的最小索引 。
右查找 返回 set 中 > 给定目标的最小元素的索引 。
287.寻找重复数
重复数 target 只有一个,左右查找结果相同。通过条件转化成 mid 与 target 之间的关系。
左查找二段性:cnt 是数组中 ≤ mid 的个数,如果 cnt ≤ mid,那么说明 mid < target,即 target ∈ (mid, right]。反之 cnt > mid,说明 target ∈ [left, mid]。
右查找二段性:cnt 是数组中 < mid 的个数,如果 cnt < mid,那么说明 mid ≤ target ,即 target ∈ [mid, right]。反之 cnt ≥ mid,说明 target ∈ [left, mid)。
注意 x 的个数会补齐缺失的数后还剩一个。
class Solution {
public int findDuplicate(int[] nums) {
int left = 1, right = nums.length; // [left, right)
while (left < right) {
int mid = left + right >> 1;
int cnt = 0;
// 右查找写法,左查找改为 ≤,最后返回 right
for (int x : nums) if (x < mid) cnt++;
if (cnt < mid) left = mid + 1;
else right = mid;
}
return right - 1;
}
}
快慢指针
把的数组当作一个 链表 来看,数组的下标就是指向元素的指针,把数组的元素也看作指针。如 0 是指针,指向 nums[0],而 nums[0] 也是指针,指向 nums[nums[0]]。
链表中的环
例:[1,2,3,4,5,2]。按照上面的循环下去就会得到这样一个路径:1 2 3 4 5 [3 4 5] [3 4 5] . . . 这样就有了一个环。

第二次为什么 newSlow 和 slow 相遇在入口
slow 在环中走了 nc - m 步,再走 m 步,就回到入口。
class Solution {
public int findDuplicate(int[] nums) {
int fast = 0, slow = 0;
while (true){
fast = nums[nums[fast]];
slow = nums[slow];
if (fast == slow) break;
}
int res = 0;
while(slow != res){
res = nums[res];
slow = nums[slow];
}
return res;
}
}
★1608.特殊数组的特征值
方法一:排序
根据特征值 x 的定义,x 一定是在 [1, n] 范围内的一个整数。
若 i 为特征值,那么 nums 中恰好有 i 个元素大于等于 i。说明 nums[n - i] 必须大于等于 i,并且 nums[n - i - 1](如果存在)必须小于 i。
[6, 6, 6, 6] 4
class Solution {
public int specialArray(int[] nums) {
int n = nums.length;
Arrays.sort(nums);
for (int i = 1; i <= n; i++)
if (nums[n - i] >= i && (n == i || nums[n - i - 1] < i))
return i;
return -1;
}
}
方法二:计数数组后缀和
class Solution {
public int specialArray(int[] nums) {
// int max = Arrays.stream(nums).max().getAsInt();
int n = nums.length;
int[] counts = new int[n + 1];
for (int x : nums)
counts[Math.min(x, n)] += 1;
int sum = 0;
for (int i = n; i > 0; i--) {
sum += counts[i];
if (sum == i) return i;
}
return -1;
}
}
方法三:二分答案
二段性:
class Solution {
public int specialArray(int[] nums) {
int left = 1, right = nums.length + 1;
while (left < right) {
int mid = left + right >> 1;
int target = check(nums, mid);
if (mid < target) left = mid + 1;
else {
right = mid;
if (target == mid) return mid;
}
}
return -1;
// ★ 需要后处理
// return check(nums, left) == left ? left : -1;
}
int check(int[] nums, int mid) {
int cnt = 0;
for (int x : nums) if (x >= mid) cnt++;
return cnt;
}
}
1095. 山脉数组中查找目标值
class Solution {
MountainArray arr;
public int findInMountainArray(int target, MountainArray mountainArr) {
arr = mountainArr;
// !!!峰值在开区间内 (0, n - 1),这样才不会越界。
int left = 1, right = arr.length() - 1;
while (left < right) {
int mid = left + right >> 1;
if (arr.get(mid) < arr.get(mid + 1)) left = mid + 1;
else right = mid;
}
if (arr.get(left) == target) return left;
int top = left;
int index = binarySearch(target, 0, top, 1);
if (index != -1) return index;
return binarySearch(target, top + 1, arr.length(), -1);
}
public int binarySearch(int target, int left, int right, int sign) {
target *= sign;
while (left < right) {
int mid = left + right >> 1;
int cur = arr.get(mid) *sign;
if (cur < target) left = mid + 1;
else right = mid;
}
// [1,5,2] 0 会找到索引 3,后处理包括越界问题。
return left < arr.length() && arr.get(left) * sign == target ? left : -1;
}
}
一、最小值 左查找
★875.爱吃香蕉的珂珂
适合左查找,即 >= h 的最小值
class Solution {
public int minEatingSpeed(int[] piles, int h) {
int max = 0;
for (int p : piles) if (max < p) max = p;
int left = 1, right = max;
while (left < right) {
int mid = left + right >> 1;
int hours = 0;
for (int p : piles)
hours += (p - 1) / mid + 1; // 向上取整
if (hours > h) left = mid + 1; // hours > h,吃的少了,吃不完。说明:mid < target
else right = mid;
}
return right;
}
}
1283. 使结果不超过阈值的最小除数
lass Solution {
public int smallestDivisor(int[] nums, int threshold) {
int l = 1, r = Arrays.stream(nums).max().getAsInt();
while (l < r) {
int mid = l + r >> 1;
int total = 0;
for (int x: nums)
total += (x - 1) / mid + 1;
if (total > threshold) l = mid + 1; // mid < target
else r = mid;
}
return l;
}
}
2187.完成旅途的最少时间
bisect_left 与 bisect_right 答案不同,可能存在一个时间区间,完成的任务相同。如:[3, 6] 在 3 - 5 完成的任务都是 1 ,所以本题适合左查找。
class Solution {
public long minimumTime(int[] time, int totalTrips) {
long left = 1L, right = 1L * time[0] * totalTrips;
while (left < right) {
long mid = left + right >> 1;
long cnt = 0L;
for (int x : time) cnt += mid / x;
if (cnt < totalTrips) left = mid + 1;
else right = mid;
}
return left;
}
}
1870.准时到达的列车最小时速
由于时速必须为正整数,因此二分的下界为 1;对于二分的上界,考虑 hours 为两位小数,因此对于最后一段路程,最小的时限为 0.01,那么最高的时速要求即为 dist[i]/0.01 ≤ 107,同时为二分时速的上界。
class Solution {
public int minSpeedOnTime(int[] dist, double hour) {
if (dist.length > Math.ceil(hour)) return -1;
int left = 1, right = 10000000;
while (left < right) {
int mid = left + (right - left) / 2;
// 如果以 mid 速度不可达,那么就尝试增大速度,否则就需要减了
if (check(dist, hour, mid)) left = mid + 1;
else right = mid;
}
return left;
}
boolean check(int[] dist, double hour, int speed) {
double time = 0.0;
int n = dist.length - 1;
// 对除了最后一个站点以外的时间进行向上取整累加
for (int i = 0; i < n; ++i)
// 除法的向上取整
time += (dist[i] + speed - 1) / speed;
// 加上最后一个站点所需的时间
time += (double) dist[n] / speed;
return time > hour;
}
}
1011.在D天内送达包裹的能力
class Solution {
public int shipWithinDays(int[] weights, int days) {
int left = Arrays.stream(weights).max().getAsInt();
int right = Arrays.stream(weights).sum();
while (left < right) {
int mid = left + right >> 1;
int day = 1, sum = 0;
for (int x : weights) {
sum += x;
if (sum > mid) {
day++;
sum = x;
}
}
if (day > days) left = mid + 1;
else right = mid;
}
return left;
}
}
1482.制作m束花所需的最少天数
class Solution {
public int minDays(int[] bloomDay, int m, int k) {
long n = bloomDay.length;
if (n < (long) m * k) return -1;
int left = Arrays.stream(bloomDay).min().getAsInt(),
right = Arrays.stream(bloomDay).max().getAsInt();
while (left < right) {
int mid = left + right >> 1;
int cnt = 0, mack = 0;
for (int x : bloomDay) {
if (x <= mid) {
cnt++;
if (cnt == k) {
mack++;
cnt = 0;
if (mack > m) break;
}
} else cnt = 0;
}
if (mack < m) left = mid + 1;
else right = mid;
}
return right;
}
}
1648.销售价值减少的颜色球
class Solution {
int MOD = (int)1e9 + 7;
int[] inventory;
public int maxProfit(int[] inventory, int orders) {
this.inventory = inventory;
int l = 0, r = Arrays.stream(inventory).max().getAsInt();
long res = 0;
while(l < r) {
int mid = l + (r - l) / 2;
long count = 0;
for(int x: inventory) {
count += Math.max(x - mid, 0);
// if (count > orders) break;
}
if (count > orders) l = mid + 1;
// if (check(mid, orders)) l = mid + 1;
else r = mid;
}
for (int x: inventory) {
if (x > r) {// 先 取到 r + 1
res += (x + r + 1L) * (x - r) / 2;
orders -= x - r; // 取球
}
}
// ★优先运算的乘法需要类型提升
res += (long) r * orders; // 剩余补齐, 特殊处理下边界
return (int)(res % MOD);
}
// 统计 > amount 的个数,不包含相等的个数。
public boolean check(int amount, int orders){
int count = 0;
for(int x: inventory) {
count += Math.max(x - amount, 0);
// ★int 需要剪枝,long 不需要
if (count > orders) return true;
}
return false;
}
}
2594. 修车的最少时间
注意到 ranks 值域很小,在 t 相同时,相同 r 值的人修的车数是一样的,所以可以直接按 r 分组计算。
class Solution {
public long repairCars(int[] ranks, int cars) {
int minR = ranks[0];
var cnt = new int[101];
for (int r : ranks) {
minR = Math.min(minR, r);
++cnt[r];
}
long left = 0, right = (long) minR * cars * cars;
while (left < right) {
long mid = left + right >> 1, s = 0;
for (int r = minR; r <= 100; ++r)
s += (long) Math.sqrt(mid / r) * cnt[r];
if (s < cars) left = mid + 1;
else right = mid;
}
return right;
}
}
1631.最小体力消耗路径
class Solution {
int m, n;
int[] DIRS = {1, 0, -1, 0, 1};
int[][] heights;
public int minimumEffortPath(int[][] heights) {
this.heights = heights;
m = heights.length;
n = heights[0].length;
int left = 0, right = 1000000;
while (left < right) {
int mid = left + right >> 1;
if (check(mid)) left = mid + 1;
else right = mid;
}
return left;
}
boolean check(int mid) {
Deque<int[]> q = new LinkedList<>();
q.offer(new int[]{0, 0});
boolean[][] vis = new boolean[m][n];
vis[0][0] = true;
while (!q.isEmpty()) {
int i = q.peek()[0], j = q.poll()[1];
if (i == m - 1 && j == n - 1) return false;
for (int k = 0; k < 4; k++) {
int x = i + DIRS[k], y = j + DIRS[k + 1];
if (inArea(x, y) && !vis[x][y] && Math.abs(heights[i][j] - heights[x][y]) <= mid) {
q.offer(new int[]{x, y});
vis[x][y] = true;
}
}
}
return true;
}
private boolean inArea(int x, int y) {
return x >= 0 && x < m && y >= 0 && y < n;
}
}
778.水位上升的泳池中游泳
public class Solution {
int n;
int[] DIRS = {1, 0, -1, 0, 1};
public int swimInWater(int[][] grid) {
n = grid.length;
int left = grid[0][0], right = n * n - 1;
while (left < right) {
int mid = left + right >> 1; // left + right 不会溢出
// if (bfs(grid, mid)) right = mid; // 8 ms
boolean[][] vis = new boolean[n][n];
if (dfs(grid, 0, 0, vis, mid)) right = mid; // 3 ms
else left = mid + 1;
}
return left;
}
// 使用广度优先遍历得到从 (x, y) 开始向四个方向的所有小于等于 threshold 且与 (x, y) 连通的结点
private boolean bfs(int[][] grid, int threshold) {
Deque<int[]> q = new LinkedList<>();
q.offer(new int[]{0, 0});
boolean[][] vis = new boolean[n][n];
vis[0][0] = true;
while (!q.isEmpty()) {
int i = q.peek()[0], j = q.poll()[1];
if (i == n - 1 && j == n - 1) return true;
for (int k = 0; k < 4; k++) {
int x = i + DIRS[k], y = j + DIRS[k + 1];
if (inArea(x, y) && !vis[x][y] && grid[x][y] <= threshold) {
q.offer(new int[]{x, y});
vis[x][y] = true;
}
}
}
return false;
}
// 使用深度优先遍历得到从 (x, y) 开始向四个方向的所有小于等于 threshold 且与 (x, y) 连通的结点
private boolean dfs(int[][] grid, int i, int j, boolean[][] vis, int threshold) {
vis[i][j] = true;
for(int k = 0; k < 4; k++){
int x = i + DIRS[k], y = j + DIRS[k + 1];
if (i == n - 1 && j == n - 1) return true;
if (inArea(x, y) && !vis[x][y] && grid[x][y] <= threshold) {
if (dfs(grid, x, y, vis, threshold)) return true;
}
}
return false;
}
private boolean inArea(int x, int y) {
return x >= 0 && x < n && y >= 0 && y < n;
}
}
2258. 逃离火灾
二、最大值 右查找
右查找模板,改进 模板一。闭区间
开闭区间是针对数据源而言,查找区间为闭区间。
int rightFind(int[] arr, int target) {
int n = arr.length, res = -1;
int left = 0, right = n - 1; // ★★★ 闭区间 ★★★
while (left <= right) { // ★ [i,j] 当 i == j 时仍然有效,[i, i) 则无效
int mid = left + right >> 1;
if (arr[mid] <= target) {
// !!!找到就直接返回,实现的是 左查找。
// if (arr[mid] == target) return mid;
// 在含 = 处更新答案
// 让子弹再飞一会儿,最后实现的 右查找
// res = mid; // 也可以不记录,最后返回 right
left = mid + 1;
} else right = mid - 1;
}
// return res; // 找到的是 <= target 最大的数的下标
// 需要后处理
// return res > -1 && arr[res] == target ? res : -1;
return right > 0 && arr[right] == target ? right : -1;
}
模板二 右查找插入位置 - 1。半开区间
int rightFind(int[] arr, int target) {
int n = arr.length;
int left = 0, right = n; // ★ [left, right)
while (left < right) {
int mid = left + right >> 1;
if (arr[mid] <= target) left = mid + 1;
else right = mid;
}
// 需要后处理
// return left;
return left < n && arr[left-1] == target ? left - 1: -1;
}
模板三 右查找目标位置。闭区间
int rightFind(int[] arr, int target) {
int n = arr.length;
int left = 0, right = n - 1;
while (left < right) {
int mid = 1 + left + right >> 1; // 向上取整
if (arr[mid] <= target) left = mid;
else right = mid - 1;
}
// 需要后处理
return left >= 0 && arr[left] == target ? left : -1;
}
1898. 可移除字符的最大数目
class Solution {
public int maximumRemovals(String s, String p, int[] removable) {
char[] cs = s.toCharArray(), cp = p.toCharArray();
int n = cs.length, m = cp.length;
int left = 0, right = removable.length + 1; [left, right)
while (left < right) {
int mid = left + right >> 1;
boolean[] state = new boolean[n];
for (int i = 0; i < mid; i++)
state[removable[i]] = true;
int j = 0;
for (int i = 0; i < n && j < m; i++) {
if (cs[i] == cp[j] && !state[i]) j++;
}
if (j == m) left = mid + 1;
else right = mid;
}
return right - 1;
}
}
2226.每个小孩最多能分到多少糖果
每人能拿到 i 个糖果,就可以拿到闭区间 [0, i] 内任一糖果数量。
辅助函数 check(i) ,遍历 candies 数组,用 cnt 统计每一个元素 c 最多可以分给 ⌊c/i⌋ 个小孩的总和,返回 cnt ≥ k。
二分查找的上界,最多一堆糖果的数量或 sum(candies) // k;
二分查找的下界,为 0。
class Solution {
public int maximumCandies(int[] candies, long k) {
// [2, 5] 11 if sum < k 会出现了 /0 的情况, 右分割需要预处理。左分割不需要
int left = 0, right = Arrays.stream(candies).max().getAsInt() + 1;
// long sum = Arrays.stream(candies).toMapLong().sum();
long sum = 0;
for (int x : candies) sum += x;
if (sum < k) return 0; // 左分割需要预处理
while (left < right) {
int mid = left + right >> 1;
long cnt = 0;
for (int x : candies) cnt += x / mid;
if (cnt >= k) left = mid + 1;
else right = mid;
}
return right - 1;
}
}
class Solution {
public int maximumCandies(int[] candies, long k) {
// [2, 5] 11 if sum < k 会出现了 /0 的情况, 右分割需要预处理。左分割不需要
int left = 0, right = Arrays.stream(candies).max().getAsInt();
while (left < right) {
int mid = left + right + 1 >> 1; // 向上取整 左分割
long cnt = 0;
for (int x : candies) cnt += x / mid;
// mid <= target, cnt(人数) 与 mid(糖果数) 成反比,人数越多,分的越少。
if (cnt >= k) left = mid;
else right = mid - 1;
}
return right;
}
}
★2141. 同时运行 N 台电脑的最长时间
n = 2, batteries = [3,3,3] 充分利用小电池,可分段供给不同时段的电脑用。
class Solution {
public long maxRunTime(int n, int[] batteries) {
long tot = 0L;
for (int b : batteries) tot += b;
long l = 0L, r = tot / n + 1;
while (l < r) {
long mid = l + r >> 1;
long sum = 0L;
for (int b : batteries)
sum += Math.min(b, mid);
if (mid * n <= sum) l = mid + 1;
else r = mid;
}
return r - 1;
}
}
class Solution {
public long maxRunTime(int n, int[] batteries) {
Arrays.sort(batteries);
var sum = 0L;
for (var b : batteries) sum += b;
for (var i = batteries.length - 1; ; --i) {
if (batteries[i] <= sum / n) {
return sum / n;
}
sum -= batteries[i];
--n;
}
}
}
2861. 最大合金数
class Solution {
public int maxNumberOfAlloys(int n, int k, int budget, List<List<Integer>> composition, List<Integer> stock, List<Integer> cost) {
int res = 0;
int max = stock.get(0) + budget + 1;
for (var com : composition) {
// 合金数
int left = 0, right = max;
while (left < right) { // 开区间写法
int mid = left + right >> 1;
boolean ok = true;
long money = 0;
for (int i = 0; i < n; ++i) {
long have = stock.get(i), need = 1L * com.get(i) * mid;
if (have < need) {
money += (need - have) * cost.get(i);
if (money > budget) {
ok = false;
break;
}
}
}
if (ok) left = mid + 1;
else right = mid;
}
res = Math.max(res, left - 1);
}
return res;
}
}
三、最小化最大值 (最大值 最小)
2064.分配给商店的最多商品的最小值
class Solution {
public int minimizedMaximum(int n, int[] quantities) {
int left = 1, right = Arrays.stream(quantities).max().getAsInt();
while (left < right) {
int mid = left + right >> 1;
int cnt = 0;
for (int x : quantities)
cnt += (x + mid - 1) / mid;
if (cnt > n) left = mid + 1;
else right = mid;
}
return right;
}
}
1760.袋里最少数目的球
class Solution {
public int minimumSize(int[] nums, int maxOperations) {
// max min
int l = 1, r = Arrays.stream(nums).max().getAsInt();
while (l < r) {
int m = l + r >> 1;
if(f(nums, m) > maxOperations) l = m + 1;
else r = m;
}
return r;
}
int f(int[] nums, int k){
int sum = 0;
for(int x : nums)
// 要少分一份
sum += (x - 1) / k;
return sum;
}
}
410.分割数组的最大值
class Solution {
public int splitArray(int[] nums, int k) {
int left = Arrays.stream(nums).max().getAsInt();
int right = Arrays.stream(nums).sum();
while (left < right) {
int mid = left + right >> 1;
if (check(nums, k, mid)) left = mid + 1;
else right = mid;
}
return left;
}
boolean check(int[] nums, int k, int mid) {
int acc = 0, cnt = 1;
for (int x : nums) {
if (acc + x > mid) {
acc = x;
cnt++;
} else acc += x;
}
return cnt > k;
}
}
★2439. 最小化数组中的最大值
class Solution {
public int minimizeArrayValue(int[] nums) {
int left = 0, right = 0;
for (int x : nums) if (x > right) right = x;
while (left < right) {
int mid = left + right >> 1;
if (check(nums, mid)) left = mid + 1;
else right = mid;
}
return left;
}
boolean check(int[] nums, int mid) {
Long dif = 0L; // 可以容纳后面转移过的最大数
for (int x : nums) {
if (x - mid > dif) return true;
dif += mid - x;
}
return false;
}
}
遍历数组逐步求平均值 ave,当前值 x,if x < ave,then x 无法使前面的值更小或是使本身增大,也不会更新答案。
if x > ave,then x 一定可以将自己的值匀传递给给前面的数,得到最优解。
class Solution {
public int minimizeArrayValue(int[] nums) {
int n = nums.length,res = 0;
Long sum = 0L;
for (int i = 0; i < n; i++) {
sum += nums[i];
if (nums[i] > res)
res = (int) Math.max(res, (sum + i) / (i + 1));
}
return res;
}
}
2513. 最小化两个数组中的最大值
lcm 为 d1 和 d2 的最小公倍数
能被 d2 整除但不能被 d1 整除的数,能在 arr1 中且不能在 arr2 中;
能被 d1 整除但不能被 d2 整除的数,能在 arr2 中且不能在 arr1 中;
既不能被 d1 整除也不能被 d2 整除的数,可以在 arr1 和 arr2 中。
每个数组去掉独享的,剩余的数字只能在共享中选择。
class Solution {
int d1, d2, u1, u2;
long lcm;
public int minimizeSet(int divisor1, int divisor2, int uniqueCnt1, int uniqueCnt2) {
d1 = divisor1; d2 = divisor2;
u1 = uniqueCnt1; u2 = uniqueCnt2;
lcm = (long)d1 * d2 / gcd(d1, d2);
long l = 0L, r = (u1 + u2) * 2;
while (l < r){
long mid = l + r >> 1;
if (check(mid)) l = mid + 1;
else r = mid;
}
return (int)l;
}
boolean check(long x){
long need1 = Math.max(0L, u1 - x / d2 + x / lcm);
long need2 = Math.max(0L, u2 - x / d1 + x / lcm);
long common = x - x / d1 - x / d2 + x / lcm;
return common < need1 + need2;
}
int gcd(int a, int b){
while (b != 0){
int tmp = a % b;
a = b;
b = tmp;
}
return a;
}
}
2560. 打家劫舍 IV
class Solution {
public int minCapability(int[] nums, int k) {
int left = 0, right = 0;
for (int x : nums) if (x > right) right = x;
while (left < right) {
int mid = left + right >> 1;
int a = 0, b = 0;
for (int x : nums)
if (x > mid) a = b;
else {
int c = b;
b = Math.max(b, a + 1);
a = c;
}
if (b < k) left = mid + 1;
else right = mid;
}
return left;
}
}
2616. 最小化数对的最大差值
二分数对中的最大差值 max。
由于下标和答案无关,可以先排序。为了让匹配的数对尽量多,应尽量选相邻的元素,这样更能满足要求。
class Solution {
public int minimizeMax(int[] nums, int p) {
Arrays.sort(nums);
int n = nums.length;
int left = 0, right = nums[n-1] - nums[0];
while (left < right) {
int mid = left + right >> 1;
int cnt = 0;
for (int i = 1; i < n; i++) {
// 相邻两对只能选一对,符合条件就选。
if (nums[i] - nums[i-1] <= mid) {
cnt++; i++;
if (cnt >= p) break;
}
}
if (cnt < p) left = mid + 1;
else right = mid;
}
return left;
}
}
1723.完成所有工作的最短时间
第一个答案,最大值可能是 jobs 的和,所以弱化了 max >= ans 剪枝效果的。二分答案限定 max 的大小,
class Solution {
int INF = 0x3f3f3f3f, k, n;
int[] jobs, work;
public int minimumTimeRequired(int[] jobs, int k) {
this.k = k;
this.jobs = jobs;
Arrays.sort(jobs); // 排序优化:先分配大的,再用小的填充,
n = jobs.length;
work = new int[k];
return dfs(n - 1, 0);
}
public int dfs(int i, int max){
if (i == -1) return max;
int x = jobs[i];
int res = INF; // 也可以把 res 设成全局变量。
for (int j = 0; j < k; j++){
// 剪枝:排除全部分配给了前面的工人,后面的空闲。
if (j > 0 && work[j] == 0 && work[j - 1] == 0) break;
// 剪枝:和上一个工人选择得到的结果是一致的
if (j > 0 && work[j] == work[j - 1]) continue;
work[j] += x;
int tmp = Math.max(max, work[j]);
// 剪枝
if (tmp < res) res = Math.min(res, dfs(i - 1, tmp));
work[j] -= x;
}
return res;
}
}
二分答案优化
class Solution {
int[] jobs;
int k, n;
public int minimumTimeRequired(int[] jobs, int k) {
Arrays.sort(jobs);
this.jobs = jobs;
this.k = k;
n = jobs.length;
int l = jobs[n-1], r = Arrays.stream(jobs).sum();
while (l < r) {
int mid = (l + r) >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
boolean check(int maxMin) {
// 先分配费时的
return backtrack(new int[k], n - 1, maxMin);
}
boolean backtrack(int[] work, int i, int maxMin) {
if (i == -1) return true;
int x = jobs[i];
for (int j = 0; j < k; ++j) {
// 剪枝:排除全部分配给了前面的工人,后面的空闲。
if (j > 0 && work[j] == 0 && work[j-1] == 0) break; // 1
if (work[j] + x <= maxMin) {
work[j] += x;
if (backtrack(work, i - 1, maxMin)) return true;
work[j] -= x;
}
// if (work[j] == 0 || work[j] + x == maxMin) break; // 同 1
}
return false;
}
}
四、最大化最小值(最小值 最大)
★1552. 两球之间的磁力
class Solution {
public int maxDistance(int[] position, int m) {
Arrays.sort(position);
int n = position.length;
int left = 0, right = (position[n-1] - position[0]) / (m - 1) + 1;
while (left < right) {
int mid = left + right >> 1;
int cnt = 1, pre = position[0];
for (int x : position) {
if (x >= pre + mid) {
cnt++;
pre = x;
}
}
if (cnt >= m) left = mid + 1;
else right = mid;
}
return right - 1;
}
}
★2517. 礼盒的最大甜蜜度
「任意两种糖果价格绝对差的最小值」等价于「排序后,任意两种相邻糖果价格绝对差的最小值」。
可以二分答案:甜蜜度越大,可选择的糖果数量越小,有单调性。
f(d) 表示甜蜜度至少为 d 时,至多能选多少类糖果。
二分上界可以取
⌊
m
a
x
(
p
r
i
c
e
)
−
m
i
n
(
p
r
i
c
e
)
k
−
1
⌋
⌊\frac{max(price)-min(price)}{k-1}⌋
⌊k−1max(price)−min(price)⌋。因为最小值不会超过平均值。(平均值指选了 price 最小最大以及中间的一些糖果,相邻糖果差值的平均值。)
二分答案 d:
- 如果 f(d) ≥ k,说明答案至少为 d。
- 如果 f(d) < k,说明答案至多为 d−1。
二分结束后,设答案为 x,那么 f(x) ≥ k 且 f(x+1) < k。
对 price 从小到大排序,二分答案 d。从 x 开始,下一个可以选的数是第一个 ≥ x + d 的数。间隔选择数,最小间隔 ≥ d。如果可以选的数 < k,说明 d 取大了,否则说明 d 取小了。
请注意,二分的区间的定义是:尚未确定 f(d) 与 k 的大小关系的 d 的值组成的集合(范围)。在区间左侧外面的 d 都是 f(d) ≥ k 的,在区间右侧外面的 d 都是 f(d) < k 的。
class Solution {
public int maximumTastiness(int[] price, int k) {
Arrays.sort(price);
int n = price.length;
// k - 1 段,差尽量均匀
// 右查找
// 最小值不会超过平均值。(平均值指选了 price 最小最大以及中间的一些糖果,相邻糖果差值的平均值。)
int left = 0, right = (price[n-1] - price[0]) / (k - 1) + 1;
// while (left < right) {
// int mid = left + right >> 1;
// if (f(price, mid) > k) left = mid + 1;
// else right = mid;
// }
// return left - 1; // left 右邻居
// while (left < right) {
// int mid = left + right + 1 >> 1;
// if (f(price, mid) >= k) left = mid;
// else right = mid - 1;
// }
// return left;
while (left + 1 < right) {
int mid = left + right >> 1;
if (f(price, mid) >= k) left = mid;
else right = mid;
}
return left;
}
private int f(int[] price, int d) {
int cnt = 1, pre = price[0];
for (int p : price) {
if (p >= pre + d) {
cnt++;
pre = p;
}
}
return cnt;
}
}
3281. 范围内整数的最大得分
class Solution {
public int maxPossibleScore(int[] start, int d) {
Arrays.sort(start);
int n = start.length;
int left = 0;
int right = (start[n - 1] + d - start[0]) / (n - 1) + 1;
while (left < right) {
int mid = left + right >>> 1;
if (check(start, d, mid)) left = mid + 1;
else right = mid;
}
return left - 1;
}
private boolean check(int[] start, int d, int score) {
long x = start[0]; // 贪心
for (int i = 1; i < start.length; i++) {
x += score;
if (x > start[i] + d) return false;
if (x < start[i]) x = start[i];
}
return true;
}
}
2528. 最大化城市的最小供电站数目
二分答案 + 前缀和 + 差分数组 + 贪心
1、二分答案 minPower,从左到右遍历 stations,如果 stations[i] 电量不足 minPower,那么需要建供电站来补足。
2、由于 i 左侧的不需要补足,所以 贪心 地在 min(i+r, n−1) 处建是最合适的,恰好让 i 在覆盖范围的边界上。
3、设需要建 m 个供电站,那么需要把 [i, min(i+2r, n−1)] 范围内的电量都增加 m,用 差分数组 来更新是最简单的。
4、最后判断修建的供电站是否超过 k,如果超过说明 minPower 偏大,否则说明偏小。
注:其实前缀和也不需要,可以改为长为 2r + 1 的滑动窗口,但这样写有点麻烦。
class Solution {
int r, k, n;
long[] power;
public long maxPower(int[] stations, int r, int k) {
n = stations.length;
this.r = r; this.k = k;
long[] acc = new long[n + 1]; // 前缀和
for (int i = 0; i < n; ++i)
acc[i + 1] = acc[i] + stations[i];
long min = Long.MAX_VALUE;
power = new long[n]; // 电量
for (int i = 0; i < n; ++i) {
power[i] = acc[Math.min(i + r + 1, n)] - acc[Math.max(i - r, 0)];
min = Math.min(min, power[i]);
}
long left = min, right = min + k + 1;
while (left < right) {
long mid = left + (right - left) / 2;
if (check(mid) <= k) left = mid + 1;
else right = mid;
}
return left - 1;
//return check(left) ? left : right;
}
private long check(long minPower) {
long[] dif = new long[n + 1]; // 差分数组
long acc = 0, need = 0;
for (int i = 0; i < n; ++i) {
acc += dif[i]; // 累加差分值,左侧已经补充的电站数。
long m = minPower - power[i] - acc;
// 如果 i 不够贪心地建在 i + r 处,左侧会覆盖到 i,即 i 正好为左边界。
if (m > 0) { // 需要 m 个供电站
need += m;
// if (need > k) return false; // 提前退出这样快一些
acc += m; // 差分更新,相当于差分数组当前加 m
if (i + r * 2 + 1 < n) dif[i + r * 2 + 1] -= m; // 差分更新
}
}
return need;
}
}
五、第 k 小/大(部分题目还可以用堆解决)
668.乘法表中第k小的数
在 [1, m * n] 范围内,二分查找过程中取得的值不一定在乘法表中。
比如 3 * 3 乘法表:1 2 3 2 4 6 3 6 9 但数字 5、7、8 都不在乘法表中,假设查找 k = 8 的值,你会发现 x = 6, 7, 8 时,count = k = 8 都成立,所以我们不能在 count = k 时直接返回结果,而应该继续进行二分查找,最终返回能满足 count = k 的最小 x 值,比如对于 k = 8,应该返回 6。
第 i 行中大于 x 的个数 min(x / i, n),其中(i 是行号,n 是矩阵的列数)x / i 代表的是如果 x 也在第 i 行,
它存在的列数,所以只要取最小值就是第 i 行不大于 x 的个数。
class Solution {
public int findKthNumber(int m, int n, int k) {
int l = 1, r = m * n;
while (l < r) {
int mid = l + r >> 1;
if (check(m, n, mid) < k) l = mid + 1;
else r = mid;
}
return r;
}
int check(int m, int n, int x) {
// int row = x / n;
// int cnt = row * n; // 先取整行的
// for (int i = row + 1; i <= m; i++)
// cnt += x / i; // 不是整行的再按列数取
int cnt = 0;
for (int i = 1; i <= m; i++)
cnt += Math.min(n, x / i);
return cnt;
}
}
719.找出第K小的数对距离
排序,最小距离为 0,最大距离为 nums[n - 1] - nums[0]。
距离 dis 越大,满足条件的数对就越多, 二分查找,找出合适的距离 dis。
class Solution {
public int smallestDistancePair(int[] nums, int k) {
Arrays.sort(nums);
int left = 0, right = nums[nums.length-1] - nums[0];
while (left < right) {
int mid = left + right >> 1;
if (check(nums, mid) < k) left = mid + 1;
else right = mid;
}
return left;
}
int check(int[] nums, int dis) {
int cnt = 0, left = 0;
for (int i = 0; i < nums.length; i++) {
// !!!
while (nums[i] - nums[left] > dis) left++;
cnt += i - left;
}
return cnt;
}
}
3134. 找出唯一性数组的中位数
class Solution {
int[] nums;
long k;
public int medianOfUniquenessArray(int[] nums) {
this.nums = nums;
int n = nums.length;
k = (n * (n + 1L) >> 1) + 1 >> 1;
int left = 1, right = n;
while (left < right) {
int mid = left + right >> 1;
if (!check(mid)) left = mid + 1;
else right = mid;
}
return left;
}
boolean check(int upper) {
int n = nums.length, left = 0;
long count = 0;
Map<Integer, Integer> cnt = new HashMap();
// 统计 不同元素个数 <= upper 的子数组个数
for (int i = 0; i < n; i++) {
cnt.merge(nums[i], 1, Integer::sum);
// 滑动窗口
while (cnt.size() > upper) {
int y = nums[left++];
if (cnt.merge(y, -1, Integer::sum) == 0) cnt.remove(y);
}
count += i - left + 1; // 子数组个数,每个子数组贡献一个数。
if (count >= k) return true;
}
return false;
}
}
786. 第 K 个最小的素数分数
1439. 有序矩阵中的第 k 个最小数组和
2040. 两个有序数组的第 K 小乘积
2386. 找出数组的第 K 大和
373. 查找和最小的 K 对数字
378. 有序矩阵中第 K 小的元素
其它
754.到达终点数字
k 为移动次数,总步数 p = k * (k + 1) // 2; 找到满足 target <= p 的第一个 k,如果 x = p - target 是偶数,把第 x // 2 次移动方向取反,总步数会减 x;x 是奇数,需要再移动一次或二次凑成偶数,即次数为 k + 1 + k%2。
class Solution:
def reachNumber(self, target: int) -> int:
t = abs(target)
# 方法一:二分答案
i, j = 1, t - 1
while i < j:
mid = i + j >> 1
x = mid * (mid+1) // 2
if x < t: i = mid + 1
else: j = mid
# i = bisect_left(range(t), t, key = lambda x: x * (x + 1) // 2)
return i + i % 2 + 1 if (i * (i + 1) // 2 - t) % 2 else i
# 方法二:直接计算
i = p = 0
while p < t or (p - t) % 2:
i += 1
p = p + i
return i
878. 第 N 个神奇数字
class Solution:
def nthMagicalNumber(self, n: int, a: int, b: int) -> int:
# 容斥原理 + 二分(左查找)
lcm = math.lcm(a, b)
i, j = 0, min(a, b) * n
while i < j:
mid = i + j >> 1
if mid // a + mid // b - mid // lcm < n: i = mid + 1
else: j = mid
return i % (10 ** 9 + 7)
lcm = math.lcm(a, b)
return bisect_left(range(min(a, b) * n), n, key=lambda x: x // a + x // b - x // lcm) % (10 ** 9 + 7)
1201. 丑数 III
class Solution:
def nthUglyNumber(self, n: int, a: int, b: int, c: int) -> int:
# 最小公倍数
# def lcm(a, b):
# return a * b / gcd(a, b)
# def gcd(a, b):
# return a if b == 0 else gcd(b, a % b)
x, y, z, p = lcm(a, b),lcm(a, c), lcm(b, c), lcm(a, b, c)
def cnt(m):
return m // a + m // b + m // c - m // x - m // y - m // z + m // p
i, j = 1, min(a, b, c) * n
while i < j:
mid = i + j >> 1
if cnt(mid) < n: i = mid + 1
else: j = mid
return i
1237.找出给定方程的正整数解
class Solution:
def findSolution(self, customfunction: 'CustomFunction', z: int) -> List[List[int]]:
ans = []
# for i in range(1, 1001):
# l, r = 1, 1000
# while l < r:
# mid = l + r >> 1
# x = customfunction.f(i, mid)
# if x < z: l = mid + 1
# else: r = mid
# r = bisect_left(range(1000), z, key=lambda x:customfunction.f(i, x))
# if r > 0 and customfunction.f(i, r) == z: ans.append([i, r])
i, j = 1, 1000
while i < 1001 and j > 0:
x = customfunction.f(i, j)
if x > z: j -= 1
elif x < z: i += 1
else:
ans.append([i, j])
i += 1
j -= 1
return ans
718.最长重复子数组
class Solution:
def findLength(self, nums1: List[int], nums2: List[int]) -> int:
n, m = len(nums2), len(nums1)
#dp = [[0] * (n + 1) for _ in range(m + 1)]
dp = [0] * (n + 1)
ans = 0
for i in range(m):
# # 逆序遍历可降维,正序需要滚动数组
# for j in range(n-1,-1,-1):
# if nums1[i] == nums2[j]:
# dp[j+1] = dp[j] + 1
# ans = max(ans, dp[j+1])
# else:dp[j+1] = 0
tmp = [0] * (n + 1) # 滚动数组
for j in range(n):
if nums1[i] == nums2[j]:
#dp[i+1][j+1] = dp[i][j] + 1
#ans = max(ans, dp[i+1][j+1])
tmp[j+1] = dp[j] + 1
ans = max(ans, tmp[j+1])
dp = tmp
return ans
# 方法:二分查找 + 哈希
base, mod = 113, 10**9 + 9
def check(length: int) -> bool:
hashA = 0
for i in range(length):
hashA = (hashA * base + nums1[i]) % mod
bucketA = {hashA}
mult = pow(base, length - 1, mod)
for i in range(length, len(nums1)):
hashA = ((hashA - nums1[i - length] * mult) * base + nums1[i]) % mod
bucketA.add(hashA)
hashB = 0
for i in range(length):
hashB = (hashB * base + nums2[i]) % mod
if hashB in bucketA:
return True
for i in range(length, len(nums2)):
hashB = ((hashB - nums2[i - length] * mult) * base + nums2[i]) % mod
if hashB in bucketA:
return True
return False
i, j, ans = 0, min(len(nums1), len(nums2)), 0
while i <= j:
mid = (i + j) // 2
if check(mid):
# ans = mid
i = mid + 1
else:
j = mid - 1
return j
1044.最长重复子串
1838.最高频元素的频数
class Solution:
def maxFrequency(self, nums: List[int], k: int) -> int:
# 方法一:前缀和 + 二分 纯为了二分
def check(w): # 滑动窗口
for i in range(n - w + 1):
j = i + w - 1
tmp = nums[j] * w
presum = acc[j+1] - acc[i] + k
if tmp <= presum: return True
return False
nums.sort()
acc = list(accumulate(nums, initial=0))
n = len(nums)
i, j = 1, n # 最大可能频数范围
while i < j:
mid = i + j + 1 >> 1
if check(mid): i = mid
else: j = mid - 1
# while i <= j:
# mid = i + j >> 1
# if check(mid): i = mid + 1
# else: j = mid - 1
return j
# 方法二:前缀和+滑动窗口 单边扩张
nums.sort()
acc, left = k, 0 # 执行最多 k 次操作
for i, x in enumerate(nums):
acc += x
# 不够补齐,收缩左边,窗口平移,否则向右扩展;大于时可以减少 k 的次数,保持相等。
if acc < (i - left + 1) * x:
acc -= nums[left]
left += 1
return len(nums) - left
793. 阶乘函数后 K 个零
计算出 x! 的末尾 0 的个数,0 的个数由阶乘中可分解出多少个 10 决定,10 = 5 * 2,因此问题转化为 x! 的阶乘中有多少个因子 5 和多少个因子 2,并取他们的最小值,即有多少对儿5 * 2,又因为 5 的个数肯定小于等于 2 的个数,所以我们只需要计算出可以分解多少个 5 即可。
每个 k 对应 5 个连续整数,x∈[5,9]⇔k=1,k 与区间上界最多是 9 倍关系,最少在 1 倍以上,二分查找区间 [k,9k],注意,有些 k 对应的结果不存在,比如 25 = 52 k = 5 被跳过,同理 125 = 53 一样使得 29 和 30 被跳过,所以如果结果不存在则返回 0 。
class Solution:
def preimageSizeFZF(self, k: int) -> int:
# 172
def zeta(n: int) -> int:
res = 0
while n:
n //= 5
res += n
return res
def nx(k: int) -> int:
#return bisect_left(range(5 * k), k, key=zeta)
l, r = 4 * k, 5 * k
while l < r:
mid = l + r >> 1
if zeta(mid) < k: l = mid + 1
else: r = mid
return r
return nx(k + 1) - nx(k)