表现良好的最长时段[前缀和思想&子数组]

前言

对于子数组/子串问题,紧密连续前缀和/滑动窗口/单调栈;挖掘内在规律,可以简化代码,降低时空复杂度或消耗量。

一、表现良好的最长时间段

在这里插入图片描述

二、前缀和思想&子数组

1、前缀和&map

抽象一下工作时长,工作时长不重要,重要的是两种类型,用1/-1标记两种类型,问题就转换成了求和为正数的最长子数组。
采用前缀和思想,用map记录前面前缀和,重复的只记最前面(为了最长子数组),为了让和为正数,当sum < 0时,查阅map中是否存在(sum - 1)即可。

class Solution {
    public int longestWPI(int[] hours) {
        // 由于只有1和-1,不可能前缀和出现跳跃的情况,所有hashMap就可以处理。
        // TreeMap<Integer,Integer> tm = new TreeMap<>();
        Map<Integer,Integer> fx = new HashMap<>();
        for(int i = 0;i < hours.length;i++){
            hours[i] = hours[i] > 8 ? 1:-1;
        }
        int sum = 0;
        int max = 0;
        for(int i = 0;i < hours.length;i++){
            sum += hours[i];
            // 记录最大长度
            if(sum > 0) max = i + 1;
            else if(fx.containsKey(sum - 1)){
                max = Math.max(max,-fx.get(sum - 1) + i);
            }
            // 为了最长子数组,只记录最前面的前缀和。
            if(!fx.containsKey(sum)) fx.put(sum,i);
        }
        return max;
    }
}
// 总结:对于连续子数组问题,多半前缀和/滑动窗口/单调栈
// 1-如何确定连续时间段?两层for循环暴力确定?以结果向导的前缀和确定!!!
// 2-如何确定该连续时间段有效?根据map中记录的前缀和确定。

2、前缀和&单调栈

前缀和子数组[l , r]有如下特点,
当prfix[r] > prefix[l]时,则该子数组是有效数组,当prefix[l1] < prefix[l2],那么l2作为左边界是无意义的,毕竟l2可以,则l1也可以,而且还更长,所以记录一下单调减序列即可。
注:这里的单调栈和传统的单调栈不同,不是整个数组的单调栈,没有出栈操作。
再反向遍历前缀和数组,和栈中元素比较,不断出栈寻找prefix[r] > min(prefix[l]),再记录最长数组长度。
这里出栈元素是对接下来的r没有影响的,证明如下,
当r1 > r 2时,则l1 > l2,两个数组存在包含关系,没有影响。
当r1 < r2时,则l1 < l2,按照单调栈规则,l1一定在l2上面,所以出栈l1是没任何影响的。

// 单调栈
func longestWPI(hours []int) int {
    // 问题转换,求和大于0的最长子数组;
    updateHours(hours)
    // 记录前缀和
    prefix := recordPrefix(hours)
    // 得到单调减栈
    stack,top := getStack(prefix)
    //fmt.Println(prefix)
    //fmt.Println(stack)
    // 寻找最大子数组
    i := len(prefix) - 1
    m := 0
    for ; i > 0;i-- {
        // 剪枝,一旦prefix>0,即前面的子数组最大良好就是本身。
        if prefix[i] > 0 {
            return max(m,i)
        }
        k := i
        for ; top > 0 && prefix[stack[top - 1]] < prefix[i] ; {
            top--
            k = stack[top]
        }
        m = max(m,i - k)
    }
    return m
} 
func getStack(prefix []int)([]int,int){
    stack := make([]int,0)
    top := 0
    for i := 1;i < len(prefix);i++ {
        if top == 0 || prefix[stack[top - 1]] > prefix[i] {
            stack = append(stack[:top],i)
            top++
        } 
    }
    return stack,top
}
func recordPrefix(hours []int)[]int{
    prefix := make([]int,len(hours) + 1)
    for i,h := range hours {
        prefix[i + 1] = prefix[i] + h 
    }
    return prefix
}
func updateHours(hours []int) {
    for i := 0;i < len(hours);i++ {
        if hours[i] > 8 {
            hours[i] = 1
        }else {
            hours[i] = -1
        }
    }
}
func max(x,y int) int {
    if x > y {
        return x
    }
    return y
}

总结

1)对于子数组/子串问题,紧密连续前缀和/滑动窗口/单调栈。
2)挖掘内在规律,可以简化代码,降低时空复杂度或消耗量。比如这里单调栈,将前缀和简化;比如这里不用TreeMap排序,数值只有1/0/-1三种可能,前缀和必定连续。

参考文献

[1] LeetCode 表现良好的最长时段
[2] LeetCode 官方题解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值