想成功先发疯,不顾一切向前冲
二分查找
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
提示:
- 你可以假设
nums
中的所有元素是不重复的。 n
将在[1, 10000]
之间。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
。 - 如果
left
和right
都很大(接近Integer.MAX_VALUE
),left + right
可能会超过这个最大值,导致整数溢出,计算错误。
No.2
给你一个按照非递减顺序排列的整数数组
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
不在数组中。
-
lowerBound
返回的是大于或等于target
的第一个位置:lowerBound(nums, target)
返回的是第一个不小于target
的元素的索引。- 如果所有元素都小于
target
,lowerBound
会返回数组长度nums.length
。 - 如果返回的索引指向的值不等于
target
,则说明数组中不存在target
。
-
边界条件处理:
- 如果
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
,该数组按非递减顺序排序,以及一个字符target
。letters
里至少有两个不同的字符。返回
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
是一个正整数。
-
如果
num
是d
的倍数: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
小时内吃掉所有香蕉的最小速度k
(k
为整数)。示例 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)
给你一个整数数组 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
方法中,通过两个状态变量 f0
和 f1
,确保选择的元素是非相邻的。这两个变量的含义如下:
f0
: 表示在当前元素x
没有被选中的情况下,最多可以选择的元素数量。f1
: 表示在当前元素x
被选中的情况下,最多可以选择的元素数量。
通过以下的状态转移来确保选取的元素是非相邻的:
-
如果当前元素
x
大于给定的最大值mx
:- 不能选择该元素,因此只更新
f0
为f1
,表示当前元素不被选中。
- 不能选择该元素,因此只更新
-
如果当前元素
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
用来记录上一个元素是否已被选择,这样可以确保不会选择相邻的元素。
-
遍历数组:在遍历数组时,每次检查当前元素
x
是否小于等于middle
(当前二分查找中间值),且前一个元素是否未被选择(visited
为false
)。 -
选择条件:如果满足这两个条件(
x <= middle && !visited
),则选择当前元素,并将visited
设置为true
,表示已经选择了一个元素。这样一来,下一次循环中,如果当前元素的下一个元素也满足x <= middle
,由于visited
是true
,这个下一个元素就不会被选中,确保不选相邻元素。 -
跳过相邻元素:如果不满足选择条件或者已经选择了一个元素(
visited
为true
),则将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; // 返回能够选择的价格数量
}
}
代码逻辑说明
-
数组排序:首先对输入的
price
数组进行排序,这样在后续的操作中,价格是从低到高的顺序排列的,这样可以简化逻辑处理。 -
二分查找最大美味度:
- 初始化
left
为 0,这是美味度的最小可能值。 - 初始化
right
为(price[price.length - 1] - price[0]) / (k - 1) + 1
,这是美味度的最大可能值。这里的计算方式确保了即使在最理想的情况下(即价格分布均匀的情况下),也不会错过最大美味度。 - 使用二分查找法在
left
和right
之间寻找最大可能的美味度值。
- 初始化
-
辅助函数
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];
}
}