每日算法-250407
记录一下今天刷的三道 LeetCode 题目。
2389. 和有限的最长子序列
题目
思路
排序 + 前缀和 + 二分查找
解题过程
- 理解题意: 题目要求我们对于
queries
数组中的每个查询值q
,找出nums
数组中元素和 小于等于q
的 最长子序列 的长度。注意,是子序列,不是子数组,意味着我们可以任意选择元素,不必连续。 - 贪心策略: 为了让子序列尽可能长,我们应该优先选择
nums
中较小的元素。因此,第一步是对nums
数组进行 排序。 - 前缀和: 排序后,为了快速计算前
k
个最小元素的和,我们可以计算nums
数组的 前缀和。我们将前缀和直接存储在排序后的nums
数组中以节省空间(nums[i]
存储原nums[0]
到nums[i]
的和)。 - 二分查找: 现在,对于每个查询
q = queries[i]
,我们需要在前缀和数组(即修改后的nums
数组)中找到最大的索引k
,使得nums[k] <= q
。这等价于查找第一个 严格大于q
的前缀和nums[index]
的位置index
。这个index
就是满足条件的最长子序列的长度(因为索引从 0 开始,index
个元素对应的前缀和下标是index-1
,所以第一个大于q
的位置index
正好是满足<=q
的元素个数)。 - 实现: 我们可以使用二分查找(
check
函数,寻找下界lower_bound
的变种)来找到第一个大于q
的位置。具体地,我们查找q + 1
的下界,其返回的left
值即为所求的长度index
。 - 结果: 将每个查询得到的长度
index
存回queries
数组的相应位置。
复杂度
- 时间复杂度:
O
(
N
log
N
+
M
log
N
)
O(N \log N + M \log N)
O(NlogN+MlogN)
N
是nums
的长度,M
是queries
的长度。- 排序
nums
需要 O ( N log N ) O(N \log N) O(NlogN)。 - 计算前缀和需要 O ( N ) O(N) O(N)。
- 对于
M
个查询,每个查询进行一次二分查找,需要 O ( log N ) O(\log N) O(logN)。总共是 O ( M log N ) O(M \log N) O(MlogN)。 - 总时间复杂度为 O ( N log N + M log N ) O(N \log N + M \log N) O(NlogN+MlogN)。
- 空间复杂度:
O
(
1
)
O(1)
O(1) (或
O
(
log
N
)
O(\log N)
O(logN) 取决于排序算法)
- 我们在原地修改了
nums
数组来存储前缀和,并在原地修改了queries
数组来存储结果,因此辅助空间复杂度是 O ( 1 ) O(1) O(1) (不考虑排序可能产生的栈空间)。
- 我们在原地修改了
Code
class Solution {
public int[] answerQueries(int[] nums, int[] queries) {
Arrays.sort(nums);
for (int i = 1; i < nums.length; i++) {
nums[i] += nums[i - 1];
}
for (int i = 0; i < queries.length; i++) {
int index = check(nums, queries[i] + 1);
queries[i] = index;
}
return queries;
}
private int check(int[] arr, int k) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] < k) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
}
1385. 两个数组间的距离值
题目
思路
排序
arr2
+ 二分查找
解题过程
- 理解题意: 我们需要计算
arr1
中有多少个元素x
,满足以下条件:对于 所有arr2
中的元素y
,都有|x - y| > d
。换句话说,arr1
中的元素x
如果要计入结果,那么在arr2
中 不能存在 任何元素y
使得|x - y| <= d
,即不能存在y
位于闭区间[x - d, x + d]
内。 - 优化查找: 为了高效地检查
arr2
中是否存在位于[x - d, x + d]
区间的元素,我们可以先对arr2
排序。 - 二分查找: 遍历
arr1
中的每个元素x
。对于当前的x
,我们要在已排序的arr2
中查找是否存在元素y
满足x - d <= y <= x + d
。- 我们可以使用二分查找找到
arr2
中第一个 大于等于x - d
的元素的位置index
(即查找x - d
的下界lower_bound
)。 - 如果
index == arr2.length
,说明arr2
中所有元素都小于x - d
,因此不可能有元素落在[x - d, x + d]
区间内。此时x
符合条件。 - 如果
index < arr2.length
,说明arr2[index]
是arr2
中第一个(即最小的)大于等于x - d
的元素。我们还需要检查这个元素是否也 小于等于x + d
。- 如果
arr2[index] <= x + d
,则说明arr2[index]
正好落在[x - d, x + d]
区间内,x
不符合条件。 - 如果
arr2[index] > x + d
,则说明arr2
中最小的不小于x - d
的元素都已经大于x + d
了,因此arr2
中没有元素落在[x - d, x + d]
区间内。此时x
符合条件。
- 如果
- 我们可以使用二分查找找到
- 计数: 维护一个计数器
ret
,当x
符合条件时(即index == arr2.length
或arr2[index] > x + d
),将其加一。 - 返回: 遍历完
arr1
后,返回ret
。
复杂度
- 时间复杂度:
O
(
M
log
M
+
N
log
M
)
O(M \log M + N \log M)
O(MlogM+NlogM)
N
是arr1
的长度,M
是arr2
的长度。- 排序
arr2
需要 O ( M log M ) O(M \log M) O(MlogM)。 - 遍历
arr1
(N
次),每次在arr2
中进行二分查找,需要 O ( log M ) O(\log M) O(logM)。总共是 O ( N log M ) O(N \log M) O(NlogM)。 - 总时间复杂度为 O ( M log M + N log M ) O(M \log M + N \log M) O(MlogM+NlogM)。
- 空间复杂度:
O
(
1
)
O(1)
O(1) (或
O
(
log
M
)
O(\log M)
O(logM) 取决于排序算法)
- 排序
arr2
通常可以原地完成,二分查找使用常数额外空间。辅助空间复杂度为 O ( 1 ) O(1) O(1)。
- 排序
Code
class Solution {
public int findTheDistanceValue(int[] arr1, int[] arr2, int d) {
int ret = 0;
Arrays.sort(arr2);
for (int x : arr1) {
int index = check(arr2, x - d);
if (index == arr2.length || arr2[index] > x + d) {
ret++;
}
}
return ret;
}
private int check(int[] arr, int t) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] < t) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
}
2300. 咒语和药水的成功对数
题目
思路
二分查找
解题过程
- 理解题意: 给定咒语强度数组
spells
和药水强度数组potions
,以及一个成功阈值success
。对于每个咒语spells[i]
,需要计算有多少种药水potions[j]
使得它们的乘积spells[i] * potions[j] >= success
。返回一个数组pairs
,其中pairs[i]
是第i
个咒语对应的成功药水数量。 - 优化查找: 对于固定的
spells[i]
,我们需要在potions
中找到所有满足potions[j] >= success / spells[i]
的potions[j]
的数量。如果potions
是无序的,我们需要遍历整个potions
数组,时间复杂度较高。 - 排序与二分: 我们可以先对
potions
数组进行 排序。排序后,对于给定的spells[i]
,所有满足条件的potions[j]
会集中在数组的右侧。 - 查找边界: 我们需要找到第一个(即强度最小的)满足
spells[i] * potions[j] >= success
的药水potions[j]
的索引index
。可以使用二分查找来完成。- 注意:乘积
spells[i] * potions[j]
可能 超过int
的范围,因此在比较时应使用long
类型进行计算。 - 我们的目标是找到最小的
j
使得(long)spells[i] * potions[j] >= success
。这相当于在potions
数组中查找success / spells[i]
(可能需要向上取整,或直接用乘法比较) 的下界lower_bound
。 check
函数实现的正是找到第一个mid
使得(long)potions[mid] * x >= success
的位置left
。
- 注意:乘积
- 计算数量: 一旦找到这个最小索引
index
,由于potions
已经排序,从potions[index]
到数组末尾的所有药水都将满足条件。因此,成功的药水数量就是m - index
,其中m
是potions
数组的长度。 - 结果: 对每个
spells[i]
重复步骤 4 和 5,将计算出的数量存入结果数组ret
的对应位置。
复杂度
- 时间复杂度:
O
(
M
log
M
+
N
log
M
)
O(M \log M + N \log M)
O(MlogM+NlogM)
N
是spells
的长度,M
是potions
的长度。- 排序
potions
需要 O ( M log M ) O(M \log M) O(MlogM)。 - 遍历
spells
(N
次),每次在potions
中进行二分查找,需要 O ( log M ) O(\log M) O(logM)。总共是 O ( N log M ) O(N \log M) O(NlogM)。 - 总时间复杂度为 O ( M log M + N log M ) O(M \log M + N \log M) O(MlogM+NlogM)。
- 空间复杂度:
O
(
N
)
O(N)
O(N) (或
O
(
N
+
log
M
)
O(N + \log M)
O(N+logM))
- 需要一个长度为
N
的数组ret
来存储结果。 - 排序
potions
可能需要 O ( log M ) O(\log M) O(logM) 的栈空间(取决于实现)。 - 主要空间开销来自于结果数组,为 O ( N ) O(N) O(N)。
- 需要一个长度为
Code
class Solution {
public int[] successfulPairs(int[] spells, int[] potions, long success) {
int n = spells.length, m = potions.length;
int[] ret = new int[n];
Arrays.sort(potions);
for (int i = 0; i < n; i++) {
long t = (success - 1) / spells[i];
int index = check(potions, (int)t, m);
ret[i] = m - index;
}
return ret;
}
private int check(int[] potions, int t, int m) {
int left = 0, right = m - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (potions[mid] < t + 1) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
}