11.5二分法

11/5

每日一题:3222. 求出硬币游戏的赢家

给你两个 整数 xy ,分别表示价值为 75 和 10 的硬币的数目。

Alice 和 Bob 正在玩一个游戏。每一轮中,Alice 先进行操作,Bob 后操作。每次操作中,玩家需要拿出价值 总和 为 115 的硬币。如果一名玩家无法执行此操作,那么这名玩家 输掉 游戏。

两名玩家都采取 最优 策略,请你返回游戏的赢家。

示例 1

输入:x = 2, y = 7

输出:"Alice"

解释:

游戏一次操作后结束:

  • Alice 拿走 1 枚价值为 75 的硬币和 4 枚价值为 10 的硬币。

示例 2:

输入:x = 4, y = 11

输出:"Bob"

解释:

游戏 2 次操作后结束:

  • Alice 拿走 1 枚价值为 75 的硬币和 4 枚价值为 10 的硬币。
  • Bob 拿走 1 枚价值为 75 的硬币和 4 枚价值为 10 的硬币。

提示:

  • 1 <= x, y <= 100

思路:

直接模拟即可

class Solution {
public:
    string losingPlayer(int x, int y) {
        int turn = 1;
      //注意样例(1,4)的边界情况,要加“=”
        while(x >= 1 && y >= 4) {
          x -= 1;
          y -= 4;
          turn ++;
        }
        if(turn % 2 == 0) return "Alice";
        else return "Bob";    
        }
};

704. 二分查找

重点:

TLE了

对区间的定义没有理解清楚,在循环中没有始终坚持根据查找区间的定义来做边界处理。

区间的定义就是不变量,在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则

针对数组无重复元素且有序的前提:

二分法的前提条件:有序数组数组中无重复元素

二分法定义一般两种,左闭右闭即[left, right],或者左闭右开即[left, right)。

  • 左闭右闭时,初始定义 l = 0 , r = nums.size() - 1,判断条件为while(l <= r) , 更新区间为 l = mid + 1 , r = mid - 1
  • 左闭右开时,初始定义 l = 0 , r = nums.size() ,判断条件为while(l < r) , 更新区间为l = mid + 1 , r = mid

举例:在数组nums:[1,2,3,4,7,9,10]中查找元素targrt = 2,如图所示:(注意区别

704.二分查找

704.二分查找1

用统一的优雅代码:定义左右边界为-1、n

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int l = -1;
        int r = nums.size();
        while(l + 1 != r){         
          int mid = l + ((r - l) >> 1);
       	if(target < nums[mid]) 		r = mid;
        else if(target > nums[mid]) l = mid ;
        else  return mid ;    
        }       
        return -1;
    }
};

拓展(如果数组元素有重复)

1. 是否可以使用二分法的关键在于:二分后,能否判断出答案所在的区间,而不是数据是否有序。

2.防溢出计算mid(移位+用减法计算中间索引)

int mid = l + ((r - l ) >> 1)//这里两个括号,移位运算的优先级跟加减一样

3. 如何优雅的处理边界条件?

对于一个有序数组,假设下标为0,1,2,3…,n-1;总共n个数字(不要求数字不重复)。指针L要赋值为-1,指针R要赋值为n

无论何种情况,初始的L+1始终小于R,历经循环后最终L和R相邻,不会出现一开始L就和R重合等情况导致出现while(L+1!=R)循环不能结束的情况。

我们就能够通过二分得到不重合的两区间,而且只需要L=midR=mid,不需要考虑L=mid+1R=mid-1的情况
记得是对于一个下标为0,1,2…n-1的数组,模板为:

int L = -1 ,  R = n;
while( L + 1  != R)
{
	int mid = L + R >> 1;
	if(check())     L = mid;
	else   R = mid;
	//最后根据你所分左右两边区间的结果
	//选取L或者R作为结果
}

如例题找最后出现的位置(或找第一次出现的位置)

给定一个排好序的整数数组a,数组中可能存在重复元素。给定数组中的一个值target,求出它最后出现的位置
例如数组nums为:[1 3 3 3 5],目标值target = 3。nums中最后一个等于3的元素为:nums[3],所以结果为3。

int findLast(vector<int> nums,int t){
    int l = -1, r = nums.size();
    while (l + 1 != r){//l<r进行二分,直到l=r时,停止二分
        int mid = l + ((r - l ) >> 1);//下方语句,出现了l = mid,mid要用(l + r + 1) / 2计算
        if (nums[mid] > t)//nums[mid] > t,t最后一次出现的位置一定在mid之前
            r = mid ;
        else //nums[mid] <= t,t最后一次出现的位置一定在mdi处或者mid之后
            l = mid;//出现了l = mid,mid要用(l + r + 1) / 2计算
    }
    return l;
}

如果找第一次出现的位置,则相应的代码逻辑为:(r指针取等)

if(nums[mid] < k)  l = mid;
else r = mid;

如一道综合例题:

AcWing 789. 数的范围

#include <iostream>
#include <vector>
using namespace std;

const int N = 1e5 + 10;

vector<int> nums(N);

int main(){
  int n , q , k ;
  cin >> n >> q;
  for (int i = 0; i < n; i++)   cin >> nums[i];
  
  while(q --){
    cin >> k;
    //找第一次出现的位置r
    int l = -1 , r = n;
    while(l + 1 != r){
      int mid = l + ((r - l) >> 1);
      if(nums[mid] < k) l = mid;
      else r = mid;  //此时r是第一次出现的位置
    }
      if(nums[r] != k)  cout << "-1 -1" << endl;
      else{      //对应于k已经出现在数组中的情况
        cout << r << " ";
        int ll = -1 , rr = n;
        while(ll + 1 != rr){
          int mid = ll + ((rr - ll) >> 1);
          if(nums[mid] > k) rr = mid;
          else ll = mid;//此时ll是最后一次出现的位置,如果就只出现一次,则ll = r
                     }
         cout << ll << endl;
      }
    }
  return 0;
}

35.搜索插入位置

思路:

优雅法,找到了target返回mid,没找到返回r(代入样例试试,其实是该数字应该第一次出现的位置)

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int l = -1;
        int r = nums.size();
        while(l + 1 != r){
          int mid = l + ((r - l) >> 1);
          if(nums[mid] < target) l = mid;
          else if(nums[mid] > target)  r = mid;
          else return mid;
        }
        return r;
    }
};

34. 在排序数组中查找元素的第一个和最后一个位置

思路:

没啥好说的,跟上面acwing那题一样。

值得注意的点

  1. 本题函数是searchRange,格式为vector,因此要定义一个vector<int> res ,cout 改为return res;

    也可不定义,但要return{-1 , -1}

  2. 其次,vectorpush_back()只能一个个地加进去,且是小括号(),不是大括号{}

  3. if判断里两个条件的由来:对于特殊样例的判断,比如:

    • nums = { },target = 0,初始l = -1 , r = 1 , mid = 0,一次while后 r = mid = 0,此时满足nums[r] != target
    • nums = {2 , 2} , target = 3,初始l = -1 , r = 2 mid = 0,一次while后l = mid = 0,两次while后 l = (0 + 2)/2 = 1;跳出while时 l = 1 , r = 2 此时满足 r == nums.size() ,即r越界了,数组中不存在target 。
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int l = -1;
        int r = nums.size();
        while(l + 1 != r){
          int mid = l + ((r - l) >> 1);
          if(nums[mid] < target) l = mid;
          else r = mid;
        }
        if(r == nums.size() || nums[r] != target) {
            return {-1 , -1};
        } 
        else {      
        int ll = -1;
        int rr = nums.size();
        while(ll + 1 != rr){
          int mid = ll + ((rr - ll) >> 1);
          if(nums[mid] > target) rr = mid;
          else ll = mid;
        }
        return {r , ll};
        }   
    }
};

69. x 的平方根

本题一开始没有AC,问题在于mid*mid的溢出问题,强制类型转换(long long)要加括号。

将本题抽象,相当于找第一次出现\(\sqrt{x}\)的地方r,如果它是整数,那么(long long)r*r== x,返回r;否则说明是小数,返回整数部分l。

二分法:

class Solution {
public:
    int mySqrt(int x) {
        int l = -1;
        int r = x;
        while(l + 1 != r){
          int mid = (l + r) / 2;
          if((long long)mid * mid < x) l =  mid;
          else  r = mid;
        } 
        if((long long)r * r == x)  return r;
        else return l;
    }
};

官网的另解:

公式法 : \(e^{\frac{1}{2} \ln{x}} = \sqrt{x}\),于是计算exp(0.5 * log(x)),算完要找出ansans + 1 哪个是答案。

class Solution {
public:
    int mySqrt(int x) {
        if (x == 0) {
            return 0;
        }
      int ans = exp(0.5 * log(x));
      return ((long long)(ans + 1) * (ans + 1) <= x ? ans + 1 : ans);
    }
};

367. 有效的完全平方数

思路:用优雅法

将本题抽象,相当于找第一次出现target的地方,为r的值,再验证r*r== num即可

class Solution {
public:
    bool isPerfectSquare(int num) {
        int l = -1;
        int r = num;
        while(l + 1 != r){
          int mid = (l+r) / 2;
          if((long long)mid * mid < num) l = mid;
          else   r = mid;
        }
        if((long long)r * r == num)  return true;
        return false;
    }
};
posted @ 2024-11-05 23:53  七龙猪  阅读(1)  评论(0)    收藏  举报
-->