leetcode刷题—二分查找

想成功先发疯,不顾一切向前冲

二分查找

No.1 (来个温柔的)

704.二分查找. - 力扣(LeetCode)

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1


示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示:

  1. 你可以假设 nums 中的所有元素是不重复的。
  2. n 将在 [1, 10000]之间。
  3. nums 的每个元素都将在 [-9999, 9999]之间。
class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length - 1;

        while (left <= right) {
            int mid = (right - left) / 2 + left;
            int num = nums[mid];
            if (num == target) {
                return mid;
            }
            if (num > target) {
                right = mid - 1;
            } 
            else {
                left = mid + 1;
            }
        }
        return -1;

    }
}
为什么使用 (right - left) / 2 + left 公式?

使用公式 (right - left) / 2 + left 代替 (left + right) / 2 是为了避免 整数溢出

  • 在计算机中,整数有最大值。对于32位的 int 类型,这个最大值是 2,147,483,647
  • 如果 leftright 都很大(接近 Integer.MAX_VALUE),left + right 可能会超过这个最大值,导致整数溢出,计算错误。

No.2

34. - 力扣(LeetCode)

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [
5,7,7,8,8,10]
, target = 8
输出:[3,4]

示例 2:

输入:nums = [
5,7,7,8,8,10]
, target = 6
输出:[-1,-1]

示例 3:

输入:nums = [], target = 0
输出:[-1,-1]
class Solution {
    public int[] searchRange(int[] nums, int target) {
        int start = lowerBound(nums,target);
        if(start==nums.length||nums[start]!=target){
            return new int[]{-1,-1};
        }
        int end=lowerBound(nums,target+1)-1;
        return new int[]{start,end};      
    }

    private int lowerBound(int[] nums,int target){
        int left=0,right = nums.length-1;
        while(left<=right){
            int mid = (right-left)/2 +left;
            if(nums[mid]<target){
                left = mid+1;
            }else{
                right = mid-1;
            }
        }
        return left;
    }
}
设计解决方案
  • 步骤1: 实现 lowerBound: lowerBound 函数应该返回数组中第一个大于或等于 target 的元素索引。
  • 步骤2: 使用 lowerBound 确定范围:
    • 第一个 lowerBound 查找 target 的起始位置。
    • 第二个 lowerBound 查找 target + 1 的起始位置并减去 1,以获得 target 的结束位置。
  • 边界条件处理: 检查如果 start 等于数组的长度或数组中位置 start 的元素不等于 target,返回 [-1, -1] 表示 target 不在数组中。
  1. lowerBound 返回的是大于或等于 target 的第一个位置

    • lowerBound(nums, target) 返回的是第一个不小于 target 的元素的索引。
    • 如果所有元素都小于 targetlowerBound 会返回数组长度 nums.length
    • 如果返回的索引指向的值不等于 target,则说明数组中不存在 target
  2. 边界条件处理

    • 如果 start == nums.length,说明数组中所有的元素都小于 target,即 target 不存在于数组中。
    • 如果 nums[start] != target,说明虽然我们找到了一个大于或等于 target 的位置,但是这个位置上的值并不是 target,即 target 不存在于数组中。
举个例子

假设数组 nums = [1, 3, 5, 7, 9]target = 4,调用 lowerBound(nums, 4)

  • lowerBound 的返回值是 2(指向元素 5),因为 5 是第一个大于 4 的元素。
  • 但是,nums[2] != 4,这意味着 4 并不在数组中。

因此,检查 if (start == nums.length || nums[start] != target) 这一行代码是必要的,用于验证找到的索引是否真的对应目标值 target

No.3

744.寻找比目标字母大的最小字母. - 力扣(LeetCode)

给你一个字符数组 letters,该数组按非递减顺序排序,以及一个字符 targetletters 里至少有两个不同的字符。

返回 letters 中大于 target 的最小的字符。如果不存在这样的字符,则返回 letters 的第一个字符。

示例 1:

输入: letters = ["c", "f", "j"],target = "a"
输出: "c"
解释:letters 中字典上比 'a' 大的最小字符是 'c'。

示例 2:

输入: letters = ["c","f","j"], target = "c"
输出: "f"
解释:letters 中字典顺序上大于 'c' 的最小字符是 'f'。

示例 3:

输入: letters = ["x","x","y","y"], target = "z"
输出: "x"
解释:letters 中没有一个字符在字典上大于 'z',所以我们返回 letters[0]。

提示:

  • 2 <= letters.length <= 104
  • letters[i] 是一个小写字母
  • letters 按非递减顺序排序
  • letters 最少包含两个不同的字母
  • target 是一个小写字母
class Solution {
    public char nextGreatestLetter(char[] letters, char target) {
        int length = letters.length;
        char nextGreater = letters[0];
        for (int i = 0; i < length; i++) {
            if (letters[i] > target) {
                nextGreater = letters[i];
                break;
            }
        }
        return nextGreater;
    }
}

class Solution {
    public char nextGreatestLetter(char[] letters, char target) {
        if (target >= letters[letters.length - 1]) {
            return letters[0];
        }
        return binary(letters,(char)(target + 1));
    }

    private char binary(char[] letters, char target) {
        int left = -1, right = letters.length;
        while(left + 1 < right) {
            int mid = left + (right - left) / 2;
            if(letters[mid] < target) {
                left = mid;
            }else {
                right = mid;
            }
        }
        return letters[right];
    }
}

二分答案:求最小

No.1

1283.使结果不超过与之的最小除数. - 力扣(LeetCode)

给你一个整数数组 nums 和一个正整数 threshold  ,你需要选择一个正整数作为除数,然后将数组里每个数都除以它,并对除法结果求和。

请你找出能够使上述结果小于等于阈值 threshold 的除数中 最小 的那个。

每个数除以除数后都向上取整,比方说 7/3 = 3 , 10/2 = 5 。

题目保证一定有解。

示例 1:

输入:nums = [1,2,5,9], threshold = 6
输出:5
解释:如果除数为 1 ,我们可以得到和为 17 (1+2+5+9)。
如果除数为 4 ,我们可以得到和为 7 (1+1+2+3) 。如果除数为 5 ,和为 5 (1+1+1+2)。

示例 2:

输入:nums = [2,3,5,7,11], threshold = 11
输出:3

示例 3:

输入:nums = [19], threshold = 5
输出:4
class Solution {
    public int smallestDivisor(int[] nums, int threshold) {
        int left = 1, right = getMax(nums);

        while (left <= right) {
            int mid = (right - left) / 2 + left;
            if (useAll(nums, mid) > threshold) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return left;

    }

    private int useAll(int[] nums, int s) {
        // (n + nums[mid] - 1) / nums[mid]
        int res = 0;
        for (int n : nums) {
            res += (n + s - 1) / s;
        }
        return res;
    }

    private int getMax(int[] nums) {
        int max = nums[0];
        for (int num : nums) {
            if (num > max) {
                max = num;
            }
        }
        return max;
    }
}

 解释 (num + d - 1) / d

需要注意的是,每次除法都要向上取整,即 Math.ceil((double) num / d)。为了避免使用浮点数,可以直接使用整数的向上取整技巧: (num + d - 1) / d

假设 num 是一个正整数,d 是一个正整数。

  • 如果 numd 的倍数num % d == 0,则 (num + d - 1) / d == num / d

  • 如果 num 不是 d 的倍数num % d != 0,则 num / d 会舍去小数部分,而 (num + d - 1) / d 会对结果向上取整。

向下取整不需要任何特殊处理,因为在大多数编程语言中,整数除法已经是向下取整直接使用 num / d 即可

顺便说一句:这里的二分法不是针对的nums的索引取值,而是对于1~max(nums)中这段升序数字

No.2(和上一道题差不多,可以忽略)

875.爱吃香蕉的珂珂

珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。

珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。  

珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

返回她可以在 h 小时内吃掉所有香蕉的最小速度 kk 为整数)。

示例 1:

输入:piles = [3,6,7,11], h = 8
输出:4

示例 2:

输入:piles = [30,11,23,4,20], h = 5
输出:30

示例 3:

输入:piles = [30,11,23,4,20], h = 6
输出:23

提示:

  • 1 <= piles.length <= 104
  • piles.length <= h <= 109
  • 1 <= piles[i] <= 109

class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        int low = 1, high = 0;
        for(int pile : piles){
            high = Math.max(pile, high);
        }

        int speed = 0;
        int k = high;
        while(low <= high){
            speed = ((high - low) >> 1) + low;
            long time = getTime(piles, speed);
            if(time <= h){
                k = speed;
                high = speed - 1;
            }else{
                low = speed + 1;
            }
        }
        return k;
    }

    private long getTime(int[] piles, int speed){
        long sum = 0;
        for(int pile : piles){
            sum += ((pile + speed - 1) / speed);
        }
        return sum;
    }
}

 这道题的关键是看出数字h是需要向上取整的就可以了。

二分答案:求最大

No.1

275.H指数II. - 力扣(LeetCode)

. - 力扣(LeetCode)

给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数,citations 已经按照 升序排列 。计算并返回该研究者的 h 指数。

h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (n 篇论文中)至少 有 h 篇论文分别被引用了至少 h 次。

请你设计并实现对数时间复杂度的算法解决此问题。

示例 1:

输入:citations = [0,1,3,5,6]
输出:3
解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6 次。
     由于研究者有3篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3

示例 2:

输入:citations = [1,2,100]
输出:2

提示:

  • n == citations.length
  • 1 <= n <= 105
  • 0 <= citations[i] <= 1000
  • citations 按 升序排列

class Solution {
    public int hIndex(int[] citations) {
        int left = 0,right = citations.length-1;
        
        while(left<=right){
            int mid = (right-left)/2+left;
            if(citations[mid]>= citations.length-mid){
                right = mid -1;
            }
            else{
                left = mid+1;
            }
        }
        return citations.length-left;
    }
}

查找逻辑:

citations[mid] == n - mid,这意味着从 mid 开始到 n-1 的文章数量(即 n - mid 篇)都等于 citations[mid] 次引用。

No.2

2576.求出最多标记下表. - 力扣(LeetCode)

给你一个下标从 0 开始的整数数组 nums 。

一开始,所有下标都没有被标记。你可以执行以下操作任意次:

  • 选择两个 互不相同且未标记 的下标 i 和 j ,满足 2 * nums[i] <= nums[j] ,标记下标 i 和 j 。

请你执行上述操作任意次,返回 nums 中最多可以标记的下标数目。

示例 1:

输入:nums = [3,5,2,4]
输出:2
解释:第一次操作中,选择 i = 2 和 j = 1 ,操作可以执行的原因是 2 * nums[2] <= nums[1] ,标记下标 2 和 1 。
没有其他更多可执行的操作,所以答案为 2 。

示例 2:

输入:nums = [9,2,5,4]
输出:4
解释:第一次操作中,选择 i = 3 和 j = 0 ,操作可以执行的原因是 2 * nums[3] <= nums[0] ,标记下标 3 和 0 。
第二次操作中,选择 i = 1 和 j = 2 ,操作可以执行的原因是 2 * nums[1] <= nums[2] ,标记下标 1 和 2 。
没有其他更多可执行的操作,所以答案为 4 。

示例 3:

输入:nums = [7,6,8]
输出:0
解释:没有任何可以执行的操作,所以答案为 0 。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 109

class Solution {
    public int maxNumOfMarkedIndices(int[] nums) {
        // 对数组进行排序
        Arrays.sort(nums);
        
        // 初始化左右指针,left表示当前满足条件的最大标记数量
        // right是二分查找的上界,指向数组的一半位置+1
        int left = 0;
        int right = nums.length / 2 + 1; 
        
        // 二分查找,找到最大的k,使得前k个数满足条件
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;  // 计算中间值
            if (check(nums, mid)) {
                left = mid;  // 如果满足条件,则增大left
            } else {
                right = mid;  // 否则缩小right
            }
        }
        
        // 返回最大标记数量,乘以2表示标记的索引对数
        return left * 2; 
    }

    // 检查前k个数是否满足条件:nums[i] * 2 <= nums[nums.length - k + i]
    private boolean check(int[] nums, int k) {
        for (int i = 0; i < k; i++) {
            // 如果前k个数中,任意一个数的两倍大于后k个数中的对应元素,则返回false
            if (nums[i] * 2 > nums[nums.length - k + i]) {
                return false;
            }
        }
        // 如果所有前k个数都满足条件,则返回true
        return true;
    }
}
class Solution {
    public int maxNumOfMarkedIndices(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        int i = 0;
        for (int j = (n + 1) / 2; j < n; j++) {
            if (nums[i] * 2 <= nums[j]) { // 找到一个匹配
                i++;
            }
        }
        return i * 2;
    }
}

 

 

二分间接值

二分的不是答案,而是一个和答案有关的值(间接值)

No.1

1648.销售价值减少的颜色球. - 力扣(LeetCode)

你有一些球的库存 inventory ,里面包含着不同颜色的球。一个顾客想要 任意颜色 总数为 orders 的球。

这位顾客有一种特殊的方式衡量球的价值:每个球的价值是目前剩下的 同色球 的数目。比方说还剩下 6 个黄球,那么顾客买第一个黄球的时候该黄球的价值为 6 。这笔交易以后,只剩下 5 个黄球了,所以下一个黄球的价值为 5 (也就是球的价值随着顾客购买同色球是递减的)

给你整数数组 inventory ,其中 inventory[i] 表示第 i 种颜色球一开始的数目。同时给你整数 orders ,表示顾客总共想买的球数目。你可以按照 任意顺序 卖球。

请你返回卖了 orders 个球以后 最大 总价值之和。由于答案可能会很大,请你返回答案对 109 + 7 取余数 的结果。

示例 1:

输入:inventory = [2,5], orders = 4
输出:14
解释:卖 1 个第一种颜色的球(价值为 2 ),卖 3 个第二种颜色的球(价值为 5 + 4 + 3)。
最大总和为 2 + 5 + 4 + 3 = 14 。

示例 2:

输入:inventory = [3,5], orders = 6
输出:19
解释:卖 2 个第一种颜色的球(价值为 3 + 2),卖 4 个第二种颜色的球(价值为 5 + 4 + 3 + 2)。
最大总和为 3 + 2 + 5 + 4 + 3 + 2 = 19 。

示例 3:

输入:inventory = [2,8,4,10,6], orders = 20
输出:110

示例 4:

输入:inventory = [1000000000], orders = 1000000000
输出:21
解释:卖 1000000000 次第一种颜色的球,总价值为 500000000500000000 。 500000000500000000 对 109 + 7 取余为 21 。

提示:

  • 1 <= inventory.length <= 105
  • 1 <= inventory[i] <= 109
  • 1 <= orders <= min(sum(inventory[i]), 109)
class Solution {
    private final int MOD = (int) 1e9 + 7;

    public int maxProfit(int[] inventory, int orders) {
        int max = 0;
        for (int x : inventory) {
            max = Math.max(max, x);
        }
        int l = -1, r = max + 1;
        while (l + 1 < r) {
            int c = l + (r - l) / 2;
            if (check(inventory, orders, c)) {
                l = c;
            } else {
                r = c;
            }
        }
        long ans = 0;
        for (int x : inventory) {
            if (x > r) {
                ans += (x + r + 1) * 1L * (x - r) / 2;
                ans %= MOD;
                orders -= x - r;
            }
        }
        ans += r * 1L * orders;
        return (int) (ans % MOD);
    }

    private boolean check(int[] nums, int orders, int x) {
        int sum = 0;
        for (int num : nums) {
            sum += Math.max(num - x, 0);
            if (sum > orders) {
                return true;
            }
        }
        return false;
    }
}

排序+遍历数组

class Solution {
    // 定义一个常量用于取模,防止数字过大
    static final int mod = (int) 1e9 + 7;

    public int maxProfit(int[] inventory, int orders) {
        // 将库存数组按升序排序
        Arrays.sort(inventory);
        int len = inventory.length;
        long ans = 0, getnum = 0; // 初始化变量用于存储答案和已取出的球的数量
        int cnt = 1, lastPrice = inventory[len - 1]; // 初始化计数器和上一次的价格
        
        // 从后往前遍历库存数组,同时计算取出球的数量
        for (int i = len - 2; i >= 0 && getnum <= orders; --i) {

            if (inventory[i] == inventory[i + 1]) {
                cnt++; // 如果当前的库存量与下一个相同,则增加计数器
                continue;
            }

            long diff = inventory[i + 1] - inventory[i]; // 计算相邻两个库存量之间的差距

            // 如果取出球的数量超过了订单数,停止计算
            if (getnum + diff * cnt >= orders)
                break;

            getnum += diff * cnt; // 更新已取出的球的总数量
            lastPrice = inventory[i]; // 更新上一次的价格
            ans += (inventory[i + 1] + inventory[i] + 1) * diff / 2 * cnt; // 计算当前批次的利润
            cnt++; // 增加计数器
        }

        long needNum = orders - getnum; // 计算还需要取出的球数
        long n = needNum / cnt; // 计算能均分给每个库存位置的球数
        ans += (lastPrice + lastPrice - n + 1L) * n / 2 * cnt; // 计算均分部分的利润

        ans += (needNum % cnt) * (lastPrice - n); // 计算剩余部分的利润
        return (int) (ans % mod); // 返回结果,取模以防止溢出
    }
}

二分法

class Solution {
    static final int mod = (int)1e9 + 7;
    public int maxProfit(int[] inventory, int orders) {
        // wuzhenyu
        int l = 0, r = 0;
        long ans = 0;
        for(int num: inventory) if (num > r) r = num;
        while(l < r) { // 二分找下边界
            int mid = l + (r - l) / 2;
            if(check(inventory, mid, orders)) l = mid + 1;
            else r = mid;
        }
        for(int num: inventory) { // 取到下边界之上一个值
            if(num > l) {
                ans += (num + l + 1L) * (num - l) / 2; // 取到 l + 1
                orders -= num - l; // 取球
            }
        }
        ans += (l + 0L) * orders; // 剩余补齐, 特殊处理下边界
        return (int)(ans % mod);
    }

    // 以price为底线
    public boolean check(int[] nums, int price, int orders){
        int sum = 0;
        for(int num: nums) {
            sum += Math.max(num - price, 0);
            if (sum >= orders) return true;
        }
        return false;
    }
}

最小化最大值

本质是二分答案求最小

No,1

看到「最大化最小值」或者「最小化最大值」就要想到二分答案,这是一个固定的套路。

2560.打家劫舍IV. - 力扣(LeetCode)

沿街有一排连续的房屋。每间房屋内都藏有一定的现金。现在有一位小偷计划从这些房屋中窃取现金。

由于相邻的房屋装有相互连通的防盗系统,所以小偷 不会窃取相邻的房屋 。

小偷的 窃取能力 定义为他在窃取过程中能从单间房屋中窃取的 最大金额 。

给你一个整数数组 nums 表示每间房屋存放的现金金额。形式上,从左起第 i 间房屋中放有 nums[i] 美元。

另给你一个整数 k ,表示窃贼将会窃取的 最少 房屋数。小偷总能窃取至少 k 间房屋。

返回小偷的 最小 窃取能力。

示例 1:

输入:nums = [2,3,5,9], k = 2
输出:5
解释:
小偷窃取至少 2 间房屋,共有 3 种方式:
- 窃取下标 0 和 2 处的房屋,窃取能力为 max(nums[0], nums[2]) = 5 。
- 窃取下标 0 和 3 处的房屋,窃取能力为 max(nums[0], nums[3]) = 9 。
- 窃取下标 1 和 3 处的房屋,窃取能力为 max(nums[1], nums[3]) = 9 。
因此,返回 min(5, 9, 9) = 5 。

示例 2:

输入:nums = [2,7,9,3,1], k = 2
输出:2
解释:共有 7 种窃取方式。窃取能力最小的情况所对应的方式是窃取下标 0 和 4 处的房屋。返回 max(nums[0], nums[4]) = 2 。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 109
  • 1 <= k <= (nums.length + 1)/2

题意(版本 A)
说,有个小偷公司,给小偷定的 KPI 是偷至少 k 间房子,要求偷的房子不能相邻。

张三作为其中的一个小偷,他不想偷太多,否则一不小心就「数额巨大」,这可太刑了。所以张三计划,在他偷过的房子中,偷走的最大金额要尽量地小。

这个最小值是多少呢?

题意(版本 B)
给定数组 nums,从中选择一个长度至少为 k 的子序列 A,要求 A 中没有任何元素在 nums 中是相邻的。

最小化 max(A)。

方法一:二分+DP
有关二分的三种写法,请看【基础算法精讲 04】。本文采用开区间写法。

看到「最大化最小值」或者「最小化最大值」就要想到二分答案,这是一个固定的套路。

对于本题,「偷走的最大金额」越小,能偷的房子就越少,反之越多。例如 nums=[1,4,2,3],在最大金额为 2 时,nums 中只有 1 和 2 是可以偷的;在最大金额为 4 时,nums 中 1,2,3,4 都可以偷。

一般地,二分的值越小,越不能/能满足要求;二分的值越大,越能/不能满足要求。有单调性的保证,就可以二分答案了。

把二分中点 mid 记作 mx,仿照 198. 打家劫舍,定义 f[i] 表示从 nums[0] 到 nums[i] 中偷金额不超过 mx 的房屋,最多能偷多少间房屋。如果 f[n−1]≥k 则表示答案至多为 mx,否则表示答案必须超过 mx。

用「选或不选」来分类讨论:

不选 nums[i]:f[i]=f[i−1];
选 nums[i],前提是 nums[i]≤mx:f[i]=f[i−2]+1。
这两取最大值,即

f[i]=max(f[i−1],f[i−2]+1)
代码实现时,可以用两个变量滚动计算。具体请看【基础算法精讲 17】。

答疑
问:有没有可能,二分出来的答案,不在 nums 中?

答:不可能。二分出来的答案,一定在 nums 中。证明如下:

设答案为 ans,那么当最大金额为 ans 时,可以偷至少 k 间房子。如果 ans 不在 nums 中,那么当最大金额为 ans−1 时,也可以偷至少 k 间房子。这与二分算法相矛盾:根据视频中讲的红蓝染色法,循环结束时,ans 和 ans−1 的颜色必然是不同的,即 ans 可以满足题目要求,而 ans−1 不满足题目要求。所以,二分出来的答案,一定在 nums 中。

复杂度分析
  • 时间复杂度:O(nlogU),其中 n 为 nums 的长度,U=max(nums)。
  • 空间复杂度:O(1)。仅用到若干额外变量。
class Solution {
    public int minCapability(int[] nums, int k) {
        int left = 0, right = 0;
        for (int x : nums) {
            right = Math.max(right, x);
        }
        while (left + 1 < right) { // 开区间写法
            int mid = (left + right) >>> 1;
            if (check(nums, k, mid)) {
                right = mid;
            } else {
                left = mid;
            }
        }
        return right;
    }

    private boolean check(int[] nums, int k, int mx) {
        int f0 = 0, f1 = 0;
        for (int x : nums) {
            if (x > mx) {
                f0 = f1;
            } else {
                int tmp = f1;
                f1 = Math.max(f1, f0 + 1);
                f0 = tmp;
            }
        }
        return f1 >= k;
    }
}

如何保证选择的元素是非相邻的

check 方法中,通过两个状态变量 f0f1,确保选择的元素是非相邻的。这两个变量的含义如下:

  • f0: 表示在当前元素 x 没有被选中的情况下,最多可以选择的元素数量。
  • f1: 表示在当前元素 x 被选中的情况下,最多可以选择的元素数量。

通过以下的状态转移来确保选取的元素是非相邻的:

  1. 如果当前元素 x 大于给定的最大值 mx:

    • 不能选择该元素,因此只更新 f0f1,表示当前元素不被选中。
  2. 如果当前元素 x 小于或等于给定的最大值 mx:

    • 可以选择该元素或不选择:
      • 不选择当前元素: f0 保持不变。
      • 选择当前元素: 为了确保选择的元素是非相邻的,当前元素 x 可以被选中时,最多能选择的元素数量为 f0 + 1。因为选择 x 后,前一个元素不能被选中,所以使用 f0 而不是 f1
      • 然后用 Math.max(f1, f0 + 1) 来更新 f1
状态转移解释
  • f0 = f1: 当 x > mx 时,当前元素不能被选中,所以 f0 直接变为 f1(即前一个元素的状态),表示当前状态不选中任何元素。

  • f1 = Math.max(f1, f0 + 1):

    • f1 是选择当前元素 x 时的最大选择数目。如果选择当前元素 x,我们需要确保它与之前选择的元素不相邻。因此使用 f0 + 1 来计算选择当前元素的情况(f0 表示前一个元素未被选中)。
    • Math.max(f1, f0 + 1) 选择不选择当前元素 x 的最优解。
  • f0 = tmp: 这里将 f0 更新为上一次的 f1,这样确保如果下一个元素需要考虑时,当前元素未被选中的情况已经被记录下来。

class Solution {
    public int minCapability(int[] nums, int k) {
        // 定义二分查找的边界
        int lower = Arrays.stream(nums).min().getAsInt(); // 数组中的最小值
        int upper = Arrays.stream(nums).max().getAsInt(); // 数组中的最大值
        
        // 进行二分查找
        while (lower <= upper) {
            int middle = (lower + upper) / 2; // 计算中间值
            int count = 0; // 计数器,用于记录选择的元素数量
            boolean visited = false; // 标记变量,表示上一个元素是否已被选择
            
            // 遍历数组,统计在不选相邻元素的情况下,选取<=middle的元素个数
            for (int x : nums) {
                if (x <= middle && !visited) { // 当前元素小于等于middle并且上一个元素没有被选中
                    count++; // 选择当前元素
                    visited = true; // 标记当前元素已被选择
                } else {
                    visited = false; // 不选择当前元素,重置标记
                }
            }

            // 根据选择的元素数量调整二分查找的范围
            if (count >= k) {
                upper = middle - 1; // 说明可以选更多的较小元素,因此缩小上边界
            } else {
                lower = middle + 1; // 说明选取的元素不足,因此扩大下边界
            }
        }
        
        return lower; // 返回符合条件的最小能力值
    }
}

代码如何确保不选择相邻数组元素

在这段代码中,关键在于变量 visited 的使用。visited 用来记录上一个元素是否已被选择,这样可以确保不会选择相邻的元素。

  1. 遍历数组:在遍历数组时,每次检查当前元素 x 是否小于等于 middle(当前二分查找中间值),且前一个元素是否未被选择(visitedfalse)。

  2. 选择条件:如果满足这两个条件(x <= middle && !visited),则选择当前元素,并将 visited 设置为 true,表示已经选择了一个元素。这样一来,下一次循环中,如果当前元素的下一个元素也满足 x <= middle,由于 visitedtrue,这个下一个元素就不会被选中,确保不选相邻元素。

  3. 跳过相邻元素:如果不满足选择条件或者已经选择了一个元素(visitedtrue),则将 visited 设置为 false,表示当前元素没有被选择,这样可以在下一个非相邻元素的检查中重新选择。

 最大值最小化

No.1

2517.礼盒的最大甜蜜度. - 力扣(LeetCode)

给你一个正整数数组 price ,其中 price[i] 表示第 i 类糖果的价格,另给你一个正整数 k 。

商店组合 k 类 不同 糖果打包成礼盒出售。礼盒的 甜蜜度 是礼盒中任意两种糖果 价格 绝对差的最小值。

返回礼盒的 最大 甜蜜度

示例 1:

输入:price = [13,5,1,8,21,2], k = 3
输出:8
解释:选出价格分别为 [13,5,21] 的三类糖果。
礼盒的甜蜜度为 min(|13 - 5|, |13 - 21|, |5 - 21|) = min(8, 8, 16) = 8 。
可以证明能够取得的最大甜蜜度就是 8 。

示例 2:

输入:price = [1,3,1], k = 2
输出:2
解释:选出价格分别为 [1,3] 的两类糖果。 
礼盒的甜蜜度为 min(|1 - 3|) = min(2) = 2 。
可以证明能够取得的最大甜蜜度就是 2 。

示例 3:

输入:price = [7,7,7,7], k = 2
输出:0
解释:从现有的糖果中任选两类糖果,甜蜜度都会是 0 。

提示:

  • 2 <= k <= price.length <= 105
  • 1 <= price[i] <= 109

思路
「任意两种糖果价格绝对差的最小值」等价于「排序后,任意两种相邻糖果价格绝对差的最小值」。

如果题目中有「最大化最小值」或者「最小化最大值」,一般都是二分答案,请记住这个套路。

为什么?对于本题来说,甜蜜度越大,能选择的糖果越少,有单调性,所以可以二分。

定义 f(d) 表示甜蜜度至少为 d 时,至多能选多少类糖果。

二分答案 d:

如果 f(d)≥k,说明答案至少为 d。
如果 f(d)<k,说明答案至多为 d−1。
二分结束后,设答案为 d0 ,那么 f(d0)≥k 且 f(d0+1)<k。
如何计算 f(d)?对 price 从小到大排序,贪心地计算 f(d):从 price[0] 开始选;假设上一个选的数是 pre,那么当 price[i]≥pre+d 时,才可以选 price[i]。

二分下界可以取 1,上界可以取 ⌊k−1max(price)−min(price) ⌋,这是因为最小值不会超过平均值。(平均值指选了 price 最小最大以及中间的一些糖果,相邻糖果差值的平均值。)

请注意,二分的区间的定义是:尚未确定 f(d) 与 k 的大小关系的 d 的值组成的集合(范围)。在区间左侧外面的 d 都是 f(d)≥k 的,在区间右侧外面的 d 都是 f(d)<k 的。在理解二分时,请牢记区间的定义及其性质。

答疑
问:为什么二分出来的答案,一定来自数组中价格的差?有没有可能,二分出来的答案不是任何价格的差?

答:反证法。如果答案 d 不是任何价格的差,也就是说,礼盒中任意两种糖果的价格的绝对差都大于 d,也就是大于等于 d+1。那么对于 d+1 来说,它也可以满足 f(d + 1) == true,这与循环不变量相矛盾。
class Solution {
    public int maximumTastiness(int[] price, int k) {
        Arrays.sort(price);

        // 二分模板·其三(开区间写法)https://www.bilibili.com/video/BV1AP41137w7/
        int left = 0, right = (price[price.length - 1] - price[0]) / (k - 1) + 1;
        while (left + 1 < right) { // 开区间不为空
            // 循环不变量:
            // f(left) >= k
            // f(right) < k
            int mid = left + (right - left) / 2;
            if (f(price, mid) >= k) left = mid; // 下一轮二分 (mid, right)
            else right = mid; // 下一轮二分 (left, mid)
        }
        return left;
    }

    private int f(int[] price, int d) {
        int cnt = 1, pre = price[0];
        for (int p : price) {
            if (p >= pre + d) {
                cnt++;
                pre = p;
            }
        }
        return cnt;
    }
}
复杂度分析
时间复杂度:O(nlogn+nlogU),其中 n 为 price 的长度,U=⌊
k−1
max(price)−min(price)

 ⌋。
空间复杂度:O(1),忽略排序的空间,仅用到若干额外变量。

class Solution {
    public int maximumTastiness(int[] price, int k) {
        Arrays.sort(price);  // 对价格数组进行排序,确保价格从小到大排列

        // 初始化二分查找的左右边界
        int left = 0;  // 最小美味度(差值)为0
        int right = (price[price.length - 1] - price[0]) / (k - 1) + 1;  // 最大美味度(差值)初始值

        // 开始二分查找
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;  // 计算中间值mid,代表当前尝试的美味度
            if (f(price, mid) >= k) {  // 如果能找到至少k个满足条件的价格
                left = mid;  // 更新左边界
            } else {
                right = mid;  // 更新右边界
            }
        }

        return left;  // 返回最大可能的美味度
    }

    // 辅助函数:计算在当前差值d下,能选择的最大元素数量
    private int f(int[] price, int d) {
        int cnt = 1;  // 已选中的元素数量,初始化为1(默认选择第一个元素)
        int pre = price[0];  // 记录前一个选中的价格

        // 遍历价格数组,从第二个元素开始
        for (int p : price) {
            if (p >= pre + d) {  // 如果当前价格p与之前选中的价格pre的差值>=d
                cnt++;  // 选择当前价格
                pre = p;  // 更新前一个选中的价格
            }
        }

        return cnt;  // 返回能够选择的价格数量
    }
}

代码逻辑说明

  1. 数组排序:首先对输入的 price 数组进行排序,这样在后续的操作中,价格是从低到高的顺序排列的,这样可以简化逻辑处理。

  2. 二分查找最大美味度

    • 初始化 left 为 0,这是美味度的最小可能值。
    • 初始化 right(price[price.length - 1] - price[0]) / (k - 1) + 1,这是美味度的最大可能值。这里的计算方式确保了即使在最理想的情况下(即价格分布均匀的情况下),也不会错过最大美味度。
    • 使用二分查找法在 leftright 之间寻找最大可能的美味度值。
  3. 辅助函数 f(int[] price, int d)

    • 该函数用于计算在当前差值 d 下,最多可以选择多少个元素,确保每两个被选中的元素的差值至少为 d
    • 使用变量 cnt 来记录当前能够选择的元素数量,初始值为1,因为默认选择第一个元素。
    • 遍历 price 数组,如果当前价格与上一个选中价格的差值大于等于 d,就可以选择当前元素,并更新上一个选中价格的值。
    • 最后返回能选择的最大元素数量 cnt

 第K小/大

No.1

378.有序矩阵中第K小的元素. - 力扣(LeetCode)

给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。

你必须找到一个内存复杂度优于 O(n2) 的解决方案。

示例 1:

输入:matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8
输出:13
解释:矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13

示例 2:

输入:matrix = [[-5]], k = 1
输出:-5

提示:

  • n == matrix.length
  • n == matrix[i].length
  • 1 <= n <= 300
  • -109 <= matrix[i][j] <= 109
  • 题目数据 保证 matrix 中的所有行和列都按 非递减顺序 排列
  • 1 <= k <= n2
class Solution {
    public int kthSmallest(int[][] matrix, int k) {
        int rows = matrix.length, columns = matrix[0].length;
        int[] sorted = new int[rows * columns];
        int index = 0;
        for (int[] row : matrix) {
            for (int num : row) {
                sorted[index++] = num;
            }
        }
        Arrays.sort(sorted);
        return sorted[k - 1];
    }
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值