执行操作后元素的最高频率1 2(LeetCode 3346 3347)
1 介绍
本文会介绍执行操作后元素的最高频率1 & 2的思路以及解法,使用的代码包括C++
、Java
和Go
。
2 执行操作后元素的最高频率1
2.1 原题
2.2 思路
由于排序不影响结果,并且排序能更好的符合题目的“增加范围”特性,因此先排序处理。
题目本质上是找一个数x
,x
可以在nums
中也可以不在nums
中,求[x-k,x+k]
的最大长度。
虽然x
可以在nums
中,也可以不在nums
中,但是x
的边界是确定的,最小值是min(nums)
,最大值是max(nums)
。因为假如能调整出了nums
的边界,为什么不直接取边界值?比如nums=[2,4],k=3,numsOperations=2
,如果调整到[5,5]
,为什么不调整到[4,4]
?
换句话说,x
可以取到nums
中最小值到最大值之间的值。
所以,思路就是:
- 排序
- 遍历
[min(nums),max(nums)]
中的每个数x
- 判断
nums
有多少个数能调整得到x
由于调整的范围是[-k,+k]
,可以使用二分去处理。
在C++
中,二分可以使用lower_bound()
以及upper_bound()
,遍历范围中每个数x
,接着:
ranges::lower_bound(nums,x-k)
:找到nums
中大于等于x-k
的位置ranges::upper_bound(nums,x+k)
:找到nums
中大于x+k
的位置
两者相减,就得到了“有多少个数能调整得到x
的数量”。
剩下还有一个问题是如何处理numsOperations
,因为处理的次数是有限制的,所以是两者取最小值:
int res = 0;
for (int x = nums.front(); x <= nums.back(); ++x) {auto l = ranges::lower_bound(nums, x - k);auto r = ranges::upper_bound(nums, x + k);// 计算结果,两者取最小值res=max(res,min(numOperations, static_cast<int>(r - l)));
}
对吗?
并不对,忽略了一个非常重要的东西,就是nums[i]
自己。
x
变成x
本身是不需要消耗numsOperations
的(严格意义上来说是不需要变),因此处理r-l
的时候,需要减去这部分。同时,取min
之后需要把这部分加回来,因为min
本质上就是求能调整得到x
的数量,但是没算上x
本身。
完整代码如下:
class Solution {
public:int maxFrequency(vector<int> &nums, int k, int numOperations) {// 排序ranges::sort(nums);// 用于计数unordered_map<int, int> m;for (int x: nums) {++m[x];}// 存储结果int res = 0;// 遍历[min(nums),max(nums)]范围内的所有数,因为排序了,所以就是遍历[nums.front(),nums.back()]for (int x = nums.front(); x <= nums.back(); ++x) {// 找到大于等于x-k的位置auto l = ranges::lower_bound(nums, x - k);// 找到大于x+k的位置auto r = ranges::upper_bound(nums, x + k);// r-l就是相等于[-k,+k]的长度// 由于是迭代器,并且写在一起了,手动转换一下int// 然后减去x本身的数量,再与numOperations取最小值,表示能有多少个数能通过调整得到x// 最后累加上x本身的数量,与res比较取最大值res = max(res, min(numOperations, static_cast<int>(r - l) - m[x]) + m[x]);}return res;}
};
如果没有lower_bound()
和upper_bound()
怎么处理?
手动写个二分就行了,参考下面的Java
代码。
2.3 Java
版本
由于upper_bound(x+k)
等价于lower_bound(x+k+1)
,所以只需要选择实现upper_bound
或者lower_bound
即可,这里实现的是lower_bound
。
import java.util.*;public class Solution {private int lowerBound(int[] nums, int target) {int l = 0;int r = nums.length - 1;while (l <= r) {int m = (l + r) >> 1;// 如果小于target,l增大,保证nums[l]>=targetif (nums[m] < target) {l = m + 1;} else {r = m - 1;}}return l;}public int maxFrequency(int[] nums, int k, int numOperations) {Arrays.sort(nums);Map<Integer, Integer> map = new HashMap<>();for (int x : nums) {map.merge(x, 1, Integer::sum);}int res = 0;for (int max = nums[nums.length - 1], x = nums[0]; x <= max; x++) {int l = lowerBound(nums, x - k);// upper_bound(x+k)等价于lower_bound(x+k+1)int r = lowerBound(nums, x + k + 1);int xCount = map.getOrDefault(x, 0);res = Math.max(res, Math.min(numOperations, r - l - xCount) + xCount);}return res;}
}
2.4 Go
版本
go
有标准库sort.Search()
可以使用,用来模拟lower_bound()
和upper_bound()
,参考代码用法。
func maxFrequency(nums []int, k int, numOperations int) int {slices.Sort(nums)res, m := 0, make(map[int]int)for _, x := range nums {m[x]++}for x := nums[0]; x <= nums[len(nums)-1]; x++ {l, r := sort.Search(len(nums), func(i int) bool {return nums[i] >= x-k}), sort.Search(len(nums), func(i int) bool {return nums[i] > x+k})res = max(res, min(numOperations, r-l-m[x])+m[x])}return res
}
3 执行操作后元素的最高频率2
3.1 原题
3.2 思路
第二题的题目描述和第一题是一致的,不同的只是范围:
k
和nums
的范围从1e5
变成了1e9
。
一种简单的方法是,使用第一题的思路,不过不是遍历nums
范围中的所有数,而是遍历nums
中的每一个数以及加上-k
和+k
。
代码如下:
class Solution {
public:int maxFrequency(vector<int> &nums, int k, int numOperations) {int res = 0;unordered_map<int, int> m;// 排序,后面需要二分ranges::sort(nums);// 计数for (int x: nums) {++m[x];}// 这个的逻辑和第一题的一致,只不过是写成了单独函数的形式auto get_result = [&](int x) -> void {// 注意这里需要加一下转换,不然会爆intconst auto l = ranges::lower_bound(nums, static_cast<long>(x) - k);const auto r = ranges::upper_bound(nums, static_cast<long>(x) + k);res = max(res, m[x] + min(static_cast<int>(r - l) - m[x], numOperations));};// 主要修改是这个地方// 从遍历[min(nums),max(nums)]到遍历nums中的每一个数x,以及x-k和x+kfor (int x: nums) {get_result(x);get_result(x + k);get_result(x - k);}return res;}
};
这个思路本质上也是和第一题一样是枚举,只不过枚举的范围,从[min(nums),max(nums)]
变成了nums
中的每一个数x
,以及x-k
和x+k
。
为什么这个思路可以呢?
考虑任意一个x
,目标就是最大化落在[x-k,x+k]
的元素个数,当移动x
时,覆盖的区间滑动。覆盖的元素个数只会在区间边界(也就是x-k
或x+k
)碰到某个nums[i]
时才会发生变化,所以最大值一定出现在下面三者之一:
x
本身x-k
,左区间端点x+k
,右区间端点
这个算法的时间复杂度是O(n log n)
,耗时如下:
一个非常简单高效的小优化是,由于遍历顺序的问题,x-k
会被之前的某个x
遍历过,因此不需要再次遍历x-k
,也就是优化如下:
for (int x: nums) {get_result(x);get_result(x + k);// 不需要x-k了,已经遍历过// get_result(x - k);
}
耗时如下,优化比较明显,短了差不多400ms:
3.3 差分
另一个思路是差分,差分的思想是,直接计算出[x-k,x+k]
之间的数量。
每当遍历到nums
中的一个x
的时候:
- 累加差分数组中的
x-k
,表示从x-k
开始,就多了一个数 - 累减差分数组中的
x+k+1
,表示直到x+k
(包含),x-k
的累加效果就没有了
由于x
的取值范围是1e9
,不能使用常规数组,在C++
中,需要使用map
。
处理差分之后,遍历差分数组中的每个数对,累加当前的值作为sum
,sum
就是[x-k,x)
的数量(不需要计算(x,x+k]
,由于遍历顺序的问题后面会计算),然后与numOperations
比较得到结果。
class Solution {
public:int maxFrequency(vector<int> &nums, int k, int numOperations) {unordered_map<int, int> m;map<int, int> diff;for (int x: nums) {// 如果没有包含,设置为0if (!diff.contains(x)) {diff[x] = 0;}// ++[x-k],表示从x-k开始所有的数值都增加1++diff[x - k];// --[x+k+1],表示到前面++[x-k]的效果到x+k为止,后面就不会再加--diff[x + k + 1];++m[x];}int res = 0;int sum = 0;// 遍历diff中的每对数for (auto &[x,d]: diff) {// sum表示对于当前的x,[x-k,x)的数量// 不需要计算(x,x+k],因为后面会遍历到sum += d;// res的计算逻辑和上面一致res = max(res, min(numOperations, sum-m[x])+m[x]);}return res;}
};
时间消耗:
这个做法虽然没有排序,但是实际上map
里面会有排序,时间复杂度是O(n log n)
3.4 同向三指针
这个思路来自灵神,详见出处。
这个方法思路是:
- 如果答案
x
在nums
中,使用同向三指针处理 - 如果答案
x
不在nums
中,使用滑动窗口处理
如果x
在nums
中,使用两个指针l
和r
,分别指向:
- 大于等于
x-k
的位置 - 大于
x+k
的位置
这样,直接进行r-l
就能得到[x-k,x+k]
的长度,将该长度和numOperations
比较,得到结果。
如果x
不在nums
中,使用滑动窗口计算[x,x+2*k]
的长度,然后计算得到结果。
详见代码:
class Solution {
public:int maxFrequency(vector<int> &nums, int k, int numOperations) {// 先排序ranges::sort(nums);// 存储结果int res = 0;int n = static_cast<int>(nums.size());// 计数unordered_map<int, int> m;for (int x: nums) {++m[x];}// 同向三指针,i,l和rfor (int i = 0, l = 0, r = 0; i < n; ++i) {// l指向第一个>=x-k的位置while (l < n && nums[l] < nums[i] - k) {++l;}// r指向第一个>x+k的位置while (r < n && nums[r] <= nums[i] + k) {++r;}// 这个计算逻辑和前面一致,不再解释res = max(res, min(numOperations, r - l - m[nums[i]]) + m[nums[i]]);}// 滑动窗口for (int i = 0, r = 0; i < n; ++i) {// 得到当前的[x,x+2*k]的长度while (r < n && nums[r] - nums[i] <= 2 * k) {++r;}// r-i就是长度,和numOperations作比较,取minres = max(res, min(numOperations, r - i));}return res;}
};
耗时如下:
这段代码还能再优化一下,第一个优化是,后面的滑窗在某些条件下不需要计算。
这个条件就是res>=numOperations
,因为滑窗计算的结果最大就是numOperations
。
第二个优化是,计数的时候可以不需要一个额外的unordered_map<int,int>
,因为已经排序了,并且计数只有在三指针的时候使用,在后面的滑窗没有使用,可以在遍历的时候累加计数。
优化后的代码如下:
class Solution {
public:int maxFrequency(vector<int> &nums, int k, int numOperations) {// 排序ranges::sort(nums);int res = 0;const int n = static_cast<int>(nums.size());// 三指针for (int i = 0, l = 0, r = 0; i < n;) {// 指向>=x-kwhile (l < n && nums[l] < nums[i] - k) {++l;}// 指向>x+kwhile (r < n && nums[r] <= nums[i] + k) {++r;}// 跳过相同的xint j = i + 1;for (; j < n && nums[j] == nums[i]; ++j) {}// 直接计算cnt,不需要通过unordered_map<int,int>计算const int cnt = j - i;res = max(res, min(numOperations, r - l - cnt) + cnt);// 跳过相同的xi = j;}// 如果res>=numOperations,直接返回// 因为滑窗计算的结果最大就是numOperationsif (res >= numOperations) {return res;}for (int i = 0, r = 0; i < n; ++i) {while (r < n && nums[r] - nums[i] <= 2 * k) {++r;}res = max(res, min(numOperations, r - i));}return res;}
};
时间消耗如下:
可以看到,虽然都是O(n log n)
的时间复杂度,但是这个时间消耗最短。
3.5 Java
版本
3.5.1 方法1
import java.util.*;public class Solution {private int numOperations;private Map<Integer, Integer> map;private int[] nums;private int k;private int res = 0;private int lowerBound(int target) {int l = 0;int r = nums.length - 1;while (l <= r) {int m = (l + r) >> 1;if (nums[m] < target) {l = m + 1;} else {r = m - 1;}}return l;}private void getResult(int x) {int l = lowerBound(x - k);int r = lowerBound(x + k + 1);int xCount = map.getOrDefault(x, 0);res = Math.max(res, Math.min(numOperations, r - l - xCount) + xCount);}public int maxFrequency(int[] nums, int k, int numOperations) {Arrays.sort(nums);this.numOperations = numOperations;this.map = new HashMap<>();this.nums = nums;this.k = k;for (int x : nums) {map.merge(x, 1, Integer::sum);}for (int x : nums) {getResult(x);getResult(x - k);getResult(x + k);}return res;}
}
3.5.2 方法2
import java.util.*;public class Solution {public int maxFrequency(int[] nums, int k, int numOperations) {Map<Integer, Integer> diff = new TreeMap<>();Map<Integer, Integer> map = new HashMap<>();for (int x : nums) {diff.putIfAbsent(x, 0);diff.merge(x - k, 1, Integer::sum);diff.merge(x + k + 1, -1, Integer::sum);map.merge(x, 1, Integer::sum);}int res = 0;int sum = 0;for (Map.Entry<Integer, Integer> entry : diff.entrySet()) {int key = entry.getKey();int value = entry.getValue();int xCount = map.getOrDefault(key, 0);sum += value;res = Math.max(res, Math.min(numOperations, sum - xCount) + xCount);}return res;}
}
3.5.3 方法3
import java.util.*;public class Solution {public int maxFrequency(int[] nums, int k, int numOperations) {Arrays.sort(nums);int n = nums.length;int res = 0;for (int i = 0, l = 0, r = 0; i < n; ) {while (l < n && nums[l] < nums[i] - k) {++l;}while (r < n && nums[r] <= nums[i] + k) {++r;}int j = i + 1;while (j < n && nums[j] == nums[i]) {++j;}int xCount = j - i;res = Math.max(res, Math.min(numOperations, r - l - xCount) + xCount);i = j;}if (res >= numOperations) {return res;}for (int i = 0, r = 0; i < n; i++) {while (r < n && nums[r] - nums[i] <= 2 * k) {++r;}res = Math.max(res, Math.min(numOperations, r - i));}return res;}
}
3.6 Go
版本
3.6.1 方法1
func maxFrequency(nums []int, k int, numOperations int) int {slices.Sort(nums)res, m := 0, make(map[int]int)for _, x := range nums {m[x]++}getResult := func(x int) {l, r := sort.Search(len(nums), func(i int) bool {return nums[i] >= x-k}), sort.Search(len(nums), func(i int) bool {return nums[i] > x+k})res = max(res, min(numOperations, r-l-m[x])+m[x])}for _, x := range nums {getResult(x)getResult(x + k)getResult(x - k)}return res
}
3.6.2 方法2
由于Go
没有内置的类似C++
的map
,这里使用的是无序的,然后统计完差分之后手动排序。
func maxFrequency(nums []int, k int, numOperations int) int {slices.Sort(nums)res, m, diff, sum := 0, make(map[int]int), make(map[int]int), 0for _, x := range nums {m[x]++_, contains := diff[x]if !contains {diff[x] = 0}diff[x-k]++diff[x+k+1]--}var sortedKeys []intfor keys := range diff {sortedKeys = append(sortedKeys, keys)}slices.Sort(sortedKeys)for _, x := range sortedKeys {sum += diff[x]res = max(res, min(numOperations, sum-m[x])+m[x])}return res
}
3.6.3 方法3
func maxFrequency(nums []int, k int, numOperations int) int {slices.Sort(nums)res, n := 0, len(nums)for i, l, r := 0, 0, 0; i < n; {for l < n && nums[l] < nums[i]-k {l++}for r < n && nums[r] <= nums[i]+k {r++}j := i + 1for j < n && nums[j] == nums[i] {j++}cnt := j - ires, i = max(res, min(numOperations, r-l-cnt)+cnt), j}if res >= numOperations {return res}for i, r := 0, 0; i < n; i++ {for r < n && nums[r] <= nums[i]+2*k {r++}res = max(res, min(numOperations, r-i))}return res
}
4 总结
本文主要介绍了三种方法:
- 枚举+排序+二分
- 差分
- 同向三指针+滑窗
三种方法本质上都是基于排序的,但是找到[x-k,x+k]
长度的方式各不相同,其中第三种方法由于实现方式简单,容易优化,耗时最短。
5 附录
- 执行操作后元素的最高频率1
- 执行操作后元素的最高频率2
- 灵神题解-同向三指针