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

文章介绍了如何解决LeetCode上的一道题目,涉及员工工作小时数和劳累状态。通过两种方法——贪心算法和哈希表,求解最长的「表现良好时间段」,即工作小时数超过8小时的天数多于不超过的天数的最长连续区间。文章详细阐述了每种方法的思路、算法实现以及时间复杂度和空间复杂度分析。
摘要由CSDN通过智能技术生成

来源:力扣(LeetCode)

描述:

给你一份工作时间表 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 的为 1 分,否则为 −1 分。那么原问题可以看做求解区间分数和大于 0 的最长区间长度。为了方便计算区间分数和,我们首先预处理分数前缀和 s:

  1. 令 s[0] 等于 0
  2. 设 n 为 hours 的长度,从小到大遍历 i (1 ≤ i ≤ n),若 hours[i − 1] > 8,则令 s[i] = s[i − 1] + 1,否则令 s[i] = s[i − 1] − 1。

至此,我们只需求解最长的一段区间 [l, r] 使得 s[r] − s[l] > 0,其中 0 ≤ l ≤ r ≤ n。我们固定 r,目标找到一个最小的 l 使得 s[l] < s[r]。倘若有 l1 ≤ l2 ,并且
s[l1] ≤ s[l2],那么 l1 要比 l2 更优, l2 永远不为成为任意一个 r 的候选。

因此,我们维护一个栈 stk,栈中元素为 s[0] ∼ s[r − 1] 的递减项。具体的,我们遍历 i (0 ≤ i ≤ r − 1),如果 stk 为空或者栈顶元素大于 s[i],则将 s[i] 入栈。求解 l 时,我们不断的弹出栈顶元素,直到栈顶元素是最后一个小于 s[r] 的元素,此时栈顶元素所在位置即为我们要求的 l。

由于过程中弹出的元素值都要比当前栈顶元素值小,因此这些弹出的元素仍然可能成为后面 r 的候选。如果按照从左到右的顺序去遍历 r,我们仍需将这些弹出的元素值再次入栈。这样做的代价是昂贵的,我们不妨试试从大到小遍历 r,整个求解过程如下:

  1. 我们遍历整个 s,求出维护递减序列的栈 stk,注意它并不是我们通常意义上的单调栈。
  2. 倒序遍历 r,对于每个 r:
    1. 如果当前 stk 不为空并且栈顶元素小于 s[r],我们设栈顶元素在原数组的下标为 l,用 r − l 更新答案,再令栈顶元素出栈。该过程不断循环直到条件不被满足。
    2. 否则,继续考虑下一个 r。

这样做的正确性在于:

  1. 如果有 r1 < r2,并且 s[r1] > s[r2],那么 r1 所匹配的左端点 l1 和 r2 所匹配的左端点 l2 一定有 l1 ≤ l2。在 stk 中, l2 相比 l1 更靠近栈 l1 = l2 的情况,由于此时满足 r2 − l2 > r1 − l1,因此我们将 l2 弹出栈也不会影响最终答案的求解。
  2. 如果有 r1 < r2,并且 s[r1] ≤ s[r2],那么 r1 永远不会成为最优答案的右端点。

至此,我们通过维护一个栈 stk,倒序遍历 r 求解可能成为最优区间的左端点 l,在 O(n) 的时间复杂度内得到答案。

代码:

class Solution {
public:
    int longestWPI(vector<int>& hours) {
        int n = hours.size();
        vector<int> s(n + 1);
        stack<int> stk;
        stk.push(0);
        for (int i = 1; i <= n; i++) {
            s[i] = s[i - 1] + (hours[i - 1] > 8 ? 1 : -1);
            if (s[stk.top()] > s[i]) {
                stk.push(i);
            }
        }

        int res = 0;
        for (int r = n; r >= 1; r--) {
            while (stk.size() && s[stk.top()] < s[r]) {
                res = max(res, r - stk.top());
                stk.pop();
            }
        }
        return res;
    }
};

执行用时:20 ms, 在所有 C++ 提交中击败了89.33%的用户
内存消耗:22.1 MB, 在所有 C++ 提交中击败了69.00%的用户
复杂度分析
时间复杂度:O(n),其中 n 为 hours 的长度。每个元素最多入栈和出栈一次,因此时间复杂度为 O(n)。
空间复杂度:O(n),其中 n 为 hours 的长度。

方法二:哈希表

思路与算法

在方法一中,我们记工作小时数大于 8 的为 1 分,小于等于 8 的为 −1 分,原问题由求解最长的「表现良好的时间段」长度转变为求解分数和大于 0 的最长区间长度。

我们仍然使用前缀和 s,对于某个下标 i(从 0 开始),我们期待找到最小的 j (j < i),满足 s[j] < s[i]。接下来,我们按照 s[i] 是否大于 0 来分情况讨论:

  1. 如果 s[i] > 0,那么前 i + 1 项元素之和大于 0,表示有一个长度为 i + 1 的大于 0 的区间。
  2. 如果 s[i] < 0,我们在前面试图寻找一个下标 j,满足 s[j] = s[i]−1。如果有,则表示区间 [j + 1, i] 是我们要找的以 i 结尾的最长区间。

为什么第 2 种情况要找 s[i] − 1,而不是 s[i] − 2 或更小的一项?因为在本题中分数只有 1 或者 −1,如果前缀和数组中在 i 之前要出现小于 s[i] 的元素,它的值一定是 s[i] − 1。也就是说当 s[i] < 0 时,我们要找到 j 使得 s[j] < s[i],如果有这样的 j 存在,这个 j 一定满足 s[j] = s[i]−1。

实现过程中,我们可以使用哈希表记录每一个前缀和第一次出现的位置,即可在 O(1) 的时间内判断前缀和等于 s[i] − 1 的位置 j 是否存在。

代码:

class Solution {
public:
    int longestWPI(vector<int>& hours) {
        int n = hours.size();
        unordered_map<int, int> ump;
        int s = 0, res = 0;
        for (int i = 0; i < n; i++) {
            s += hours[i] > 8 ? 1 : -1;
            if (s > 0) {
                res = max(res, i + 1);
            } else {
                if (ump.count(s - 1)) {
                    res = max(res, i - ump[s - 1]);
                }
            }
            if (!ump.count(s)) {
                ump[s] = i;
            }
        }
        return res;
    }
};

执行用时:20 ms, 在所有 C++ 提交中击败了89.33%的用户
内存消耗:23 MB, 在所有 C++ 提交中击败了20.34%的用户
复杂度分析
时间复杂度:O(n),其中 n 为 hours 的长度。
空间复杂度:O(n),其中 n 为 hours 的长度。
author:LeetCode-Solution

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千北@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值