二分查找:数组有序,寻找有序数组第一个大于等于a的数的位置
参考灵神的视频:
二分查找 红蓝染色法
左闭右闭:
int get_pos(vector<int>& nums, int target) {
int l = 0, r = (int)nums.size() - 1;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (nums[mid] < target)
l = mid + 1;
else
r = mid - 1;
}
return l;
}
l-1一定指向小于8的数,r+1指向大于等于8的数;最后,l在第一个数的位置,r在第一个数的前一个位置
左闭右开:
int lower_bound2(vector<int> &nums, int target) {
int left = 0, right = nums.size(); // 左闭右开区间 [left, right)
while (left < right) { // 区间不为空
// 循环不变量:
// nums[left-1] < target
// nums[right] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1; // 范围缩小到 [mid+1, right)
} else {
right = mid; // 范围缩小到 [left, mid)
}
}
return left; // 返回 left 还是 right 都行,因为循环结束后 left == right
}
左开右开:
int lower_bound3(vector<int> &nums, int target) {
int left = -1, right = nums.size(); // 开区间 (left, right)
while (left + 1 < right) { // 区间不为空
// 循环不变量:
// nums[left] < target
// nums[right] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid; // 范围缩小到 (mid, right)
} else {
right = mid; // 范围缩小到 (left, mid)
}
// 也可以这样写
// (nums[mid] < target ? left : right) = mid;
}
return right;//最后right指向答案,left指向前一个数
}
以上是求大于等于的情况,可以把二分查找分为四种类型(前提是数组的数都为整数):
1.大于等于x
2.大于x:转化为>=x+1
3.小于:>=x左边的数减1
4.小于等于:>x的数减1
二分答案法:
有一类题目,让你求满足条件的最值,如果直接去求,不好直接求出来,如果满足以下几个条件,
则可以使用二分答案法解决
1.最终的答案有一个粗略的范围
2.问题的答案和给定的条件之间存在单调性的关系
3.可以建立一个函数,判断某个值是否为答案
4.在答案所在的范围不断的二分搜索,直到找到答案
class Solution {
public:
long f(vector<int>& nums, int m) {
long sum = 0;
for (auto i : nums) {
sum += (i + m - 1) / m;
}
return sum;
}
int minEatingSpeed(vector<int>& piles, int h) {
int l = 1;
int r = 0;
for (auto i : piles)
r = max(r, i);
while (l <= r) {
int m = l + ((r - l) >> 1);
if (f(piles, m) <= h) {
r = m - 1;
} else {
l = m + 1;
}
}
return l;
}
};
如果采用暴力方法,从1开始枚举速度,再去遍历数组计算时间。题目说如果那堆香蕉的数目不足
k,剩下的时间就不吃了,所以速度的最大值就是数组中的最大值,题目中数组元素的最大值到
10^9,明显超过了10^8,所以时间上会超时。
就如刚才上述的分析,速度的最大值为数组元素的最大值,最小就到0,当速度变小时,花费的时
间会越多,在规定时间内吃完的可能性就会越小,这就使答案和给定的条件之间产生了单调性关
系。然后再看能不能写出一个函数来判断某一个数值是否为答案?其实是可以的,我们只要遍历一
遍数组,用数组的元素除以速度取上限值,累加和h判断大小。那么此题就可以用二分搜索的方式
解决
注:在我们的二分搜索介绍中,我们是对一个升序数组求第一个大于等于x的数值的位置,用上述二分搜索的模板可以解决"模板类二分搜索"。然而此题不太一样,对于速度从l到r,随着速度的增大,花费的时间就会越少,速度的区间和对应时间的区间的单调性是相反的,所以要求最小的速度,其实就是求第一个小于等于h的位置,此时应把二分搜索模板nums[mid]>=target;r=mid-1改为nums[mid]<=target;r=mid-1
class Solution {
public:
int get_parttion(vector<int>& nums, int mid) {
int ans = 0;
int sum = 0;
for (auto i : nums) {
if (i > mid)
return -1;
if (sum + i <= mid)
sum += i;
else {
ans++;
sum = i;
}
}
return ans + 1;
}
int splitArray(vector<int>& nums, int k) {
long l = 0, r = 0;
for (auto i : nums)
r += i;
while (l <= r) {
long mid = l + ((r - l) >> 1);
int rem = get_parttion(nums, mid);
if (rem <= k && rem != -1)
r = mid - 1;
else
l = mid + 1;
}
return l;
}
};
求分割的部分的最小值
1.分割部分和最小可能为0,最大为所有数组的元素的和
2.分割的部分和越小,分割的部分就越多,二者之间存在单调性关系
3.想要判断一个数作为划分部分的和,可以将数组划分为多少部分,只要遍历数组来就可以求出划分的部分数
4.那么就可以用二分答案法不断的试探答案
时间复杂度:在区间l~r上不断的二分,其规模是log(sum);每次求划分的部分,就要遍历一遍数组,其规模是n,所以其时间复杂度为O(long(sum)*n),吃香蕉的哪个题的时间复杂度的计算与此题类似
二分答案题单:后续会更新解析
class Solution {
public:
int f(vector<int>& nums, int mid) {
int ans = 0;
for (int l = 0, r = 0; l < nums.size(); l++) {
while (r < nums.size() - 1 && nums[r + 1] - nums[l] <= mid)
r++;
ans += r - l;
}
return ans;
}
int smallestDistancePair(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
int l = 0, r = nums[nums.size() - 1] - nums[0];
while (l <= r) {
int mid = l + ((r - l) >> 1);
int rem = f(nums, mid);
if (rem >= k)
r = mid - 1;
else
l = mid + 1;
}
return l;
}
};
如果直接暴力求解,时间复杂度为O(N^2),根据数据大小可猜测超时了;关于数对的距离,是有
范围的,且数对的距离越大,其是第k小的数对的k的数值就会越大,二者之间存在单调性关系;然
后就是建立f函数,因为数组我们之前已经排好序了,只要建立一个滑动窗口,不停的收集左右边
界来统计答案,就可以求出此距离下是第几小的数对距离
class Solution {
public:
bool f(vector<int>& batteries, long long mid, int n) {
long long sum = 0;
for (int i : batteries) {
if (i >= mid)
n--;
else {
sum += i;
}
if (sum >= n * mid)
return true;
}
return false;
}
long long maxRunTime(int n, vector<int>& batteries) {
long long l = 0, r = 0;
for (int i : batteries)
r += i;
while (l <= r) {
long long mid = l + ((r - l) >> 1);
if (f(batteries, mid, n))
l = mid + 1;
else
r = mid - 1;
}
return l - 1;
}
};
此题的关键是怎么分配电池的问题,当电池的供电量大于等于运行的时间时,到底是一直给一个
电脑供电划算,还是分配供电划算?其实一直给一个电脑供电是更划算的:如果这个电池给A,B供
电,当给A供电时,B还是需要其他的电池给他供电,然后再给B供电,不如一直给一个电池供电
更划算;其次是不足运行时间的电池的分配,其实只要满足所有电池的供电量不小于所有电脑的总
运行时间就可以,根据上面分析,就可以写f函数
然后就是关于二分的过程,供电的时间粗略的估计在0~sum(所有的电池供电量之和)之间,只要
mid的供电时间满足,l=mid+1。注意,此题求的是最大的供电时间,最后r或l-1位置上的才是答
案,与上面的题目"求最小"注意区别