主要参考:
代码随想录
写对二分查找不是套模板并往里面填空,需要仔细分析题意
文章目录
分治算法
算法思想
分而治之
条件:
(1)原问题可以分解成若干个规模比较小的相同子问题。
(2)子问题相互独立。
(3)子问题的解可以合并为原问题的解。
解题步骤
(1)分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
(2) 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
(3)合并:将各个子问题的解合并为原问题的解。
二分搜索
二分搜索是分治的一个实例,只不过二分搜索有着自己的特殊性:
(1)序列有序(必须满足单调性)
(2)结果为一个值
正常二分将一个完整的区间分成两个区间,两个区间本应单独找值然后确认结果,但是通过有序的区间可以直接确定结果在那个区间,所以分的两个区间只需要计算其中一个区间,然后继续进行一直到结束。
实现方式有递归和非递归。
注意:不要死记模板,根据题意分析!!!
leetcode部分题目
704. 二分查找
二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
区间的定义一般为两种,左闭右闭[left, right],或者左闭右开[left, right)。
(1)左闭右闭[left, right]
定义 target 是在一个在左闭右闭的区间[left, right] ,循环可以进行的条件是left <= right,
终止条件是 left == right + 1,[right + 1, right],可见这时候区间为空
最开始时,left、right都可以取的 int left = 0, right = nums.size() - 1;
- while (left <= right) 要使用 <= ,因为left == right是有意义的(还有一个数字可以搜),所以使用 <=
- if (nums[middle] > target) right =middle - 1,target所在位置[left, middle - 1]
- if (nums[middle] < target) left = middle + 1, target所在位置[middle + 1, right]
- if (nums[middle] == target) return middle
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
}
if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
}
if (nums[middle] == target){
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
(2)左闭右开[left, right)
定义 target 是在一个在左闭右开的区间[left, right),循环可以进行的条件是left < right,
终止条件是 left == right, [right, right],这时候区间非空,还有一个数 ,但此时 while 循环终止了
最开始时,right是取不到的,意思是:如果我知道搜索范围是[1…10],设置right = 11。或者 int left = 0, right = nums.size() ;
这叫「左闭右开」,不是看到 while(left < right)就说搜索区间是「左闭右开」;
- while (left < right) 要使用 <,因为left == right是无意义的(没有数字可以搜)
- if (nums[middle] > target) right =middle,target所在位置[left, middle)
- if (nums[middle] < target) left = middle + 1, target所在位置[middle + 1, right)
- if (nums[middle] == target) return middle
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
}
if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在[middle + 1, right)中
}
if (nums[middle] == target) {
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
35. 搜索插入位置
写法一:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
int ans=right;
int mid;
while(left<=right){
mid=left+(right-left)/2;
if(nums[mid]<target){
left=mid+1;
}
if(nums[mid]>target){
right=mid-1;
}
if(nums[mid]==target){
return mid;
}
}
//ans为插入位置
if(nums[mid]>target)
ans=mid;
else//(nums[mid]<=target)
ans=mid+1;
return ans;
}
};
写法二:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
int ans=nums.size();//nums.size()有可能是插入位置,故初始化的时候要保留
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]>target){
right=mid-1;
ans=mid;//ans为插入位置 target比较小,则至少是把nums[mid]往后挤一位,挤在当前位置插入
}
if(nums[mid]<target){
left=mid+1;
}
if(nums[mid]==target){
return mid;
}
}
return ans;
}
};
34. 在排序数组中查找元素的第一个和最后一个位置
class Solution {
public:
//与上题寻找插入位置类似
int binarySearch(vector<int>& nums, int target, bool lower) {
int left=0;
int right=nums.size()-1;
int ans=nums.size();
if(lower==true){//找第一个位置(先找>=target的第一个)
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]>=target){
right = mid-1;// 右边界下标慢慢左移
ans = mid;
} else {
left = mid+1;
}
}
//循环结束的条件是left == right + 1,而且right下标对应的值大于等于目标值
return ans;
}
else{//找第二个位置(再找>target的第一个)
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]>target){//相等的位置的下一个
right = mid-1;
ans = mid;
} else {
left = mid+1;
}
}
return ans;
}
}
vector<int> searchRange(vector<int>& nums, int target) {
//找到第一个位置
int leftIdx=binarySearch(nums,target,true);
//找到第二个位置
int rightIdx=binarySearch(nums,target,false)-1;
if(leftIdx<=rightIdx && rightIdx<nums.size() && nums[leftIdx]==target && nums[rightIdx]==target){
return vector<int>{leftIdx, rightIdx};
}
//未找到target
return vector<int>{-1, -1};
}
};
875. 爱吃香蕉的珂珂
875. 爱吃香蕉的珂珂
参考题解:
https://leetcode.cn/problems/koko-eating-bananas/solution/er-fen-cha-zhao-ding-wei-su-du-by-liweiwei1419/
思路:
(1)单调性:珂珂吃香蕉的速度越小,耗时越多;反之,速度越大,耗时越少 。由于速度是一个有范围的整数,具有上界和下界。
题目限制了珂珂一个小时之内只能选择一堆香蕉吃,因此速度最大值(上界)就是这几堆香蕉中,数量最多的那一堆。
由于二分搜索的时间复杂度很低,可以设定速度的最小值是 1(下界)。
珂珂一个小时之内只能选择一堆香蕉:每堆香蕉吃完的耗时 = 这堆香蕉的数量 / 珂珂一小时吃香蕉的数量。根据题意,这里的 / 在不能整除的时候,需要 上取整。
(2)结果:「最小速度 」
**注意:**当「二分查找」算法猜测的速度恰好使得珂珂在规定的时间内吃完香蕉的时候,还应该去尝试更小的速度是不是还可以保证在规定的时间内吃完香蕉。
用速度去尝试耗时(需要遍历一次香蕉堆):
如果耗时大于 h,说明速度小了,应该猜一个更大的速度,所以搜索范围是 [mid + 1…right],设置 left = mid + 1 。
耗时小于等于 h,说明当前猜的这个速度 mid 可能是符合题意的一个解(不能排除掉,后面的搜索表示找有没有更小的速度 先将其保存起来ans=mid),right = mid-1,下一次搜索范围是 [left…mid-1]
class Solution {
public:
long getTime(const vector<int>& piles, int speed) {
long time = 0;//计算花费时长
for (int pile : piles) {
//上取整
int curTime = (pile + speed - 1) / speed;
time+=curTime;
}
return time;
}
int minEatingSpeed(vector<int>& piles, int h) {
int maxVal=0;
for(int pile : piles){
maxVal=max(maxVal, pile);
}
//左闭右闭
int low=1;// 速度最小的时候,耗时最长
int high=maxVal;// 速度最大的时候,耗时最短
int ans = high;//目标k
//二分搜索
while(low <=high) {
int mid=low+(high-low)/2;
long time=getTime(piles, mid);
if (time <= h){//速度可以,在h前吃完,下一轮[low..mid-1]
high=mid-1;
ans=mid;//可能的解
}
else{//速度太慢,在h前吃完吃不完,下一轮[mid + 1..high]
low=mid+1;
}
}
return ans;
}
};
1011. 在 D 天内送达包裹的能力
1011. 在 D 天内送达包裹的能力
思路:
(1)单调性:运载能力越小,耗时越多;反之,运载能力越大,耗时越少 。由于运载能力是一个有范围的整数,具有上界和下界。
题目限制了装载的重量不会超过船的最低运载重量
因此最低运载重量最大值(上界)为所有包裹的和。
最低运载重量最小值(下界)为为所有包裹的最大值(至少要运一个包裹)。
(2)结果:「最低运载重量」
注意:
用运载能力去尝试耗时(需要遍历一次weights):
如果耗时大于 days,说明运载能力小了,应该猜一个更大的运载能力,所以搜索范围是 [mid + 1…right],设置 left = mid + 1 。
耗时小于等于 days,说明当前猜的这个运载能力 mid 可能是符合题意的一个解(不能排除掉,后面的搜索表示找有没有更小的速度 先将其保存起来ans=mid),right = mid-1,下一次搜索范围是 [left…mid-1]
class Solution {
public:
int getTime(vector<int>& weights, int cap) {
int cur= 0, days=1; //记得第一天开始
for(auto &w : weights){
if(cur+w <= cap){//装得下
cur+=w;
}
else{// 装不下
cur = w ; //前一天载货;目前装上w
days += 1;//加一天
}
}
return days;
}
int shipWithinDays(vector<int>& weights, int days) {
//左闭右闭
int left = *max_element(weights.begin(), weights.end());
int right = accumulate(weights.begin(), weights.end(), 0);
int ans;
while(left<=right){
int mid = (left+right) >> 1;
int time=getTime(weights,mid);
if(time<=days){
right=mid-1;
ans=mid;//找到了一个可能是解的值,用 ans 保存起来
}
else{
left=mid+1;
}
}
return ans;
}
};
300. 最长递增子序列
思路:
参考题解:https://leetcode.cn/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-dong-tai-gui-hua-e/
新建数组 cell,用于保存最长上升子序列
对原序列进行遍历,每进来一个新的数,用二分查找法来得知要替换在哪个位置的数,二分插入 cell 中。
- 如果 cell 中元素都比它小,将它插到最后
- 否则,用它覆盖掉比它大的元素中最小的那个。
总之,思想就是让 cell 中存储比较小的元素。这样,cell 未必是真实的最长上升子序列,但长度是对的。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size() == 0){
return 0;
}
vector<int> cell;
cell.push_back(nums[0]);
for(int i=1; i<nums.size(); i++){
if(nums[i]>cell.back()){//nums[i] > cell中的最大值
cell.push_back(nums[i]);//插到最后
}
else{
//lower_bound c++库函数(二分查找)
int target = lower_bound(cell.begin(),cell.end(),nums[i])-cell.begin();
cell[target] = nums[i];
}
}
return cell.size();
}
};
二分法自己实现:
class Solution{
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int> cell;
cell.push_back(nums[0]);
for(int i=1;i<n;i++){
int len=cell.size();
if(nums[i]>cell[len-1]){
cell.push_back(nums[i]);
}
else{
//左闭右闭
int left=0,right=len-1;
int L;//用nums[i]覆盖掉比nums[i]大的元素中 最小的那个
while(left<=right){
int mid=(left+right)/2;
if(cell[mid]>=nums[i]){
right=mid-1;
L=right;
}
else{
left=mid+1;
}
}
cell[L+1]=nums[i];
}
}
return cell.size();
}
};
dp做法请移步:leetcode_刷题总结_动态规划