【LeetCode】1124. 表现良好的最长时间段

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

算法一:前缀和 + 单调栈

思路

  1. 观察到题目中给出数组中的元素有且只有两种, 分别是大于 8小于等于 8 ,简单起见, 可以用 1 代表大于 8 的元素, 用 -1 代表小于等于 8 的元素,例 1 的数组表示为:[1, 1, -1, -1, -1, -1, 1],记为 arr。

  2. 现在题目转换成, 需要找到一个子列, 其中 1 的元素数量严格大于 -1 的元素数量。

  3. 继续简化 : 需要找到一个子列, 其中所有元素的和大于 0

  4. 这时很容易想到直接暴力遍历所有子列, 找到和大于 0 且长度最大的子列即可。

    • 我们需要的仅仅是 子列和 ,所以可以借助前缀和求解, 计算数组 arr 的前缀和, 得到数组 prefixSum = [0, 1, 2, 1, 0, -1, -2, -1], 关于前缀和的知识可以见参考资料。
  5. 将任意一个子列表示为(i, j), i 和 j 分别是 prefixSum 数组中的某个元素的下标;

  6. 直接遍历每一个(i,j)即可找出答案;

  7. 继续优化, 明确我们的目标:「 找到一个(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),所以这种情况也不会是答案。
  8. 这时我们需要从头遍历一遍 prefixSum , 找到一个严格单调递减的数组,比如测试用例,只有[0, -1, -2]符合要求, 它们对应的下标是stk = [0, 5, 6], 这可能是 i 的候选项, 由于最终答案需要的是最长距离, 所以需要存储下标。

  9. 现在问题转换成, 遍历 stk = [0, 5, 6] ,拿到一个 i ,再从右往左遍历 prefixSum ,拿到一个 j 检查每一对 (i, j)。

  10. 此时再观察:

    • 对于一个 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] 及剩下的元素。
  11. 根据上述思路, 操作恰好契合, 所以用栈 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;
    }
};

参考资料:

  1. 优秀题解,两种解法
  2. 单调栈和应用实践
  3. 前缀和
  4. 【重要】题解2,有详细思路,比较容易看懂
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值