给你一份工作时间表 hours
,上面记录着某一位员工每天的工作小时数。
我们认为当员工一天中的工作小时数大于 8
小时的时候,那么这一天就是「劳累的一天」。
所谓「表现良好的时间段」,意味在这段时间内,「劳累的天数」是严格 大于「不劳累的天数」。
请你返回「表现良好时间段」的最大长度。
示例 1:
输入:hours = [9,9,6,0,6,6,9]
输出:3
解释:最长的表现良好时间段是 [9,9,6]。
提示:
1 <= hours.length <= 10000
0 <= hours[i] <= 16
解题思路
首先不难想到暴力法,我们遍历hours
将>8
的hour
标记为1
,然后计算前缀和,计算前缀和的目的是方便我们计算区间内1
的个数。例如,
hours : 9 9 6 0 6 6 9
ht : 0 1 1 -1 -1 -1 -1 1
pre_sum: 0 1 2 1 0 -1 -2 -1
↑ ↑
i j
其中pre_sum[j]-pre_sum[i]=-1-1=-2
,就表示[i,j)
区间内>8
的数量减去<=8
的数量的结果。那么我们现在只需要建立左右区间的边界i
和j
,然后暴力枚举所有区间,只要区间的差大于0
,我们就更新结果。
class Solution:
def longestWPI(self, hours: List[int]) -> int:
t = [0] * (len(hours) + 1)
for i in range(1, len(t)):
t[i] += t[i-1] + (1 if hours[i-1] > 8 else -1)
res = 0
for i in range(len(t)):
for j in range(i, len(t)):
if t[j] - t[i]:
res = max(res, j - i)
return res
我们观察数据量发现是1e4
级别,那么这种O(n^2)
的解法就不行了。有什么改进的策略吗?
其实我们现在的问题转化为了找区间和>0
的最长区间(前后边界差大于0
)。怎么做?实际上你可以这样想,想要pre_sum[j]
大于pre_sum[i]
,那么我们只需找到比pre_sum[j]
更小的元素在哪即可。这就可以通过单调栈来实现。关于单调栈的一些例子可以看我的Leetcode 单调栈问题总结(超详细!!!)
但是这里的单调栈使用和之前的使用方法有区别,之前的一些单调栈问题,我们计算的都是下一个和前一个这种问题,这是一个最近的概念,而我们这里希望计算的是最远。
所以我们不妨先遍历一遍pre_sum
,建立一个严格单调递减栈,这样我们的栈顶元素一定是pre_sum
中的最小值。
接着我们再从后向前遍历pre_sum
(贪心的策略,我们希望最远),此时如果遍历到的元素大于栈顶元素的话,那么这个区间合法,我们需要记录区间大小,并且我们需要将栈顶元素弹出,然后继续比较当前遍历到的元素是不是大于栈顶元素。这里我们为什么要将元素弹出呢?我们定义变量i
、j1
和j2
,并且i < j1 < j2
,此时如果pre_sum[j2] > pre_sum[i]
的话,那么我们的pre_sum[j1]
显然就不用再参与比较了,因为不可能比j2-i
更大。
class Solution:
def longestWPI(self, hours: List[int]) -> int:
t = [0] * (len(hours) + 1)
for i in range(1, len(t)):
t[i] += t[i-1] + (1 if hours[i-1] > 8 else -1)
s = list()
for i, v in enumerate(t):
if not s or t[s[-1]] > v:
s.append(i)
res = 0
for i in range(len(t)-1, -1, -1):
while s and t[s[-1]] < t[i]:
res = max(res, i - s[-1])
s.pop()
return res
实际上转化后的问题就是Leetcode 560:和为K的数组(最详细的解法!!!)特例。在这个问题中K=1
,为什么呢?不应该是K>0
都可以吗?是的,确实是这样,但是pre_sum-1
一定出现在pre_sum-K
(其中K>1
)的最前面(因为我们从pre_sum>0
的时候开始记录第一个结果),所以使用K=1
的话一定最优。
class Solution:
def longestWPI(self, hours: List[int]) -> int:
res = pre_sum = 0
seen = {}
for i, h in enumerate(hours):
pre_sum += (1 if h > 8 else -1)
if pre_sum not in seen:
seen[pre_sum] = i
if pre_sum > 0:
res = i + 1
elif pre_sum - 1 in seen:
res = max(res, i - seen[pre_sum - 1])
return res
整体的算法思路非常简单,我们遍历hours
,计算前缀和,当前缀和大于0
的时候开始记录res
,同时记录相同前缀和第一次出现的位置。当前缀和<=0
的时候我们只需在记录过的数中查找是不是有pre_sum-1
即可。
reference:
https://leetcode.com/problems/longest-well-performing-interval/discuss/335163/O(N)-Without-Hashmap.-Generalized-ProblemandSolution%3A-Find-Longest-Subarray-With-Sum-greater-K.
https://leetcode.com/problems/longest-well-performing-interval/discuss/334565/JavaC%2B%2BPython-O(N)-Solution-Life-needs-996-and-669
https://leetcode.com/problems/longest-well-performing-interval/discuss/334897/ChineseC%2B%2B-1124.-O(n)
我将该问题的其他语言版本添加到了我的GitHub Leetcode
如有问题,希望大家指出!!!