二分法用途
二分搜索的核心在于,存在一个分界点,小于分界点的不合法,大于分界点的不如他优。那么理所当然他可以解决一系列最优化问题,比如最值。将求值问题转化为判定性问题。
常见题目:最大的最小值、最小的最大值、求满足条件的最长子序列长度……
STL函数实现普通二分
vector<int> a={……};
upper_bound(a.begin(),a.end(),value);//首个大于
lower_bound(a.begin(),a.end(),value);//首个大于等于
int index=lower_bound(nums.begin(),nums.end(),target)-nums.begin();
返回指向范围 [first, last) 中首个大于/大于等于 value 的元素的迭代器,或若找不到这种元素则返回 last 。采用二分实现(logn时间复杂度),所以调用前必须保证容器有序。
upper_bound(a.begin(),a.end(),value,cmp);
比较器cmp可以自己定义,因此可以用这两个函数实现二分查找。
模板
[l, r]区间中前面的部分可以看作0(check(mid)为false),后面的部分可以看作1(check(mid)为true),寻找第一个1的下标。
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;//防止死循环
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
int bsearch_3(int l, int r) {//搜索[l, r],找第一个1
int ans = -1;
while (l <= r) {
int mid = l + r >> 1;
if (check(mid)) {
ans = mid;
r = mid - 1;
} else
l = mid + 1;
}
return ans;
}
例题
【题目一】
【注意点】二分查找的目的是为了找出可以插入的位置,这个位置的范围是[0,nums.size()],不需要顾及数组是否越界,因为由于while(left<right),当left==right时,while已退出,不需要对此时的left\right位置元素进行访问、进一步处理,即可得到答案。
//STL写法
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
return lower_bound(nums.begin(),nums.end(),target)-nums.begin();
}
};
//传统二分
class Solution {
public:
int searchInsert(vector<int> &nums, int target) {
int size = nums.size();
if (size == 0) {
return 0;
}
int left = 0;
int right = size;
while (left < right) {
int mid = left + (right - left) / 2;
// 小于 target 的元素一定不是解
if (nums[mid] < target) {
// 下一轮搜索区间是 [mid + 1, right]
left = mid + 1;
} else if (nums[mid] == target) {
// 根据本题特殊性,看到等于 target 的元素,返回任意一个即可
return mid;
} else {
right = mid;
}
}
return left;
}
};
【题目二】
【思路】题目可以变为找出第一个大于等于target的下标 和 最后一个小于等于target的下标,否则直接用三个条件判断会出现越界情况。
【完整代码】
//STL写法
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int left=lower_bound(nums.begin(),nums.end(),target)-nums.begin();
if(left==nums.size() || nums[left]!=target)
return {-1,-1};
int right=upper_bound(nums.begin(),nums.end(),target)-nums.begin()-1;
return {left,right};
}
};
//传统二分
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.size()==0) return vector<int>(2,-1);
vector<int> res(2);
res[0]=helper_left(nums,target);
res[1]=helper_right(nums,target);
return res;
}
int helper_left(vector<int>& nums, int target){
int left=0,right=nums.size()-1;
while(left<right){//找到第一个大于等于target的下标
int mid=left+(right-left)/2;
//小于target的元素一定不是解
if(nums[mid]<target) //[mid+1,right]
left=mid+1;
else if(nums[mid]>=target)//[left,mid]
right=mid;
}
if(nums[left]!=target)
return -1;
return left;
}
int helper_right(vector<int>& nums, int target){
int left=0,right=nums.size()-1;
while(left<right){//找最后一个小于等于target的下标
int mid=left+(right-left+1)/2;
//大于target的元素一定不是解
if(nums[mid]>target)//[left,mid-1]
right=mid-1;
else if(nums[mid]<=target)//[mid,right]
left=mid;
}
if(nums[left]!=target)
return -1;
return right;
}
};
【题目三】
【思路】先猜一个数(有效范围 [left, right]里的中间数 mid),然后统计原始数组中小于等于这个中间数的元素的个数 cnt,如果 cnt 严格大于 mid,(注意我加了着重号的部分“小于等于”、“严格大于”)依然根据抽屉原理,重复元素就应该在区间 [left, mid] 里。
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int left=1,right=nums.size()-1;
while(left<right){
int mid=left+(right-left)/2,count=0;
for(int i:nums){
if(i<=mid)
count++;
}
if(count<=mid)
left=mid+1;
else
right=mid;
}
return left;
}
};
时间复杂度O(nlgn)符合题目要求。
【题目四】
最长上升子序列
【思路】考虑维护一个数组 tail,其中每个元素 tails[k] 的值代表 长度为 k 的所有上升子序列的结尾的最小值,并且这个数组为升序数组,可以用二分法搜索第一个元素大于等于当前位置元素的tail数组元素下标。时间复杂度O(nlgn)。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> tail;
for (int num : nums) {
auto r = lower_bound(tail.begin(), tail.end(), num);
if (r == tail.end())
tail.push_back(num);
else
*r = num;
}
return tail.size();
}
};
类似的题目:【最大上升子序列和】
dp[i]表示s[0…i]数组中包含s[i]的最大 升序子序列和
int dp[1005]{};
dp[0]=s[0];
for(int i =1 ;i<n;i++){
dp[i]=s[i];
for(int j=0;j<i;j++){
if(s[j]<=s[i]){
dp[i]=max(dp[i],dp[j]+s[i]);
}
}
}
【最大的最小值】
【思路】二分找到可能的答案,判断该答案成不成立,成立则把范围缩小到后半部分,否则缩小到前半部分。
class Solution {
public:
int maxDistance(vector<int>& position, int m) {
sort(position.begin(),position.end());
int n=position.size();
int left=0,right=(position[n-1]-position[0])/(m-1)+1;
while(left<right){
int mid=left+right+1 >>1;
if(check(position,mid,m))
left=mid;
else
right=mid-1;
}
return left;
}
bool check(vector<int>& position,int dis,int m){
int n=position.size();
int pre=position[0],cnt=1;
for(int i=1;i<n;i++){
if(position[i]-pre<dis) continue;
cnt++;
pre=position[i];
if(cnt>=m) return true;
}
return false;
}
};