文章目录
- Q1:2760. 最长奇偶子数组
- 解法1——纯纯暴力
- 解法2——枚举左端点,尝试右端点
- 继续优化
- Q2:2761. 和等于目标值的质数对
- 一个小优化
- Q3:2762. 不间断子数组
- 解法1——滑动窗口+维护哈希表
- 普通HashMap
- 使用TreeMap
- 补充:Java中的TreeMap
- 解法2——滑动窗口+维护单调队列
- 补充:相似题目——1438. 绝对差不超过限制的最长连续子数组
- Q4:2763. 所有子数组中不平衡数字之和⭐⭐⭐⭐⭐
- 方法1—— O ( n 2 ) O(n^2) O(n2) 枚举
- 方法2—— O ( n ) O(n) O(n) 贡献法
- 思路
- 补充:相关题目——2681. 英雄的力量
- 成绩记录
第 352 场周赛
Q1:2760. 最长奇偶子数组
2760. 最长奇偶子数组
提示:
1 <= nums.length <= 100
1 <= nums[i] <= 100
1 <= threshold <= 100
解法1——纯纯暴力
因为数据范围特别小,所以怎么暴力都是无所谓的。
解法2——枚举左端点,尝试右端点
class Solution {
public int longestAlternatingSubarray(int[] nums, int threshold) {
int n = nums.length, ans = 0;
for (int l = 0; l < n - ans; ++l) {
// 不能作为起始端点
if (nums[l] % 2 != 0 || nums[l] > threshold) continue;
int r = l + 1;
// 只要右端点满足条件就进行移动
while (r < n && nums[r] <= threshold && nums[r] % 2 != nums[r - 1] % 2) ++r;
ans = Math.max(ans, r - l); // 更新答案
}
return ans;
}
}
继续优化
可以发现,每个满足条件的子数组是不会重叠的,
所以在枚举 l 的时候,每次可以将下一个 l 设置成 r。
代码如下:
class Solution {
public int longestAlternatingSubarray(int[] nums, int threshold) {
int n = nums.length, ans = 0;
for (int l = 0; l < n - ans; ++l) {
// 不能作为起始端点
if (nums[l] % 2 != 0 || nums[l] > threshold) continue;
int r = l + 1; // 初始化右端点
// 只要右端点满足条件就进行移动
while (r < n && nums[r] <= threshold && nums[r] % 2 != nums[r - 1] % 2) ++r;
ans = Math.max(ans, r - l); // 更新答案
l = r - 1; // 小优化
}
return ans;
}
}
Q2:2761. 和等于目标值的质数对
2761. 和等于目标值的质数对
提示:
1 <= n <= 10^6
这道题实际上考察的知识点是 筛质数
,相关的知识点可见:【算法】数学相关知识总结 的对应部分。(包括:朴素筛,埃氏筛,欧式筛)
下面代码使用的是欧式筛。
class Solution {
private final static int MX = (int)1e6;
private final static int[] primes = new int[78498]; // 1e6以内有78498个质数
private final static boolean[] np = new boolean[MX + 1];
// 预先处理计算出所有的质数
static {
int cnt = 0;
for (int i = 2; i <= MX; ++i) {
if (!np[i]) { // 如果这个数没有被筛掉
primes[cnt++] = i;
}
for (int j = 0; primes[j] * i <= MX; ++j) {
np[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
public List<List<Integer>> findPrimePairs(int n) {
List<List<Integer>> ans = new ArrayList();
for (int x: primes) {
// 将所有符合答案的质数对加入答案列表
int y = n - x;
if (y < x) break;
if (!np[y]) ans.add(List.of(x, y));
}
return ans;
}
}
筛出质数之后枚举判断就可以了。
一个小优化
如果 n 是奇数,由于只有奇数+偶数=奇数,而偶数中只有 2 是质数,所以如果 n 是奇数时,至多只有一个质数对 (2, n−2)。
那么就在代码里主函数的开头加上:
if (n % 2 == 1) {
return n > 4 && !np[n - 2]? List.of(List.of(2, n - 2)): List.of();
}
Q3:2762. 不间断子数组
2762. 不间断子数组
提示:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^9
使用滑动窗口来解决,(通常滑动窗口是枚举右端点,然后按条件移动左端点)
Q:为什么要枚举右端点更好写?
A:因为枚举右端点,这样移动左端点时信息是枚举过的,是已知的;移动左端点是在缩小范围,通常更好写。
解法1——滑动窗口+维护哈希表
在滑动窗口滑的过程中,我们需要维护当前窗口内的最大值和最小值,当最大值和最小值的差大于 2 时,就需要移动左指针。
普通HashMap
class Solution {
public long continuousSubarrays(int[] nums) {
long ans = 0;
int n = nums.length;
Map<Integer, Integer> m = new HashMap();
for (int i = 0, j = 0; i < n; ++i) { // 双指针,i是右指针,j是左指针
m.merge(nums[i], 1, Integer::sum);
while (!check(m)) { // 判断哈希表里存的东西是否合理
m.merge(nums[j], -1, Integer::sum);
if (m.get(nums[j]) == 0) m.remove(nums[j]);
++j;
}
ans += i - j + 1;
}
return ans;
}
public boolean check(Map<Integer, Integer> m) {
int mn = (int)1e9 + 1, mx = 0;
for (int k: m.keySet()) {
if (k > mn + 2 || k < mx - 2) return false;
mn = Math.min(mn, k);
mx = Math.max(mx, k);
}
return true;
}
}
使用TreeMap
TreeMap是对key排列的HashMap,所以在判断窗口内值是否合理时会方便一些。
class Solution {
public long continuousSubarrays(int[] nums) {
long ans = 0;
// TreeMap是对key排列的HashMap
TreeMap<Integer, Integer> m = new TreeMap<Integer, Integer>();
for (int l = 0, r = 0; r < nums.length; ++r) {
m.merge(nums[r], 1, Integer::sum);
// 只需比较最大的和最小的key
while (m.lastKey() - m.firstKey() > 2) {
int x = nums[l++];
if (m.get(x) == 1) m.remove(x);
else m.merge(x, -1, Integer::sum);
}
ans += r - l + 1;
}
return ans;
}
}
方便之处在于可以使用 m.firstKey()
和 m.lastKey()
或者键中的最小值和最大值。
补充:Java中的TreeMap
相关资料:14.集合|Java学习笔记
解法2——滑动窗口+维护单调队列
我们需要维护窗口内的最大值和最小值,很容易就可以联想到单调队列这个数据结构来进行维护。
一个相关的题目是:239. 滑动窗口最大值 ,可以先通过这道题目学习一下如何使用单调队列维护滑动窗口的最大/小值。
这道题目使用两个双端队列,分别维护当前窗口中的最大值和最小值,当最大值和最小值的差不合条件时移动左端点。
class Solution {
public long continuousSubarrays(int[] nums) {
long ans = 0;
// dq1从大到小,dq2从小到大
Deque<Integer> dq1 = new ArrayDeque(), dq2 = new ArrayDeque();
for (int i = 0, j = 0; i < nums.length; ++i) {
// 处理两个单调队列
while (!dq1.isEmpty() && nums[i] > nums[dq1.peekLast()]) dq1.pollLast();
while (!dq2.isEmpty() && nums[i] < nums[dq2.peekLast()]) dq2.pollLast();
dq1.offerLast(i);
dq2.offerLast(i);
// 队列里的最大值和最小值不符合条件了
while (nums[dq1.peekFirst()] > nums[dq2.peekFirst()] + 2) {
if (dq1.peekFirst() < dq2.peekFirst()) {
j = dq1.peekFirst() + 1;
dq1.pollFirst();
}
else {
j = dq2.peekFirst() + 1;
dq2.pollFirst();
}
}
ans += i - j + 1;
}
return ans;
}
}
注意这边移动左端点时,需要使用 j = dq1.peekFirst() + 1;
或 j = dq2.peekFirst() + 1;
,而不能直接使用 Math.min(dq1.peekFirst(), dq2.peekFirst())
作为左端点。
补充:相似题目——1438. 绝对差不超过限制的最长连续子数组
1438. 绝对差不超过限制的最长连续子数组
这道题目和周赛题几乎一模一样,除了 2 换成了 limit 。
class Solution {
public int longestSubarray(int[] nums, int limit) {
int n = nums.length, ans = 1;
TreeMap<Integer, Integer> m = new TreeMap<Integer, Integer>(); // 编译类型和运行类型都得是TreeMap
for (int l = 0, r = 0; r < n; ++r) {
m.merge(nums[r], 1, Integer::sum);
while (m.firstKey() < m.lastKey() - limit) {
int x = nums[l++];
if (m.get(x) == 1) m.remove(x);
else m.merge(x, -1, Integer::sum);
}
ans = Math.max(ans, r - l + 1);
}
return ans;
}
}
Q4:2763. 所有子数组中不平衡数字之和⭐⭐⭐⭐⭐
2763. 所有子数组中不平衡数字之和
提示:
1 <= nums.length <= 1000
1 <= nums[i] <= nums.length
方法1—— O ( n 2 ) O(n^2) O(n2) 枚举
根据数据范围,可以使用
O
(
n
2
)
O(n^2)
O(n2) 的算法。
枚举左右端点。
class Solution {
public int sumImbalanceNumbers(int[] nums) {
int n = nums.length, ans = 0;
boolean[] st = new boolean[n + 2]; // 记录某个数是否出现过
for (int i = 0; i < n; ++i) { // 枚举左端点
Arrays.fill(st, false);
st[nums[i]] = true;
int cnt = 0;
for (int j = i + 1; j < n; ++j) { // 枚举右端点
int x = nums[j];
if (!st[x]) {
++cnt;
if (st[x - 1]) --cnt; // x 会让 x - 1 失效
if (st[x + 1]) --cnt; // x 会让 x + 1 失效
st[x] = true;
}
ans += cnt;
}
}
return ans;
}
}
其实就是枚举每个子数组的不平衡数字。
在对 j 的枚举过程中,每来一个新的数字 nums[j],不平衡的数字数量就先 + 1,然后判断是否存在 nums[j] - 1 和 nums[j] + 1,因为新来的数字会让 nums[j] - 1 失效,以及nums[j] + 1 会让新来的 nums[j] 失效。
方法2—— O ( n ) O(n) O(n) 贡献法
单独计算每个数字有多少贡献。
重要:何为贡献?
A:在本题中,贡献指的是一个数字,在它的所有子数组中,当它作为较大的那个数字时,有几个比它小得大于1的数字。
只讨论 x=nums[i] 与 x 和 x - 1
左边可以有 x,右边没有 x ,且整个子数组都不包含 x - 1 的子数组的个数
(不能包含 x - 1,但是需要有比 x 小的数字,这样才能组成组合产生贡献。这里计算时会计算进去,所以下面会减去 (n + 1) * n / 2)
减去 x 作为子数组最小值的情况 (n + 1) * n / 2。
(作为最小值时是没有贡献的,因为它前面没有数字比它小且差 > 1)
以 2 3 1 4 为例,
为了避免重复讨论,我们就只看每个数字 x = nums[i] 与 x、x - 1 的关系,(比如这个例子里的 2 和 3,当我枚举到 2 的时候,就不考虑 3 了,当枚举到 3 时才考虑 2,这样就不会重复考虑 2 和 3 之间的关系)。
下面具体来看这个例子中的结果:
对于2,我们找有2无1的子数组,有:【2】,【2,3】。其中这两个都是不合法的,因为2是这两个子数组的最小值。
对于3,我们找有3无2的子数组,有:【3】,【3,1】,【3,1,4】。其中【3】是不合法的,因为3是其中的最小值;剩下两个都是合法的。
对于1,一共有3 * 2 = 6 个子数组,最小值都是 1 ,都不合法。
对于4,我们找有4无3的子数组,有:【1,4】,【4】。其中【1,4】合法。
枚举完毕,最终得到 3 个合法的子数组,答案为 3。
思路
枚举每个数字 x = nums[i] ,对于每个数字,往左边看看第一个 x - 1 出现的位置,往右边看看第一个 x - 1 出现的位置,这样就可以计算出包含数字 x 但不包含数字 x - 1 的子数组的数量有多少个了。(这就是下标 i 的贡献)
最后还需要减去 x 作为子数组最小值的情况。
Q:这样的需要被删去的子数组有多少个呢?
A:汇总来看,每个子数组都必定有属于这个子数组的最小值,因此我们要减去所有子数组的个数,即
n
∗
(
n
+
1
)
2
\frac{n*(n+1)}{2}
2n∗(n+1) = 1+…+ n
class Solution {
public int sumImbalanceNumbers(int[] nums) {
int n = nums.length;
int[] right = new int[n], idx = new int[n + 1];
Arrays.fill(idx, n); // 如果右侧没有 x 和 x - 1,那么right[i] = n
// 从左向右遍历
for (int i = n - 1; i >= 0; i--) {
int x = nums[i];
// right[i] 表示 nums[i] 右侧的 x 和 x-1 的最近下标(不存在时为 n)
right[i] = Math.min(idx[x], idx[x - 1]);
idx[x] = i; // 更新idx[x]表示枚举到的最左侧x的坐标
}
int ans = 0;
Arrays.fill(idx, -1);
// 从右向左遍历
for (int i = 0; i < n; i++) {
int x = nums[i];
// 统计 x 能产生多少贡献
ans += (i - idx[x - 1]) * (right[i] - i); // 子数组左端点个数 * 子数组右端点个数
idx[x] = i; // 更新idx[x]表示枚举到的最右侧x的坐标
}
// 上面计算的时候,每个子数组的最小值必然可以作为贡献,而这是不合法的
// 所以每个子数组都多算了 1 个不合法的贡献
return ans - n * (n + 1) / 2;
}
}
Q:为什么计算 right[i] 时考虑 idx[x] 和 idx[x - 1],而在考虑 left[i] 时直接设置为 idx[x - 1]?
A:考虑样例1,3,3。在枚举第一个3时,有【3】,【1,3】;在枚举第二个3时,有【3】,【3,3】,【1,3,3】。由于设置了向右看时,看到 x 本身也会停下来,所以在枚举第一个3时才不会重复计算【1,3,3】这个子数组。(因为这两个3排序后只能有1个数字3和数字1挨边,即产生贡献。)
时间复杂度和空间复杂度都是 O ( n ) O(n) O(n)
补充:相关题目——2681. 英雄的力量
2681. 英雄的力量
一个标标准准的贡献题。
提示:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^9
根据数据范围,这道题目必须使用 O ( n ) O(n) O(n) 时间复杂度的算法。
由于是任选一部分英雄,因此数据的顺序不影响最后的结果,所以可以先排序。
从前向后进行枚举,每次枚举到一个数字,计算其作为最大值的贡献。
下面举一个例子:
考虑 a, b, c, d, e 五个数字,当前枚举到了 d。
此时 a, b, c 分别作为最小值的贡献为:
a
∗
2
2
+
b
∗
2
1
+
c
∗
2
0
a*2^2 + b*2^1 + c*2^0
a∗22+b∗21+c∗20,记为
s
s
s。(因为选a的时候b和c都是可选可不选,选b的时候c可选可不选,选c的时候a和b都不能选)
那么此时对答案的贡献为:
d
3
+
d
2
∗
s
=
d
2
∗
(
d
+
s
)
d^3+d^2*s = d^2*(d+s)
d3+d2∗s=d2∗(d+s)
继续枚举到
e
e
e,
此时 a, b, c, d 分别作为最小值的贡献为:
a
∗
2
3
+
b
∗
2
2
+
c
∗
2
1
+
d
∗
2
0
=
2
∗
(
a
∗
2
2
+
b
∗
2
1
+
c
∗
2
0
)
+
d
∗
2
0
=
2
∗
s
+
d
a*2^3 + b*2^2 + c*2^1 + d*2^0 = 2*(a*2^2 + b*2^1 + c*2^0) + d*2^0 = 2 *s + d
a∗23+b∗22+c∗21+d∗20=2∗(a∗22+b∗21+c∗20)+d∗20=2∗s+d
得到了新的
s
=
2
∗
s
+
n
u
m
s
[
i
]
s = 2 * s + nums[i]
s=2∗s+nums[i]
此时我们就得到了两个重要的递推式:
a
n
s
+
=
n
u
m
s
[
i
]
∗
n
u
m
s
[
i
]
∗
(
n
u
m
s
[
i
]
+
s
)
ans += nums[i] * nums[i] * (nums[i] + s)
ans+=nums[i]∗nums[i]∗(nums[i]+s)
s
=
2
∗
s
+
n
u
m
s
[
i
]
s = 2 * s + nums[i]
s=2∗s+nums[i]
class Solution {
private static final long MOD = (int)1e9 + 7;
public int sumOfPower(int[] nums) {
long ans = 0, sum = 0;
// 元素的顺序不影响答案,所以先排序
Arrays.sort(nums);
// 枚举每个英雄,计算其作为最大值时的力量贡献
for (long x: nums) {
ans = (ans + x * x % MOD * (x + sum)) % MOD; // 更新答案
sum = (sum * 2 + x) % MOD; // 更新 s
}
return (int)ans;
}
}
更多相关题目见:【算法】贡献法相关题目练习
成绩记录
很垃圾!最后一题其实很简单但可惜没做出来!