摘要: 一个比较综合的简单题
【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings
各位好,本文我们来看力扣上的一道我认为不错的题目,2389. 和有限的最长子序列,本题在力扣上标记为简单题,但该题综合了排序、贪心、前缀和、二分等众多基础算法,最终代码很短,按照 【连载】面试好题 的观点,非常适合面试。
题目
给你一个长度为 n 的整数数组 nums ,和一个长度为 m 的整数数组 queries 。
返回一个长度为 m 的数组 answer ,其中 answer[i] 是 nums 中 元素之和小于等于 queries[i] 的 子序列 的 最大 长度 。
子序列 是由一个数组删除某些元素(也可以不删除)但不改变剩余元素顺序得到的一个数组。
提示:
1 | n == nums.length |
示例 1:
输入:nums = [4,5,2,1], queries = [3,10,21]
输出:[2,3,4]
解释:queries 对应的 answer 如下:
- 子序列 [2,1] 的和小于或等于 3 。可以证明满足题目要求的子序列的最大长度是 2 ,所以 answer[0] = 2 。
- 子序列 [4,5,1] 的和小于或等于 10 。可以证明满足题目要求的子序列的最大长度是 3 ,所以 answer[1] = 3 。
- 子序列 [4,5,2,1] 的和小于或等于 21 。可以证明满足题目要求的子序列的最大长度是 4 ,所以 answer[2] = 4 。
示例 2:
输入:nums = [2,3,4,5], queries = [1]
输出:[0]
解释:空子序列是唯一一个满足元素和小于或等于 1 的子序列,所以 answer[0] = 0 。
题解
算法:排序 + 贪心 + 前缀和 + 二分
由于我们选的是子序列,并且是考察子序列的和,因此选出的子序列的顺序不重要。因此给定了 queries[i]
之后,我们要选出的是 nums 这 n 个元素的子集,使得子集和小于 queries[i]
的前提下的元素个数最大。
下面的问题就是给定这个子集怎么选。直觉上应该从最小的开始选,假设当前要选的子集和不超过 q = queries[i]
,最终选出的 k 个元素构成的子集为 {a1,…,ak},其和为 s,那么 s≤q 且 nums 中剩余的元素最大为 q−s−1。
如果此时 nums 中存在一个元素 x,比选出的 k 个元素中的最大的(假设为 ak)小,也就是 x<ak,那么把 ak 替换为 x,所得新子集的长度仍为 k,和为 s−ak+x,但 nums 剩余元素中可能的最大值 q−s−1 就有可能大于 q−s+ak−x,那么就可以将该值也选出来放到子集中,这样自己长度就增加到 k + 1。
以上是贪心正确性证明中的范围缩放法:
范围缩放法
证明任何对局部最优策略作用范围的扩展都不会造成整体结果变差。
因此先对 nums 从小到大排序,对于 q = queries[i]
,然后从小到大遍历 nums,每遍历到一个 nums[i]
,就将该元素累加到 sum 中,直至 sum > q
结束。
由于有 m 次查询,因此我们可以将排序后的 nums 的前缀和 sums
预处理出来,sums[i]
表示 nums[0..i-1]
的和。这样就不用每次查询都来一次遍历求和的过程了。
对于 q = queries[i]
,我们只需要在 sums 中二分地找到小于等于 q 的最大值即可。
预处理的时间复杂度 O(nlogn),查询的时间复杂度 O(mlogn),总时间复杂度 O(m+n)logn。
代码 (C++)
1 | class Solution { |