1124. 表现良好的最长时间段
题目描述
给你一份工作时间表 hours,上面记录着某一位员工每天的工作小时数。
我们认为当员工一天中的工作小时数大于 8 小时的时候,那么这一天就是「劳累的一天」。
所谓「表现良好的时间段」,意味在这段时间内,「劳累的天数」是严格 大于「不劳累的天数」。
请你返回「表现良好时间段」的最大长度。
示例 1
输入:hours = [9,9,6,0,6,6,9]
输出:3
解释:最长的表现良好时间段是 [9,9,6]。
示例 2
输入:hours = [6,6,6]
输出:0
提示
- 1 <= hours.length <= 104
- 0 <= hours[i] <= 16
算法一:前缀和 + 单调栈
思路
-
观察到题目中给出数组中的元素有且只有两种, 分别是大于 8 和小于等于 8 ,简单起见, 可以用 1 代表大于 8 的元素, 用 -1 代表小于等于 8 的元素,例 1 的数组表示为:[1, 1, -1, -1, -1, -1, 1],记为 arr。
-
现在题目转换成, 需要找到一个子列, 其中 1 的元素数量严格大于 -1 的元素数量。
-
继续简化 : 需要找到一个子列, 其中所有元素的和大于 0 ;
-
这时很容易想到直接暴力遍历所有子列, 找到和大于 0 且长度最大的子列即可。
- 我们需要的仅仅是 子列和 ,所以可以借助前缀和求解, 计算数组 arr 的前缀和, 得到数组 prefixSum = [0, 1, 2, 1, 0, -1, -2, -1], 关于前缀和的知识可以见参考资料。
-
将任意一个子列表示为(i, j), i 和 j 分别是 prefixSum 数组中的某个元素的下标;
-
直接遍历每一个(i,j)即可找出答案;
-
继续优化, 明确我们的目标:「 找到一个(i, j) 使得 prefixSum[j] - prefixSum[i] > 0 」。
- 在遍历 j 的过程中, 给定任意 i < j1 < j2 ,如果prefixSum[i] < prefixSum[j2] ,那么(i, j1) 一定不是答案, 也就是说, 如果 (i, j2) 符合条件, 那么它们中间的所有 j1 都不需要检查,因此我们可以从右往左遍历 j;
- 在遍历外层循环 i 的过程中, 对于任意的 i < i1 < j , 如果 prefixSum[i1] >= prefixSum[i], 那么(i1, j) 一定不会是答案,因为:
- 如果 prefixSum[i1] < prefixSum[j] ,由于(i, j)更长,所以i1, j) 不会是答案;
- 如果 prefixSum[i1] > prefixSum[j] ,由于我们找的是 prefixSum[j] - prefixSum[i1] > 0 的(i, j),所以这种情况也不会是答案。
-
这时我们需要从头遍历一遍 prefixSum , 找到一个严格单调递减的数组,比如测试用例,只有[0, -1, -2]符合要求, 它们对应的下标是stk = [0, 5, 6], 这可能是 i 的候选项, 由于最终答案需要的是最长距离, 所以需要存储下标。
-
现在问题转换成, 遍历 stk = [0, 5, 6] ,拿到一个 i ,再从右往左遍历 prefixSum ,拿到一个 j 检查每一对 (i, j)。
-
此时再观察:
- 对于一个 j ,如果它满足 prefixSum[j] > prefixSum[stk[0]] ,那么 (0, j)是候选项, 但是由于 stk 是单调递减的, 所以 prefixSum[j] 也大于其余的 prefixSum[stk[0+x]] ,因此 stk 全体都是 i 的候选项, 即 (stk[x] , j)都是候选项;
- 对于一个 j ,如果它满足 prefixSum[j] < prefixSum[stk[0]] ,那么(0, j)不是候选项,至于其他的 stk 还需要另外判断;
- 但是如果反过来, 反向遍历 stk , 对于一个 j , 如果它满足 prefixSum[j] < prefixSum[stk[-1]] ,因为是单调递减的, 所有 stk 的其他元素肯定也大于 prefixSum[j] ,可以直接排除 j ;
- 然后, 如果对于一个 j , 如果它满足 prefixSum[j] > prefixSum[stk[-1]] , 那么 (stk[-1], j) 就是候选项,此时, j 继续向左遍历没有意义, 因此可以排除 stk[-1] ,遍历 stk[-2] 及剩下的元素。
-
根据上述思路, 操作恰好契合栈, 所以用栈 stk 存储可能的 i 。
收获
- 一开始想用 滑动窗口 做, 但是这道题不存在单调性,而使用双指针维护的时候,如果快指针走到不满足条件的位置后,慢指针并不知道该往左还是往右。
- 通过这道题学习了单调栈和前缀和 ,前缀和之前有出现过,但是一直没学会。
- 这道题的思路值得回顾,一步步得到最终结果。
算法情况
- 时间复杂度:O(n),由于元素出栈入栈最多一次,所以二重循环的时间复杂度最多为O(n);
- 空间复杂度:O(n)。
代码
class Solution {
public:
int longestWPI(vector<int>& hours) {
int ans = 0, n = hours.size();
vector<int> arr(n);
vector<int> prefixSum(n+1);
stack<int> stk;
stk.push(0);
prefixSum[0] = 0;
for(int i=1; i<n+1; i++){
arr[i-1] = hours[i-1] > 8 ? 1 : -1;
prefixSum[i] = arr[i-1] + prefixSum[i-1]; // 前缀和
cout<<prefixSum[i]<<endl;
if(prefixSum[i] < prefixSum[stk.top()]) stk.push(i);
}
// for(auto & t : prefixSum) cout<<t<<" ";
for(int j=n; j>0; --j){
while(!stk.empty() && prefixSum[j] > prefixSum[stk.top()]){
cout<<stk.top()<<endl;
ans = max(ans, j - stk.top());
stk.pop();
}
}
return ans;
}
};
参考资料:
- 优秀题解,两种解法
- 单调栈和应用实践
- 前缀和
- 【重要】题解2,有详细思路,比较容易看懂