来源:力扣(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:
- 令 s[0] 等于 0
- 设 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,整个求解过程如下:
- 我们遍历整个 s,求出维护递减序列的栈 stk,注意它并不是我们通常意义上的单调栈。
- 倒序遍历 r,对于每个 r:
- 如果当前 stk 不为空并且栈顶元素小于 s[r],我们设栈顶元素在原数组的下标为 l,用 r − l 更新答案,再令栈顶元素出栈。该过程不断循环直到条件不被满足。
- 否则,继续考虑下一个 r。
这样做的正确性在于:
- 如果有 r1 < r2,并且 s[r1] > s[r2],那么 r1 所匹配的左端点 l1 和 r2 所匹配的左端点 l2 一定有 l1 ≤ l2。在 stk 中, l2 相比 l1 更靠近栈 l1 = l2 的情况,由于此时满足 r2 − l2 > r1 − l1,因此我们将 l2 弹出栈也不会影响最终答案的求解。
- 如果有 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 来分情况讨论:
- 如果 s[i] > 0,那么前 i + 1 项元素之和大于 0,表示有一个长度为 i + 1 的大于 0 的区间。
- 如果 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