【前缀和算法 - 左程云】Java超详细!一文带你掌握子数组前缀信息!

该文章通过四个案例讲解子数组前缀信息的构建:

  1. 构建前缀和
  2. 构建前缀和出现的最早位置
  3. 构建前缀和余数的最早位置
  4. 构建前缀奇偶状态的最早位置

每个案例都根据具体题目讲解,并给出了力扣或牛客的测试链接

本文参考 左神-b站左程云讲解046 教学视频,配合视频食用更佳!

前缀和

得到区域和

https://leetcode.cn/problems/range-sum-query-immutable/

思路:

给定数组 nums[-2, 0, 3, -5, 2, -1],维护一个前缀和数组,可以快速得到 nums 中 [left, right]范围内的累加和,即等于 sum[right + 1] - sum[left]; (前 right 个数累加和 - 前 left-1个数的累加和)

前缀和数组 从 1 开始
sum[i] = sum[i -1] + nums[i -1]
0 位置为 0,代表前 0 个数累加和为 0,这样免去了一个边界讨论
如果不补这个 0,那么 L ~ R 累加和 = (0~R) - (0 ~ L - 1) 要讨论边界

sum = [0, -2, -2, 1, -4, -2, -30]

class NumArray {
    public int[] sum;
    public NumArray(int[] nums) {
        sum = new int[nums.length + 1];
        for(int i = 1; i < sum.length; i++){
            sum[i] = sum[i - 1] + nums[i - 1];
        }
    }
    
    public int sumRange(int left, int right) {
        return sum[right + 1] - sum[left];
    }
}

前缀和最早位置

累加和为 k 的最长子数组

https://www.nowcoder.com/practice/36fb0fd3c656480c92b569258a1223d5

给定一个无序数组arr, 其中元素可正、可负、可0。给定一个整数k,求arr所有子数组中累加和为k的最长子数组长度 输入描述:
第一行两个整数N, k。N表示数组长度,k的定义已在题目描述中给出 第二行N个整数表示数组内的数 输出描述: 输出一个整数表示答案 输入:
5 0 1 -2 1 1 1 输出: 3

思路:

下标为i时的前缀和为sum 即0~i的和为sum

目标为 k,那么只要找前缀和为(sum -k) 出现的最早位置,就是i向左累加和为 k 的最长情况

用哈希表维护,要注意:哈希表必须放入前缀和为 0,位置为-1

假设数组为 [5, -5, 5], k = 5
i= 0 时,arr[0] = 5,0 到 0 的前缀和 sum=5,sum-aim=0,查 map,map 此时为 空,误认为以 0 结尾时凑不出前缀和为 0 的情况,就错过了答案
i = 2 时,同理,前缀和为 5,在 map 中查 0,map 仍然为空,又错过答案
所以要将 [0, -1] 放入 map

public class LongestSubarraySumEqualsAim {
    public static int N;
    public static int k;
    public static int MAXN = 100002;
    public static int[] arr = new int[MAXN];
    /**
     * key: 某个前缀和
     * value: 该前缀和出现的最早位置
     * 下标为i时的前缀和为sum 即0~i的和为sum,目标为k,那么只要找前缀和为(sum - k)出现的最早位置,就是i向左累加和为aim的最长情况
     */
    public static HashMap<Integer, Integer> map = new HashMap<>();


    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));

        while (in.nextToken() != StreamTokenizer.TT_EOF){
            N = (int) in.nval;
            in.nextToken();
            k = (int) in.nval;
            for(int i = 0; i < N; i++){
                in.nextToken();
                arr[i] = (int) in.nval;
            }
            out.println(compute());
        }
        out.flush();
        br.close();
        out.close();
    }

    public static int compute(){
        map.clear();
        // 重要 : 0这个前缀和,一个数字也没有的时候,就存在了
        map.put(0, -1);
        int ans = 0;
        for(int i = 0, sum = 0; i < N; i++){
            sum += arr[i];
            if(!map.containsKey(sum)){
                map.put(sum, i);
            }
            if(map.containsKey(sum - k)){
                ans = Math.max(ans, i - map.get(sum - k));
            }
        }
        return ans;
    }
}

和为 k 的子数组个数

https://leetcode.cn/problems/subarray-sum-equals-k/

该题思路和上题同理,只不过由长度变为了个数

public int subarraySum(int[] nums, int k) {
        HashMap<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        int ans = 0;
        for(int i = 0, sum = 0; i < nums.length; i++){
            sum += nums[i];
            
            ans += map.getOrDefault(sum - k, 0);
            
            map.put(sum, map.getOrDefault(sum, 0) + 1);
            
        }
        return ans;
    }

正负一样多的最长子数组

https://www.nowcoder.com/practice/545544c060804eceaed0bb84fcd992fb

给定一个无序数组arr,其中元素可正、可负、可0。求arr所有子数组中正数与负数个数相等的最长子数组的长度

思路:

转换:遇到正数就是 1, 0 还是 0,负数就是-1
就转换为 新数组中,累加和为 0 时的最长子数组

后续思路和上题同理

表现良好的最长时间段

https://leetcode.cn/problems/longest-well-performing-interval/description/

给你一份工作时间表 hours,上面记录着某一位员工每天的工作小时数。 我们认为当员工一天中的工作小时数大于 8
小时的时候,那么这一天就是「劳累的一天」。 所谓「表现良好的时间段」,意味在这段时间内,「劳累的天数」是严格 大于「不劳累的天数」。
请你返回「表现良好时间段」的最大长度。

思路:

临界值为 8,那么可以转化数组,大于 8 时为 1,小于 8 时为 -1

利用数组中只有 1 和-1 ,可以做到一次遍历

class Solution {
    public int longestWPI(int[] hours) {
        int sum = 0, ans = 0;
        //记录每个前缀和出现的最早位置
        HashMap<Integer, Integer> map = new HashMap<>();
        map.put(0, -1);
        for(int i = 0; i < hours.length; i++){
            sum += hours[i] > 8 ? 1 : -1;
            //如果前缀和大于0 此时0~i就是最长段
            if(sum > 0){
                ans = i + 1;
            }else{
                //如果sum<=0,找之前有没有比sum更小的数,如果有,一定是通过+1到达的目前sum
                //那么在这段子区间内的累加和就大于0
                //map中存储的是最早位置,所以取到的就是累加和为 sum-1 的最长子段
                if(map.containsKey(sum - 1)){
                    ans = Math.max(ans, i - map.get(sum - 1));
                }
            }
            //如果map中没有当前的 前缀和,再加入map,保证是最早位置
            //再次出现同样的sum时,不更新
            if(!map.containsKey(sum)){
                map.put(sum, i);
            }
            
        }
        return ans;
    }
}

前缀和余数的最早位置

使数组和能被 P 整除

https://leetcode.cn/problems/make-sum-divisible-by-p/

给你一个正整数数组 nums,请你移除 最短 子数组(可以为 空),使得剩余元素的 和 能被 p 整除。 不允许 将整个数组都移除。
请你返回你需要移除的最短子数组的长度,如果无法满足题目要求,返回 -1 。 子数组 定义为原数组中连续的一组元素。

思路:
在这里插入图片描述

    public int minSubarray(int[] nums, int p) {
        int mod = 0;
        for(int num : nums){
            mod = (num + mod) % p;
        }
        // 累加完再求余的方法会溢出,要使用同余原理
        // int sum = 0;
        // for(int num : nums){
        //    sum += num;
        // }
        // mod = sum % p;
        if(mod == 0) return 0;
        
        HashMap<Integer, Integer> map = new HashMap<>();
        map.put(0, -1);
        
        int ans = Integer.MAX_VALUE;
        for(int i = 0, cur = 0; i < nums.length; i++){
            //0...i部分余数
            cur = (cur + nums[i]) % p;
            int find = cur >= mod ? (cur - mod) : (cur + p - mod);
            if(map.containsKey(find)){
                //找到每个地方移除数组的长度,取最小的那个
                ans = Math.min(ans, i - map.get(find));
            }
            map.put(cur, i);
        }
        return ans == nums.length? -1 : ans;
    }

前缀奇偶状态的最早位置

每个元音包含偶数次的最长子字符串

https://leetcode.cn/problems/find-the-longest-substring-containing-vowels-in-even-counts/

给你一个字符串 s ,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 ‘a’,‘e’,‘i’,‘o’,‘u’
,在子字符串中都恰好出现了偶数次。 输入:s = “eleetminicoworoep” 输出:13 解释:最长子字符串是
“leetminicowor” ,它包含 e,i,o 各 2 个,以及 0 个 a,u 。

    public int findTheLongestSubstring(String s) {
        int ans = 0; //初始设为0,长度最小就为0,不设为MIN_VALUE
        // u o i e a 偶数次置0 奇数次置1 5位二进制数 共32种状态
        int[] map = new int[32]; //用数组作为哈希表
        Arrays.fill(map, -2); //-2表示没出现过
        //0 0 0 0 0 初始出现在-1
        map[0] = -1;
        for(int i = 0, status = 0, m; i < s.length(); i++){
            //status 0 ~ i-1 的奇偶状态,下面要判断第i位
            m = move(s.charAt(i));
            if(m != -1){
                status ^= (1 << m); //改变status这个位置的奇偶性
            }
            //找之前是否出现过相同状态的奇偶性,如果出现了,位置为j
            //那么j~i这一段就全是偶数次 0 0 0 0 0
            if(map[status] != -2){
                ans = Math.max(ans, i - map[status]);
            }else{
                map[status] = i;
            }
        }
        return ans;
    }


    public int move(char c){
        switch(c){
            case 'a': return 0;
            case 'e': return 1;
            case 'i': return 2;
            case 'o': return 3;
            case 'u': return 4;
            default: return -1;
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zbc-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值