出处:代码随想录 代码随想录 代码随想录 代码随想录
二分法的两种区间定义:
1.[left, right] 左闭右闭
2.[left, right) 左闭右开
一、target定义在左闭右闭区间内:[left, right]
此时:
- while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
- if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
public int search(int[] nums, int target) {
// 避免当 target 小于nums[0] nums[nums.length - 1]时多次循环运算
if (target < nums[0] || target > nums[nums.length - 1]) {
return -1;
}
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else {
right = mid - 1;
}
}
return -1;
}
二、target定义在左闭右开区间内:[left, right)
- while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
- if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
public int search(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else {
right = mid;
}
}
return -1;
}
leetcode题目
/**
* 875.爱吃香蕉的珂珂
* 珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。
珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。
*/
/**
* 为什么使用二分法查找呢?
* 因为是查找是否存在一个最小速度x,如果它用x速度都能在h小时内吃完,那么比这个速度快肯定也能吃完
*
* 确定速度区间[1, 香蕉堆中最大的那堆香蕉的个数]
*
*/
public int minEatingSpeed(int[] piles, int h) {
int low = 1;
int high = Arrays.stream(piles).max().getAsInt();
while (low < high) {
// target定义在左闭右开区间,所以low==high是没有必要的
int mid = low + (high - low) / 2;
if (possible(piles, h, mid)) {
// 因为while里的条件是low < high
// 所以high更新的时候要【等于】mid
// 因为是左闭右开区间,右即high==mid,但是却不会去搜索piles[mid]
high = mid;
} else {
low = mid + 1;
}
}
// 因为退出条件是 low和high相等,所以return low或者high都一样
return high;
}
/**
* 判断香蕉是否能吃完
*/
private boolean possible(int[] piles, int h, int k) {
int time = 0;
for (int p : piles) {
// 数组pile中的每一堆香蕉都用速度k吃,结果向上取整
time += (p - 1) / k + 1; // 相当于ceiling方法
}
return time <= h;
}
/**
* 1052. 爱生气的书店老板
* 书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 minutes 分钟不生气,但却只能使用一次。
* 请你返回 这一天营业下来,最多有多少客户能够感到满意 。
* 双指针法,
*/
public int maxSatisfied(int[] customers, int[] grumpy, int minutes) {
int total = 0;
int n = customers.length;
for (int i = 0; i < n; i++) {
if (grumpy[i] == 0) {
total += customers[i];
}
}
int increase = 0;
for (int i = 0; i < minutes; i++) {
increase += customers[i] * grumpy[i];
}
int maxIncrease = increase;
for (int i = minutes; i < n; i++) {
increase = increase - customers[i - minutes] * grumpy[i - minutes] + customers[i] * grumpy[i];
maxIncrease = Math.max(maxIncrease, increase);
}
return total + maxIncrease;
}
用二分法找左右边界
左边界-leftBound也叫lowerBound
public static int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定左侧边界
// 只搜索到就好的二分法这里就直接return mid了,但是寻找左边界时不直接返回
// 继续让right = mid - 1,向左缩圈
right = mid - 1;
}
}
// 最后要检查 left 越界的情况
if (left >= nums.length || nums[left] != target) {
return -1;
}
return left;
}
右边界-rightBound又叫upperBound
public static int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定右侧边界
// 一般情况会找这个mid并返回,但是现在要找upperbound
// 所以要向外扩出左边界
left = mid + 1;
}
}
// 最后要检查 right 越界的情况
if (right < 0 || nums[right] != target) {
return -1;
}
return right;
}
一道习题
public class SnowMountainCost {
/**
* 某雪场共有 N 座雪山,数组 altitude中存储了各雪山海拔(精确到整数)。雪场出售新手票与老手票,新手区票价较高。
* 若该雪场内最高海拔与最低海拔的差值大于 difference,则为老手区;否则为新手区。现在是滑雪活动旺季,雪场经营者希望获得更大收益,
想要将整个雪场打造成新手雪场。改造某座雪山海拔高度的成本为:变更高度的平方。注意:
* 变更高度仅可为整数;
* 变更工程可增加雪山海拔,也可降低雪山海拔;
* 请问雪场经营者改造需要投入的最少成本是多少(即:所有改造雪山的成本之和)?
* 答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
*
* 示例 1:
* 输入:altitude = [5,1,4,3,8], difference = 3
* 输出:8
* 解释:将 1 变成 3,8 变成 6 ,这时最高是 6,最小是 3,相差不超过 3。需要成本为:2^2 + 2^2 = 8
* 示例 2:
* 输入:altitude = [1,2,99999,3,100000], difference = 3
* 输出:998679962
* 解释:将 1,2 和 3 分别变为 40000,将 99999 和 100000 分别变为 40003,此时最高为 40003,最低为 40000,相差不超过 3,
* 此时需要成本为 11998680039,为最小值,取模后为 998679962
* 提示:
*
* 1 <= altitude.length <= 10^5
* 1 <= altitude[i],difference <= 10^5
*/
public static int minPaymentBinarySearchMinMax(int[] altitude, int difference) {
/*
x表示雪场内最后的最低山峰,f(x)表示最后的成本
*/
Arrays.sort(altitude);
// left应为最低altitude中的最低海拔
// right应为最高海拔
// 二分法寻找一个在最低海拔和最高海拔之间的,
// 且和最高海拔的差值要控制在difference内,让所有雪场都是新手区
/**
* 题目解法就是找到一个区间[h, h + difference]
* 对于超过这个区间的山峰进行改造,改造值的平方和就是h下的成本f(h)
* 首先,h的取值在min和max - difference的范围内,用二分法查找取值范围
*
*/
int left = Arrays.stream(altitude).min().getAsInt();
System.out.println(" ===== left:" + left);
int right = Arrays.stream(altitude).max().getAsInt();
System.out.println(" ===== right:" + right);
long cost = Long.MAX_VALUE;
BigInteger minCost = BigInteger.valueOf(0L);
while (left <= right) {
int mid = left + (right - left) / 2;
BigInteger leftCost = calculateCost(mid - 1, difference, altitude);
BigInteger midCost = calculateCost(mid, difference, altitude);
BigInteger rightCost = calculateCost(mid + 1, difference, altitude);
if (midCost.compareTo(leftCost) < 0 && midCost.compareTo(rightCost) < 0) {
minCost = midCost;
break;
} else if (leftCost.compareTo(rightCost) > 0) {
// 这里的调整是因为f(h)整体是个凹函数
// 如果leftcost > rightcost,则整体需要向right移动
left = mid + 1;
minCost = leftCost;
} else {
// 理由同上
right = mid - 1;
minCost = rightCost;
}
}
cost = Math.min(cost, minCost.divideAndRemainder(BigInteger.valueOf(1000000007L))[1].longValue());
return (int)cost;
}
private static BigInteger calculateCost(int height, int difference, int[] altitude) {
// 因为成本可能非常大,所以需要大数来控制
BigInteger oneCost = BigInteger.valueOf(0L);
for (int a : altitude) {
if (a < height) {
oneCost = oneCost.add(BigInteger.valueOf(height - a)
.multiply(BigInteger.valueOf(height - a)));
} else if (a > height + difference) {
oneCost = oneCost.add(BigInteger.valueOf(a - height - difference)
.multiply(BigInteger.valueOf(a - height - difference)));
}
}
return oneCost;
}
public static void main(String[] args) {
int[] altitude = new int[]{5, 1, 4, 3, 8};
int difference = 3;
System.out.println(minPaymentBinarySearchMinMax(altitude, difference));
}
}
再一道习题:
/**
* 道路是直线,多名快递员在道路上完成取件任务,(多名快递员自由行动,互不影响)。
* 快递员初始位置记录于数组persons,取件位置记录于数组tasks中(值不重复)。
* 每个快递员单位时间可向左或向右走一个单位长度,或停留不动。
* 当快递员到达取件位置即完成任务(取件花费时间忽略),该快递员可以继续参与取件。
* 快递员并行作业,请返回最少需要多少单位时间才能完成所有取件任务。
*
* 示例:
* 输入:persons= [2,8,7] tasks=[1,3,11,7] 输出3
* 解释:三个快递员并行作业,轨迹依次为:
* 2 ——1——2——3
* 8——9——10——11
* 7 不用动
* 所以最少需要三个单位时间(并行最大时间,不是求和)
*/
/**
* 题目求的是完成所有取件任务的最小值,也就是是说存在一个时间的最小值,当这个值为x,能完成就是f(x) = true
* 小于x时不能完成,就是f(x) = false,所以可以用二分法搜索这个值
*
* 还有一个策略,考虑最左边的快递员,假设它左边还有任务i,如果向左走,看能否完成最左的任务,
* 如果到不了最左的任务说明x太小了,f(x)直接返回false
* 如果能完成最左边的任务,就考虑两种走法:
* 1.先左后右走满x
* 2.先右后左走满x,找到两种走法走到右边最远的距离r,在[task[i], r]范围内的任务都完成
* 3.如果此时最左的未完成任务是在这个快递员右边,则一路向右走满x,
* [person[j], person[j] + x]范围内的任务都已完成
*/
public int expressDelivery(int[] tasks, int[] persons) {
Arrays.sort(tasks);
Arrays.sort(persons);
int left = 0;
int right = tasks[tasks.length - 1];
while (left < right) {
int mid = left + (right - left) / 2;
if (check(tasks, persons, mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
private boolean check(int[] tasks, int[] persons, int x) {
int maxComplete = 0;
int maxReach;
for (int i = 0; i < persons.length; i++) {
if (persons[i] - tasks[maxComplete] > x) {
return false;
}
if (persons[i] - tasks[maxComplete] > 0) {
maxReach = Math.max(tasks[maxComplete] + (x - persons[i] + tasks[maxComplete]),
(x + tasks[maxComplete] + persons[i]) / 2);
} else {
maxReach = persons[i] + x;
}
while (tasks[maxComplete] <= maxReach) {
if (maxComplete < tasks.length) {
maxComplete++;
}
}
}
if (maxComplete == tasks.length) {
return true;
} else {
return false;
}
}