正题
给你一份工作时间表 hours,上面记录着某一位员工每天的工作小时数。我们认为当员工一天中的工作小时数大于 8 小时的时候,那么这一天就是「劳累的一天」。所谓「表现良好的时间段」,意味在这段时间内,「劳累的天数」是严格 大于「不劳累的天数」。请你返回「表现良好时间段」的最大长度。
示例 1:
输入: hours = [9,9,6,0,6,6,9]
输出: 3
解释: 最长的表现良好时间段是 [9,9,6]。
开始以为是一个比较简单的动态规划问题,后来才发现是我把内容想的太过简单!
首先了解一下题意, > 8 小时是劳累的一天,小于8小时为不劳累,实际上是求劳累天数 > 不劳累天数的最长时间段。
示例 [9,9,6,0,6,6,9],前三个元素 9,9,6 位第一个表现良好时间段,理由很简单:3天内有2天>8小时,那么长度为3.第二个良好为最后两个元素 6,9 理由同上,长度为2,所以最大长度为3
先考虑动态规划方案解决, dpList 可以求得前面几天的最长时间段依次是多少,但是到了末尾2个元素的时候,就很难判断,因为倒数第二个6由于最后一个元素是9,也可被列为表现良好时间段内,那么动态规划也要考虑后面一个甚至多个元素,无疑对解题的复杂度提升了好几个档次,经过几次尝试之后果断放弃。。
可以看到表现良好的时间段并不在乎数组的数字大小具体是多少,只需要关注数字是否 >8。那么我们可以将所有的数据分为2类,一类是大于8的时间,一类是小于8的时间。要求连续>8的天数要小于8的天数,那么可以将大于8的天数定义为1,小于8的天数定义为-1,试想,如果连续相加>0,是不是可以认为是良好时间段?由此,可以得到新的数组:
[1,1,-1,-1,-1,-1,1]
进行遍历相加得到前缀和:
[0,1,2,1,0,-1,-2,-1]
那么前缀和的意义是什么呢?
题目要求返回表现良好时间段的最大长度,即求最长的一段中,得分 1 的个数大于得分 -1 的个数,也就是求 presum 数组中最长的一段子数组,其和大于 0,那么也就是找出前缀和数组 presum 中两个索引 i 和 j,使 j - i 最大,且保证 presum[j] - presum[i] 大于 0.因此只需要求得 presum 数组的最长上坡即可!那么求的最长上坡,我们可以先做 presum 的单调递减栈。
由上图可以得到单减栈的下标顺序是 [0,5,6],其元素表示为 [0, -1 , -2],那么单减栈的意义在于什么呢?
到此表示 ,以 0 为起点,到第五个元素(6)前缀和都是大于0的,以 第五个元素 -1 为起点,到 第六个元素 (9) ,其都是大于 0 的
然后我们从后往前遍历 presum 数组,与栈顶索引指向元素比较,如果相减结果大于 0,则一直出栈,直到不大于 0 为止,然后更新当前最大宽度。借用一张动图。
完整代码:
/**
* @param {number[]} hours
* @return {number}
*/
// > 8
//
var longestWPI = function(hours) {
// 前缀和
let preSum = new Array(hours.length+1).fill(0)
for (let i = 0; i < hours.length; i++) {
if (hours[i] > 8) preSum[i+1] = preSum[i] + 1
else preSum[i+1] = preSum[i] - 1
}
// 单减栈
let stack = []
stack.push(0)
for (let i = 1; i < preSum.length; i++){
if (preSum[stack[stack.length-1]] > preSum[i]) stack.push(i)
}
// 从右到左求最大跨度
let max = 0
for (let i = preSum.length-1; i > max; i--){
while(stack.length > 0 && preSum[stack[stack.length-1]] < preSum[i]){
max = Math.max(max, i - stack.pop() )
}
}
return max
};
这题的解法难度很大,算是中等里面很难的题了,对于理解上需要有一定的要求,之后会持续关注此类问题的类似题型和解法,加深对该算法的理解,目前对算法理解不够,表达不尽意,只做笔记,温习提示。