Leetcode每日一题_1838.最高频元素的频数
题目链接: Leetcode 1838. 最高频元素的频数
题目
元素的 频数 是该元素在一个数组中出现的次数。
给你一个整数数组 nums 和一个整数 k 。在一步操作中,你可以选择 nums 的一个下标,并将该下标对应元素的值增加 1 。
执行最多 k 次操作后,返回数组中最高频元素的 最大可能频数 。
提示:
- 1 ≤ n u m s . l e n g t h ≤ 1 0 5 1 \le nums.length \le 10^5 1≤nums.length≤105
- 1 ≤ n u m s [ i ] ≤ 1 0 5 1 \le nums[i] \le 10^5 1≤nums[i]≤105
- 1 ≤ k ≤ 1 0 5 1 \le k \le 10^5 1≤k≤105
算法1 (排序 + 前缀和 + 二分)
思路分析
由于每次操作只能对其中一个数 +1, 问在操作次数不超过k的情况下, 该数组的最大频数。 这个最大的频数对应的元素必定就是原本数组中就有的, 不可能是比原本数组中最大的数还大的. 这里我们按升序排序元素, 同时由于操作次数有限制, 故我们需要在原本的数组中找到一个元素x, 其中 a 0 , a 1 , a 2 , . . . a n a_0, a_1, a_2, ... a_n a0,a1,a2,...an 为小于x的元素, 在满足 ( x − a n ) + ( x − a n − 1 ) + . . . + ( x − a k ) ≤ k (x - a_n) + (x - a_{n-1}) +...+(x - a_{k}) \le k (x−an)+(x−an−1)+...+(x−ak)≤k , 我们要使得 n - k + 1 的长度最长, 即在k次的操作中转变为x的元素个数最多. 故如果我们经过排序之后这样的一个数x, 我们要找的频数, 必定是由连续的一段区间组成. 考虑如何找到这个数x, 这就需要枚举了, 以x为右边界时, 在操作次数不超过k的情况下,求这段区间的左边界, 如果直接线性查找左边界, 那么时间复杂度为 O( n 2 n^2 n2),显然会超时, 故这里由于固定有边界找左边界, 左边界越靠左那么将这段区间转变为x的代价必然越大, 故这里满足我们使用二分的条件, 故这里我们可以使用二分来查找左边界. 那么如何二分的条件如何判断呢, 如何求出这段区间所需的代价呢? 区间的代价我们可以这样计算, 对于一段[l, r]区间, 其有边界对应的值为 x, 那么我们是想将这段区间的值都变为x, 那么这段区间的总和就是 x * (r - l + 1), 那假设目前这段[l, r]区间的总和为s, 那么此时以l为左边界的代价就是 x * (r - l + 1) - s, 我们只需判断 x * (r - l + 1) - s <= k 即可.快速求出一段区间的和我们这里可以用前缀和技巧.
C++ 代码
class Solution
{
public:
static constexpr int N = 1e+5 + 10;
long long f[N];
int maxFrequency(vector<int>& nums, int k)
{
memset(f, 0, sizeof f);
int n = nums.size();
int res = 1;
sort(nums.begin(), nums.end());
for(int i = 1; i <= n; ++i) f[i] = f[i - 1] + nums[i - 1]; // 计算前缀和
for(int i = 1; i < n; ++i)
{
int l = 0, r = i;
while(r > l) // 二分查找
{
int mid = l + r >> 1;
// 如果此时以mid为左边界的代价 <= k, 说明我们左边界可以继续往左移,故将r更新为mid
if((long long)nums[i] * (i - mid + 1) - (f[i + 1] - f[mid]) <= k) r = mid;
else l = mid + 1;
}
res = max(res, i - l + 1);
}
return res;
}
};
时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
空间复杂度 O ( n ) O(n) O(n)
算法2 (排序 + 滑动窗口)
其实如果能想到算法1的思路应该就能想到用滑动窗口, 我们由算法1的分析思路得知, 我们将数组排序,遍历排序后数组每个元素 x 作为目标值,并求出此时可以改变至目标值的元素左边界。 我们算法1求左边界是用的二分, 但其实的话我们如果是从左到右遍历每一个元素, 那么每个元素其对应的左边界应该是一个非递减的, 因为不可能存在 a i + 1 a_{i+1} ai+1元素对应的左边界 l i + 1 l_i+1 li+1 < 元素 a i a_{i} ai 对应的左边界 l i l_i li, 因为如果这样真的存在我们必定可以使得 a i a_i ai对应的左边界变为 l i + 1 l_{i+1} li+1,且这样代价不会超过k,这样显然矛盾. 故得知了这个左边界是只加不减的, 故这样就是一个滑动窗口了, 滑动窗口维护一个左边界和一个右边界, 然后在维护这个窗口的和, 每次由右边加入一个数x, 此时右边界 +1 更新, 更新后的右边界r, 接着继续判断 (r - l + 1) * x - 上个区间的区间和 <= k; 若不满足说明此时这个右边界对应的左边界应该前进.
思路分析
C++ 代码
class Solution
{
public:
int maxFrequency(vector<int>& nums, int k)
{
int n = nums.size();
sort(nums.begin(), nums.end());
long long sum = nums[0];
int res = 1;
for(int i = 1, j = 0; i < n; ++i)
{
sum += nums[i];
while((long long)nums[i] * (i - j + 1) - sum > k) sum -= nums[j++];
res = max(res, i - j + 1);
}
return res;
}
};