Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
前言
今天的题目为中等题,题目只想着要接出来的话算是一道简单题,但是按照题目要求的进阶做法来做的话就算是一个有难度的题了,本文介绍的进阶解法理解的也不是很完全,只能说是个人理解,具体的不了解的伙伴可以去看leetcode上大佬们的讲解。
每日一题
今天的每日一题 2104. 子数组范围和,难度为中等
-
给你一个整数数组 nums 。nums 中,子数组的 范围 是子数组中最大元素和最小元素的差值。
-
返回 nums 中 所有 子数组范围的 和 。
-
子数组是数组中一个连续 非空 的元素序列。
示例 1:
输入: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:
输入: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:
输入:nums = [4,-2,-3,4,1]
输出:59
解释:nums 中所有子数组范围的和是 59
提示:
- 1 <= nums.length <= 1000
- 109 <= nums[i] <= 109
进阶:你可以设计一种时间复杂度为 O(n) 的解决方案吗?
题解
模拟
首先我们先来用模拟解题,思路很简单,用一个双重循环遍历,得到每一个子数组,然后子数组去计算最大最小值:
/**
* @param {number[]} nums
* @return {number}
*/
var subArrayRanges = function (nums) {
let arr = [];
let n = nums.length;
let ans = 0;
for (let i = 0; i < n; i++) {
for (let j = i; j < n; j++) {
arr = nums.slice(i, j + 1);
let max = -Number.MAX_VALUE;
let min = Number.MAX_VALUE;
arr.forEach((elm) => {
if (max < elm) {
max = elm;
}
if (min > elm) {
min = elm;
}
});
ans = ans + max - min;
}
}
return ans;
};
但是尴尬的来了,竟然超时了,那么就开始想一起要怎么去优化。
我们没有必要新建一个数组,在外层循环我们定义好了子数组的开头,内层循环的遍历其实就是在便利子数组,所有我们能直接在内层循环去判断最大最小值,并且在外层循环的时候去定义最大最小的变量
/**
* @param {number[]} nums
* @return {number}
*/
var subArrayRanges = function (nums) {
let n = nums.length;
let ans = 0;
for (let i = 0; i < n; i++) {
let min = Number.MAX_VALUE
let max = -Number.MAX_VALUE;
for (let j = i; j < n; j++) {
min = Math.min(min, nums[j]);
max = Math.max(max, nums[j]);
ans = ans + max - min;
}
}
return ans;
};
单调栈
这道题还有一个进阶解法,用 O(n) 的时间复杂度去解题。
结合题目最后的要求,我们要用每一个子数组中的最大值减去最小值,那么能不能换一种思路,我们碰到一个元素,去求它成为了几个子数组的最大值,或者它成为了几个子数组的最小值。因为最后每一个数组都是 最大减去最小 也就是 a - b ,我们只要求出一个数,它能成为几次 a 那么最后的答案就要加上几个 a,或者求出一个数它能成为几次 b,那么最后的答案就要减去几个 b
所以现在我们现在拿遍历到的 num[i] 出来做一个讲解,我们可以去定义,在 num[i] 左边大于它的那个数下标为left,那么从 left 到 i 这个范围中,num[i] 都是最大的,然后我们同理往右边再去定义大于等于它的数下标为 right,那么 从 i 到 right 这个范围中,num[i] 都是最大的。
接着,我们要在这个范围中去定义我们的子数组,假如子数组是从 l 下标 一直到 r 下标,那么是不是就能够得到两组组关系式:
- left < l <=i
- i <= r < right
我们从一张图来更直观的感受一下这个关系:
那么我们就能够得到在这个范围当中,我们能够定义出 (i-left)*(right-i) 个子数组来,并且我们知道它的权重为 num[i] 然后出现了 (i-left)*(right-i) 次,那么它为最后答案贡献的正数就是:
(i-left)*(right-i)*num[i]
负数方面也是同样的道理。
/**
* @param {number[]} nums
* @return {number}
*/
var subArrayRanges = function(nums) {
const n = nums.length
let arr = new Array(), ans = 0n
for(let i = 0; i <= n; i++) {
while(arr.length > 0 && (i == n || nums[arr[arr.length - 1]] < nums[i])) {
const j = BigInt(arr.pop())
ans += BigInt(nums[j]) * (BigInt(i) - j) * (j - (arr.length > 0 ? BigInt(arr[arr.length - 1]) : -1n))
}
arr.push(i)
}
arr = new Array()
for(let i = 0; i <= n; i++) {
while(arr.length > 0 && (i == n || nums[arr[arr.length - 1]] > nums[i])) {
const j = BigInt(arr.pop())
ans -= BigInt(nums[j]) * (BigInt(i) - j) * (j - (arr.length > 0 ? BigInt(arr[arr.length - 1]) : -1n))
}
arr.push(i)
}
return ans
};