前缀和题目:表现良好的最长时间段
文章目录
- 题目
- 标题和出处
- 难度
- 题目描述
- 要求
- 示例
- 数据范围
- 解法一
- 思路和算法
- 代码
- 复杂度分析
- 解法二
- 思路和算法
- 代码
- 复杂度分析
题目
标题和出处
标题:表现良好的最长时间段
出处:1124. 表现良好的最长时间段
难度
7 级
题目描述
要求
给定一份工作时间表 hours\texttt{hours}hours,记录了某一位员工每天的工作小时数。
我们认为当员工一天中的工作小时数(严格)大于 8\texttt{8}8 小时的时候,那么这一天就是劳累的一天。
一个表现良好的时间段是指在该时间内的劳累的天数严格大于不劳累的天数。
返回表现良好的时间段的最大长度。
示例
示例 1:
输入:hours=[9,9,6,0,6,6,9]\texttt{hours = [9,9,6,0,6,6,9]}hours = [9,9,6,0,6,6,9]
输出:3\texttt{3}3
解释:最长的表现良好时间段是 [9,9,6]\texttt{[9,9,6]}[9,9,6]。
示例 2:
输入:hours=[6,6,6]\texttt{hours = [6,6,6]}hours = [6,6,6]
输出:0\texttt{0}0
数据范围
- 1≤hours.length≤104\texttt{1} \le \texttt{hours.length} \le \texttt{10}^\texttt{4}1≤hours.length≤104
- 0≤hours[i]≤16\texttt{0} \le \texttt{hours[i]} \le \texttt{16}0≤hours[i]≤16
解法一
思路和算法
数组 hours\textit{hours}hours 的每个子数组对应一个时间段。如果一个子数组中的大于 888 的元素个数严格超过小于等于 888 的元素个数,则该子数组对应的时间段是表现良好的时间段。
为了方便计算,可以将每天的工作小时数转换成得分,大于 888 小时对应得分 111,小于等于 888 小时对应得分 −1-1−1。将工作小时数转换成得分以后,表现良好的时间段等价于元素和大于 000 的子数组。
对于长度为 nnn 的数组 hours\textit{hours}hours,将工作小时数转换成得分以后,计算得分数组的前缀和数组。前缀和数组 sums\textit{sums}sums 的长度为 n+1n + 1n+1,对于 0≤i≤n0 \le i \le n0≤i≤n,sums[i]\textit{sums}[i]sums[i] 表示得分数组的前 iii 个得分之和。
假设存在两个下标 iii 和 jjj 满足 0≤i<j≤n0 \le i < j \le n0≤i<j≤n,则得分数组的下标范围 [i,j−1][i, j - 1][i,j−1] 的子数组的得分之和为 sums[j]−sums[i]\textit{sums}[j] - \textit{sums}[i]sums[j]−sums[i],该子数组的长度是 j−ij - ij−i。如果 sums[j]−sums[i]>0\textit{sums}[j] - \textit{sums}[i] > 0sums[j]−sums[i]>0 即 sums[i]<sums[j]\textit{sums}[i] < \textit{sums}[j]sums[i]<sums[j],则存在一个长度为 j−ij - ij−i 的表现良好的时间段。
考虑表现良好的时间段 [i,j−1][i, j - 1][i,j−1],有 sums[i]<sums[j]\textit{sums}[i] < \textit{sums}[j]sums[i]<sums[j]。对于下标 k<ik < ik<i,如果 sums[k]≤sums[i]\textit{sums}[k] \le \textit{sums}[i]sums[k]≤sums[i],则必有 sums[k]<sums[j]\textit{sums}[k] < \textit{sums}[j]sums[k]<sums[j],因此得分数组的下标范围 [k,j−1][k, j - 1][k,j−1] 的子数组对应的时间段也是表现良好的时间段,且该时间段的长度 j−kj - kj−k 大于 j−ij - ij−i。因此,只有当任意小于 iii 的下标 kkk 都满足 sums[k]>sums[i]\textit{sums}[k] > \textit{sums}[i]sums[k]>sums[i] 时,下标 iii 才可能是表现良好的最长时间段的开始下标。根据该结论,可以排除不可能是表现良好的最长时间段的开始下标的下标。以下用「时间段」表示表现良好的时间段,用「最长时间段」表示表现良好的最长时间段。
可以使用单调栈存储可能是最长时间段的开始下标的全部下标,单调栈满足从栈底到栈顶的下标对应的 sums\textit{sums}sums 的元素单调递减。
从左到右遍历数组 sums\textit{sums}sums,对于每个下标 iii,当且仅当栈为空或者栈顶下标对应的元素大于 sums[i]\textit{sums}[i]sums[i] 时,将 iii 入栈。遍历结束之后,栈内的每个下标 iii 都满足对于任意小于 iii 的下标 kkk 都有 sums[k]>sums[i]\textit{sums}[k] > \textit{sums}[i]sums[k]>sums[i]。
然后从右到左遍历数组 sums\textit{sums}sums,对于每个下标 jjj,需要找到最小的下标 iii 使得 sums[i]<sums[j]\textit{sums}[i] < \textit{sums}[j]sums[i]<sums[j]。具体做法是,当栈不为空且栈顶下标对应的元素小于 sums[j]\textit{sums}[j]sums[j] 时,令栈顶下标为 iii,将 iii 出栈,并用 j−ij - ij−i 更新最长时间段,重复该操作直到栈为空或者栈顶下标对应的元素大于 sums[j]\textit{sums}[j]sums[j]。该做法的正确性说明如下。
-
对于下标 jjj,如果有多个小于 jjj 的下标对应的元素都小于 sums[j]\textit{sums}[j]sums[j],则其中最小的下标和 jjj 组成以 jjj 结尾的最长时间段。由于单调栈的下标入栈顺序为下标递增顺序,因此越接近栈底的下标越小,和 jjj 组成的时间段也越长。为了得到以下标 jjj 结尾的最长时间段,应在栈内找到最小的下标 iii 使得 sums[i]<sums[j]\textit{sums}[i] < \textit{sums}[j]sums[i]<sums[j],因此应该将全部满足 sums[i]<sums[j]\textit{sums}[i] < \textit{sums}[j]sums[i]<sums[j] 的下标 iii 出栈,在出栈的同时更新最长时间段。
-
假设存在下标 kkk 满足 k<jk < jk<j 且 sums[k]≤sums[j]\textit{sums}[k] \le \textit{sums}[j]sums[k]≤sums[j],则任何以 kkk 结尾的时间段的开始下标都可以是以 jjj 结尾的时间段的开始下标,因此以 kkk 结尾的最长时间段一定小于以 jjj 结尾的最长时间段。
-
假设存在下标 kkk 满足 k<jk < jk<j 且 sums[k]>sums[j]\textit{sums}[k] > \textit{sums}[j]sums[k]>sums[j],则可能存在下标 ppp 满足 p<kp < kp<k 且 sums[j]≤sums[p]<sums[k]\textit{sums}[j] \le \textit{sums}[p] < \textit{sums}[k]sums[j]≤sums[p]<sums[k],此时下标 ppp 可以是以 kkk 结尾的时间段的开始下标,但是不可以是以 jjj 结尾的时间段的开始下标。在遍历到 kkk 时,计算以 kkk 结尾的最长时间段一定会将 ppp 出栈。
代码
class Solution {public int longestWPI(int[] hours) {int maxInterval = 0;int n = hours.length;int[] sums = new int[n + 1];for (int i = 0; i < n; i++) {int score = hours[i] > 8 ? 1 : -1;sums[i + 1] = sums[i] + score;}Deque<Integer> stack = new ArrayDeque<Integer>();for (int i = 0; i <= n; i++) {int sum = sums[i];if (stack.isEmpty() || sums[stack.peek()] > sum) {stack.push(i);}}for (int j = n; j >= 0; j--) {int sum = sums[j];while (!stack.isEmpty() && sums[stack.peek()] < sum) {int interval = j - stack.pop();maxInterval = Math.max(maxInterval, interval);}}return maxInterval;}
}
复杂度分析
-
时间复杂度:O(n)O(n)O(n),其中 nnn 是数组 hours\textit{hours}hours 的长度。计算前缀和数组需要 O(n)O(n)O(n) 的时间,得到前缀和数组之后,需要从左到右遍历前缀和数组将下标入单调栈,然后从右到左遍历数前缀和数组计算表现良好的最长时间段。由于每个下标最多入栈和出栈各一次,因此时间复杂度是 O(n)O(n)O(n)。
-
空间复杂度:O(n)O(n)O(n),其中 nnn 是数组 hours\textit{hours}hours 的长度。空间复杂度主要取决于前缀和数组与栈的空间,前缀和数组的长度是 n+1n + 1n+1,栈内元素个数不会超过 n+1n + 1n+1。
解法二
思路和算法
这道题也可以使用前缀和与哈希表的做法解决,哈希表中记录每个非零前缀和的第一次出现的下标。
将前缀和记为 sum\textit{sum}sum。从左到右遍历数组 hours\textit{hours}hours,对于每个下标 iii,执行以下操作。
-
如果 hours[i]>8\textit{hours}[i] > 8hours[i]>8,则将 sum\textit{sum}sum 加 111,否则将 sum\textit{sum}sum 减 111。
-
根据 sum\textit{sum}sum 更新表现良好的最长时间段。
-
如果 sum>0\textit{sum} > 0sum>0,则以下标 iii 结尾的前缀为表现良好的时间段,其长度为 i+1i + 1i+1,用 i+1i + 1i+1 更新表现良好的最长时间段。
-
如果 sum≤0\textit{sum} \le 0sum≤0 且哈希表中存在前缀和 sum−1\textit{sum} - 1sum−1,则从哈希表中获得前缀和 sum−1\textit{sum} - 1sum−1 的第一次出现的下标 jjj,下标范围 [j+1,i][j + 1, i][j+1,i] 的子数组对应的时间段为表现良好的时间段,其长度为 i−ji - ji−j,用 i−ji - ji−j 更新表现良好的最长时间段。
-
-
如果哈希表中不存在前缀和 sum\textit{sum}sum,则将前缀和 sum\textit{sum}sum 对应下标 iii 存入哈希表。
遍历结束之后,即可得到表现良好的最长时间段。
该做法的正确性说明如下。
-
当遍历到下标 iii 时,如果 sum>0\textit{sum} > 0sum>0,则以下标 iii 结尾的最长子数组的长度为 i+1i + 1i+1,该子数组为表现良好的时间段。不存在以下标 iii 结尾且长度大于 i+1i + 1i+1 的子数组。
-
假设 xxx 和 yyy 都是数组的前缀和,且 y<x<0y < x < 0y<x<0。由于计算前缀和时每次将前缀和加 111 或减 111,因此在前缀和首次变成 yyy 之前,前缀和一定会经过 −1-1−1 到 y+1y + 1y+1 的每个整数。由于 y+1≤x≤−1y + 1 \le x \le -1y+1≤x≤−1,因此在前缀和首次变成 yyy 之前,前缀和一定会经过 xxx,即前缀和 xxx 的第一次出现的下标一定小于前缀和 yyy 的第一次出现的下标。当遍历到下标 iii 时,如果 sum≤0\textit{sum} \le 0sum≤0 且存在以 iii 结尾的表现良好的时间段,只需要考虑前缀和 sum−1\textit{sum} - 1sum−1 的第一次出现的下标 jjj,则以 iii 结尾的表现良好的最长时间段的长度一定是 i−ji - ji−j,任何小于 sum−1\textit{sum} - 1sum−1 的前缀和如果存在则第一次出现的下标一定大于 jjj。
代码
class Solution {public int longestWPI(int[] hours) {int maxInterval = 0;Map<Integer, Integer> indices = new HashMap<Integer, Integer>();int sum = 0;int n = hours.length;for (int i = 0; i < n; i++) {int score = hours[i] > 8 ? 1 : -1;sum += score;if (sum > 0) {maxInterval = Math.max(maxInterval, i + 1);} else if (indices.containsKey(sum - 1)) {int interval = i - indices.get(sum - 1);maxInterval = Math.max(maxInterval, interval);}indices.putIfAbsent(sum, i);}return maxInterval;}
}
复杂度分析
-
时间复杂度:O(n)O(n)O(n),其中 nnn 是数组 hours\textit{hours}hours 的长度。需要遍历数组 hours\textit{hours}hours 一次,对于每个元素计算前缀和、表现良好的最长时间段以及更新哈希表的时间都是 O(1)O(1)O(1)。
-
空间复杂度:O(n)O(n)O(n),其中 nnn 是数组 hours\textit{hours}hours 的长度。空间复杂度主要取决于哈希表空间,哈希表中的元素个数不会超过 nnn。