给你一个下标从 0 开始的整数数组 nums
和一个整数 k
。
你可以对数组执行 至多 k
次操作:
- 从数组中选择一个下标
i
,将nums[i]
增加 或者 减少1
。
最终数组的频率分数定义为数组中众数的 频率 。
请你返回你可以得到的 最大 频率分数。
众数指的是数组中出现次数最多的数。一个元素的频率指的是数组中这个元素的出现次数。
示例 1:
输入:nums = [1,2,6,4], k = 3 输出:3 解释:我们可以对数组执行以下操作: - 选择 i = 0 ,将 nums[0] 增加 1 。得到数组 [2,2,6,4] 。 - 选择 i = 3 ,将 nums[3] 减少 1 ,得到数组 [2,2,6,3] 。 - 选择 i = 3 ,将 nums[3] 减少 1 ,得到数组 [2,2,6,2] 。 元素 2 是最终数组中的众数,出现了 3 次,所以频率分数为 3 。 3 是所有可行方案里的最大频率分数。
示例 2:
输入:nums = [1,4,4,2,4], k = 0 输出:3 解释:我们无法执行任何操作,所以得到的频率分数是原数组中众数的频率 3 。
提示:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^9
0 <= k <= 10^14
提示 1
If you sort the original array, it is optimal to apply the operations on one subarray such that all the elements of that subarray become equal.
提示 2
You can use binary search to find the longest subarray where we can make the elements equal in at most k
operations.
解法1:滑动窗口 + 前缀和
问题描述
给定一个整数数组nums
和一个整数k
,我们可以对数组中的任意元素执行至多k
次操作,每次操作可以增加或减少该元素的值1。我们的目标是最大化数组中众数的频率,即出现次数最多的元素的个数。
问题讲解
将 nums 的所有元素变为 nums 的中位数是最优的。
证明:设 nums 的长度为 n,设要将所有 nums[i] 变为 x。假设 nums 已经从小到大排序。首先,如果 x 取在区间 [nums[0],nums[n−1]] 之外,那么 x 向区间方向移动可以使距离和变小;同时,如果 x 取在区间 [nums[0],nums[n−1]] 之内,无论如何移动 x,它到 nums[0] 和 nums[n−1] 的距离和都是一个定值 nums[n−1]−nums[0],那么去掉 nums[0] 和 nums[n−1] 这两个最左最右的数,问题规模缩小。不断缩小问题规模,如果最后剩下 1 个数,那么 x 就取它;如果最后剩下 2 个数,那么 x 取这两个数之间的任意值都可以(包括这两个数)。因此 x 可以取 nums[n/2]。
把数组排序后,要变成一样的数必然在一个连续子数组中,那么用滑动窗口来做,枚举子数组的右端点 right,然后维护子数组的左端点 left。
根据中位数贪心,最优做法是把子数组内的元素都变成子数组的中位数,操作次数如果超过 k,就必须移动左端点。
求出数组的前缀和,就可以 O(1) 算出操作次数了
解题思路
-
数组排序:首先,我们需要对数组
nums
进行排序。排序后的数组有助于我们确定操作的策略,因为最优解往往与数组的中位数有关。 -
中位数策略:排序后,最优的策略是将尽可能多的元素调整到中位数的值。因为中位数两边的元素向中位数调整所需的总变动次数是最小的。
-
滑动窗口:使用滑动窗口的方法,我们可以模拟从数组的一端到另一端,逐步确定哪些元素可以被调整,以及调整到哪个值。
-
前缀和:为了快速计算窗口内元素调整到特定值所需的总操作次数,我们使用前缀和数组来优化这个过程。
详细步骤
-
数组排序:对输入数组
nums
进行排序。 -
计算前缀和:构建一个前缀和数组
presum
,其中presum[i]
表示从数组开始到索引i
的元素的累积和。 -
初始化变量:设置
left
指针指向窗口的起始位置,ans
用于记录最大频率分数。 -
遍历数组:遍历数组
nums
,使用right
指针从0到n-1
(数组长度减1)。 -
确定中位数:在每次迭代中,确定当前窗口的中位数。由于窗口是
[left, right]
,中位数可以通过(left + right) // 2
计算得出。 -
计算操作次数:计算将窗口内所有元素调整到中位数所需的操作次数。这可以通过计算窗口两端元素与中位数差值的和来实现。
-
更新窗口:如果所需的操作次数超过
k
,说明窗口太大,需要通过增加left
指针的值来缩小窗口,然后重新计算操作次数。 -
更新最大频率:如果当前窗口的操作次数不超过
k
,则更新ans
为窗口宽度(i - left + 1)
的最大值。 -
返回结果:遍历结束后,返回
ans
作为最终结果。
Java版:
class Solution {
public int maxFrequencyScore(int[] nums, long k) {
Arrays.sort(nums);
int n = nums.length;
long[] presum = new long[n + 1];
for (int i = 0; i < n; i++) {
presum[i + 1] = presum[i] + nums[i];
}
int left = 0;
int ans = 0;
for (int i = 0; i < n; i++) {
int mid = left + (i - left) / 2;
long distanceSum = (long) nums[mid] * (mid - left) - (presum[mid] - presum[left]) + presum[i + 1] - presum[mid + 1] - (long) nums[mid] * (i - mid);
while (left <= i && distanceSum > k) {
left++;
mid = left + (i - left) / 2;
distanceSum = (long) nums[mid] * (mid - left) - (presum[mid] - presum[left]) + presum[i + 1] - presum[mid + 1] - (long) nums[mid] * (i - mid);
}
ans = Math.max(ans, i - left + 1);
}
return ans;
}
}
Python3版:
class Solution:
def maxFrequencyScore(self, nums: List[int], k: int) -> int:
nums.sort()
n = len(nums)
presum = [0] * (n + 1)
for i in range(n):
presum[i + 1] = presum[i] + nums[i]
ans = 0
left = 0
for i in range(n):
mid = (i + left) // 2
distanceSum = nums[mid] * (mid - left) - (presum[mid] - presum[left]) + presum[i + 1] - presum[mid + 1] - nums[mid] * (i - mid)
while distanceSum > k:
left += 1
mid = (i + left) // 2
distanceSum = nums[mid] * (mid - left) - (presum[mid] - presum[left]) + presum[i + 1] - presum[mid + 1] - nums[mid] * (i - mid)
ans = max(ans, i - left + 1)
return ans
复杂度分析
- 时间复杂度:O(nlogn),其中 n 为 nums 的长度。瓶颈在排序上。
- 空间复杂度:O(n)。