题目描述
给定一个整数数组 hours,其中 hours[i] 表示第 i 天的工作时间:
- 如果某天工作时间 > 8 小时,记为「劳累的一天」;
- 找到最长的连续子数组,使得「劳累的天数」严格大于「不劳累的天数」。
示例
输入:hours = [9, 9, 6, 0, 6, 6, 9]
输出:3
解释:最长表现良好的子数组为 [9, 9, 6]。
输入:hours = [6, 6, 6, 8]
输出:0
解释:没有子数组符合条件。
输入:hours = [10, 10, 10, 0, 0, 9]
输出:6
解释:整个数组满足条件。
解题思路
1. 转化问题
将原数组转换为「得分数组」:
- 如果
hours[i] > 8,得分为1; - 否则得分为
-1。
目标是找到「得分和」大于 0 的最长连续子数组。
2. 使用前缀和
前缀和的性质:
假设 prefixSum[j] - prefixSum[i] > 0,那么子数组 (i+1, j) 的和大于 0。
3. 优化:单调栈
利用单调栈记录 前缀和的最小值索引,方便快速比较子数组的和:
- 单调栈存储「前缀和最小值对应的索引」。
- 从后向前遍历数组,快速找到满足条件的最长子数组。
实现代码
import java.util.Stack;
public class Main {
public static int solution(int[] hours) {
int n = hours.length;
int[] prefixSum = new int[n + 1];
prefixSum[0] = 0;
// 1. 计算前缀和
for (int i = 0; i < n; i++) {
prefixSum[i + 1] = prefixSum[i] + (hours[i] > 8 ? 1 : -1);
}
// 2. 构造单调栈,记录最小前缀和索引
Stack<Integer> stack = new Stack<>();
for (int i = 0; i <= n; i++) {
if (stack.isEmpty() || prefixSum[stack.peek()] > prefixSum[i]) {
stack.push(i);
}
}
// 3. 倒序遍历,寻找最长的表现良好时间段
int maxLength = 0;
for (int j = n; j > 0; j--) {
while (!stack.isEmpty() && prefixSum[stack.peek()] < prefixSum[j]) {
maxLength = Math.max(maxLength, j - stack.pop());
}
}
return maxLength;
}
public static void main(String[] args) {
System.out.println(solution(new int[]{9, 9, 6, 0, 6, 6, 9}) == 3);
System.out.println(solution(new int[]{6, 6, 6, 8}) == 0);
System.out.println(solution(new int[]{10, 10, 10, 0, 0, 9}) == 6);
}
}
核心步骤详解
1. 计算前缀和
-
定义得分规则:
-
将每个
hours[i]转换成得分:- 如果
hours[i] > 8,得分为1(劳累的一天)。 - 否则得分为
-1(不劳累的一天)。
- 如果
-
-
构造前缀和数组:
-
定义
prefixSum[i]为数组score前i项的累加和。 -
初始化
prefixSum[0] = 0(表示没有元素时,和为 0)。 -
逐步累加:
prefixSum[i+1] = prefixSum[i] + score[i]
-
-
作用:
- 对于任意子数组
(i, j),其和可以用前缀和表示: sum(i,j)=prefixSum[j]−prefixSum[i−1]\text{sum}(i, j) = prefixSum[j] - prefixSum[i-1]sum(i,j)=prefixSum[j]−prefixSum[i−1] - 如果
prefixSum[j] > prefixSum[i],说明子数组(i+1, j)的和大于 0。
- 对于任意子数组
2. 单调栈维护前缀和最小值
-
为什么需要单调栈:
- 在判断
prefixSum[j] > prefixSum[i]时,我们需要快速找到最小的i,使得子数组(i+1, j)的和大于 0。 - 使用单调栈可以高效维护递减的
prefixSum[i]索引序列,确保从左到右依次记录「最小的前缀和位置」。
- 在判断
-
构造单调栈:
-
遍历前缀和数组
prefixSum:- 如果当前前缀和
prefixSum[i]小于栈顶的前缀和(prefixSum[stack.peek()]),将当前索引i入栈。
- 如果当前前缀和
-
栈内索引保持单调递减,满足: prefixSum[stack[0]]≤prefixSum[stack[1]]≤…prefixSum[stack[0]] \leq prefixSum[stack[1]] \leq \dotsprefixSum[stack[0]]≤prefixSum[stack[1]]≤…
-
-
作用:
- 栈中的索引可以快速找到「某段区间的起点」。
3. 寻找最长子数组
-
倒序遍历前缀和:
-
从右向左遍历
prefixSum[j]:- 检查单调栈顶
prefixSum[stack.peek()]是否小于当前prefixSum[j]。 - 如果满足
prefixSum[stack.peek()] < prefixSum[j],说明以j结尾的某段子数组的和大于 0。
- 检查单调栈顶
-
-
更新最长长度:
- 计算子数组长度
j - stack.pop(),更新最大值maxLength。 - 继续检查下一个栈顶元素,直到栈为空或不满足条件。
- 计算子数组长度
-
作用:
- 利用单调栈快速找到符合条件的区间长度。
-
例子:
prefixSum = [0, 1, 2, 1, 0, -1, -2, -1] 单调栈: [0, 1, 3, 4, 5, 6] 倒序遍历: j = 7, prefixSum[7] = -1 > prefixSum[6](满足条件),长度 = 7 - 6 = 1 j = 6, prefixSum[6] = -2 < prefixSum[5](不满足) ... 最长长度为 3。
复杂度分析
-
时间复杂度
- 前缀和计算:O(n)O(n)O(n)
- 单调栈构造:O(n)O(n)O(n)
- 倒序遍历栈:O(n)O(n)O(n)
- 总体复杂度:O(n)O(n)O(n)
-
空间复杂度
-
需要额外的数组存储前缀和,栈的空间为 O(n)O(n)O(n):
- 空间复杂度:O(n)O(n)O(n)。
-
测试案例
| 测试输入 | 输出 | 说明 |
|---|---|---|
hours = [9, 9, 6, 0, 6, 6, 9] | 3 | 最长表现良好的子数组为 [9, 9, 6]。 |
hours = [6, 6, 6, 8] | 0 | 没有子数组满足条件。 |
hours = [10, 10, 10, 0, 0, 9] | 6 | 整个数组满足条件,返回长度 6。 |
hours = [8, 8, 8, 9, 9, 9] | 3 | 最长表现良好的子数组为 [9, 9, 9]。 |
hours = [6, 9, 10, 8, 7, 10] | 2 | 最长表现良好的子数组为 [9, 10] 或 [10, 10]。 |