Leetcode每日一题_滑动窗口题目

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 1nums.length105
  • 1 ≤ n u m s [ i ] ≤ 1 0 5 1 \le nums[i] \le 10^5 1nums[i]105
  • 1 ≤ k ≤ 1 0 5 1 \le k \le 10^5 1k105

算法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 (xan)+(xan1)+...+(xak)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; 
    }
};
时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
空间复杂度 O ( l o g n ) O(logn) O(logn) 排序栈空间所需

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值