表现良好的时间段 | 豆包MarsCode AI刷题

102 阅读4分钟

题目描述

给定一个整数数组 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. 计算前缀和

  1. 定义得分规则

    • 将每个 hours[i] 转换成得分:

      • 如果 hours[i] > 8,得分为 1(劳累的一天)。
      • 否则得分为 -1(不劳累的一天)。
  2. 构造前缀和数组

    • 定义 prefixSum[i] 为数组 scorei 项的累加和。

    • 初始化 prefixSum[0] = 0(表示没有元素时,和为 0)。

    • 逐步累加:

      prefixSum[i+1] = prefixSum[i] + score[i]
      
  3. 作用

    • 对于任意子数组 (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. 单调栈维护前缀和最小值

  1. 为什么需要单调栈

    • 在判断 prefixSum[j] > prefixSum[i] 时,我们需要快速找到最小的 i,使得子数组 (i+1, j) 的和大于 0。
    • 使用单调栈可以高效维护递减的 prefixSum[i] 索引序列,确保从左到右依次记录「最小的前缀和位置」。
  2. 构造单调栈

    • 遍历前缀和数组 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. 作用

    • 栈中的索引可以快速找到「某段区间的起点」。

3. 寻找最长子数组

  1. 倒序遍历前缀和

    • 从右向左遍历 prefixSum[j]

      • 检查单调栈顶 prefixSum[stack.peek()] 是否小于当前 prefixSum[j]
      • 如果满足 prefixSum[stack.peek()] < prefixSum[j],说明以 j 结尾的某段子数组的和大于 0。
  2. 更新最长长度

    • 计算子数组长度 j - stack.pop(),更新最大值 maxLength
    • 继续检查下一个栈顶元素,直到栈为空或不满足条件。
  3. 作用

    • 利用单调栈快速找到符合条件的区间长度。
  4. 例子

    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。
    

复杂度分析

  1. 时间复杂度

    • 前缀和计算:O(n)O(n)O(n)
    • 单调栈构造:O(n)O(n)O(n)
    • 倒序遍历栈:O(n)O(n)O(n)
    • 总体复杂度:O(n)O(n)O(n)
  2. 空间复杂度

    • 需要额外的数组存储前缀和,栈的空间为 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]