LC 2104. 子数组范围和

题目描述

这是 LeetCode 上的 2104. 子数组范围和 ,难度为 中等

给你一个整数数组 numsnums 中,子数组的 范围 是子数组中最大元素和最小元素的差值。

返回 nums所有 子数组范围的

子数组是数组中一个连续 非空 的元素序列。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
输入:nums = [1,2,3]

输出:4

解释:nums 的 6 个子数组如下所示:
[1],范围 = 最大 - 最小 = 1 - 1 = 0
[2],范围 = 2 - 2 = 0
[3],范围 = 3 - 3 = 0
[1,2],范围 = 2 - 1 = 1
[2,3],范围 = 3 - 2 = 1
[1,2,3],范围 = 3 - 1 = 2
所有范围的和是 0 + 0 + 0 + 1 + 1 + 2 = 4

示例 2:
1
2
3
4
5
6
7
8
9
10
11
12
输入:nums = [1,3,3]

输出:4

解释:nums 的 6 个子数组如下所示:
[1],范围 = 最大 - 最小 = 1 - 1 = 0
[3],范围 = 3 - 3 = 0
[3],范围 = 3 - 3 = 0
[1,3],范围 = 3 - 1 = 2
[3,3],范围 = 3 - 3 = 0
[1,3,3],范围 = 3 - 1 = 2
所有范围的和是 0 + 0 + 0 + 2 + 0 + 2 = 4

示例 3:
1
2
3
4
5
输入:nums = [4,-2,-3,4,1]

输出:59

解释:nums 中所有子数组范围的和是 59

提示:

  • 1 <= nums.length <= 1000
  • $-10^9 <= nums[i] <= 10^9$

进阶:你可以设计一种时间复杂度为 $O(n)$ 的解决方案吗?


区间 DP(预处理)

数据范围为 $10^3$,最为朴素的三层循环为:枚举区间(左右端点)+ 扫描区间统计最值,并累加到答案中。该做法复杂度为 $O(n^3)$,会 TLE

考虑在此基础上优化,枚举所有区间的操作不好避免,考虑通过「预处理」手段来优化「扫描区间统计最值」操作,通常会将其优化为 $O(1)$ 查表。

定义 $f[l][r][k]$ 为区间 $[l, r]$ 范围内的最值情况,其中 $k$ 非 $0$ 即 $1$:$f[l][r][0]$ 代表区间 $[l, r]$ 内的最小值,$f[l][r][1]$ 代表区间 $[l, r]$ 内的最大值。

不失一般性考虑 $f[l][r][0]$ 和 $f[l][r][1]$ 该如何计算:$[l, r]$ 区间的最值可由 $[l, r - 1]$ 与 $nums[r]$ 更新而来:

最后再枚举所有区间统计答案即可。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public long subArrayRanges(int[] nums) {
int n = nums.length;
int[][][] f = new int[n][n][2];
for (int i = 0; i < n; i++) f[i][i][0] = f[i][i][1] = nums[i];
for (int len = 2; len <= n; len++) {
for (int l = 0; l + len - 1 < n; l++) {
int r = l + len - 1;
f[l][r][0] = Math.min(nums[r], f[l][r - 1][0]);
f[l][r][1] = Math.max(nums[r], f[l][r - 1][1]);
}
}
long ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
ans += f[i][j][1] - f[i][j][0];
}
}
return ans;
}
}

  • 时间复杂度:区间 DP 复杂度为 $O(n^2)$;统计范围和的复杂度为 $O(n^2)$。整体复杂度为 $O(n^2)$
  • 空间复杂度:$O(n^2)$

枚举

更进一步,我们发现在转移计算 $[l, r]$ 的最值情况时,仅依赖于 $[l, r - 1]$(小区间),因此我们可以使用两变量代替动规数组,边遍历边维护并统计答案。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public long subArrayRanges(int[] nums) {
int n = nums.length;
long ans = 0;
for (int i = 0; i < n; i++) {
int min = nums[i], max = nums[i];
for (int j = i + 1; j < n; j++) {
min = Math.min(min, nums[j]);
max = Math.max(max, nums[j]);
ans += max - min;
}
}
return ans;
}
}

  • 时间复杂度:$O(n^2)$
  • 空间复杂度:$O(1)$

单调栈

假设有 $m$ 个区间,最终的表达式为 $m$ 个等式 $\max - \min$ 之和。

若某个 $nums[i]$,如果在这 $k_1$ 个区间中充当最大值,则在最终等式中以 $\max$ 的形式出现 $k_1$ 次,如果在 $k_2$ 个区间中充当最小值,则在最终等式中以 $\min$ 形式出现 $k_2$ 次。

因此我们可以统计每个 $nums[i]$ 成为区间最大值的次数 $k_1$ 和成为区间最小值的次数 $k_2$,$(k_1 - k_2) * nums[i]$ 为 $nums[i]$ 对于最终答案的贡献。

考虑如何统计每个 $nums[i]$ 成为区间最值的次数:

  • $nums[i]$ 作为区间最大值的次数:找到 $nums[i]$ 左右最近一个不满足「小于等于 $nums[i]$」的位置,记其为 $p$ 和 $q$。此时区间左端点共有 $i - p$ 个选择,区间右端点共有 $q - i$ 个选择,根据乘法原理,区间个数为 $(i - p) * (q - i)$ 个;
  • $nums[i]$ 作为区间最小值的次数:同理,找到 $nums[i]$ 左右最近一个不满足「大于等于 $nums[i]$」的位置,记其为 $p$ 和 $q$,区间个数为 $(i - p) * (q - i)$ 个。

即问题切换为:使用「单调栈」找到某个 $nums[i]$ 的左边/右边的最近一个符合某种性质的位置,从而知道 $nums[i]$ 作为区间最值时,左右端点的可选择个数,再结合乘法原理知道 $nums[i]$ 能够作为区间最值的区间个数,从而知道 $nums[i]$ 对答案的贡献。

值得注意的是,由于 $nums[i]$ 存在相同元素,因此上述两边均取等号的做法会导致某些区间被重复计算,因此我们可以令最近右端点的部分不取等号,确保区间统计不重不漏。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
int n;
public long subArrayRanges(int[] nums) {
n = nums.length;
// min[i] 为 nums[i] 作为区间最小值的次数;max[i] 为 nums[i] 作为区间最大值的次数
long[] min = getCnt(nums, true), max = getCnt(nums, false);
long ans = 0;
for (int i = 0; i < n; i++) ans += (max[i] - min[i]) * nums[i];
return ans;
}
long[] getCnt(int[] nums, boolean isMin) {
int[] a = new int[n], b = new int[n];
Deque<Integer> d = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
while (!d.isEmpty() && (isMin ? nums[d.peekLast()] >= nums[i] : nums[d.peekLast()] <= nums[i])) d.pollLast();
a[i] = d.isEmpty() ? -1 : d.peekLast();
d.addLast(i);
}
d.clear();
for (int i = n - 1; i >= 0; i--) {
while (!d.isEmpty() && (isMin ? nums[d.peekLast()] > nums[i] : nums[d.peekLast()] < nums[i])) d.pollLast();
b[i] = d.isEmpty() ? n : d.peekLast();
d.addLast(i);
}
long[] ans = new long[n];
for (int i = 0; i < n; i++) ans[i] = (i - a[i]) * 1L * (b[i] - i);
return ans;
}
}

  • 时间复杂度:$O(n)$
  • 空间复杂度:$O(n)$

最后

这是我们「刷穿 LeetCode」系列文章的第 No.2104 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!