【Hot100】4.前缀和

这一类的核心思想是:将“子数组求和”问题转化为“两数之差”问题。

  • 定义 preSum[i] 为从数组开头到第 i 个元素的累加和。
  • 任意子数组 nums[j...i] 的和可以表示为:preSum[i] - preSum[j-1]。
  • 如果我们要找和为 k 的子数组,即寻找满足 preSum[i] - preSum[j-1] = k 的对数。
  • 变换公式:preSum[j-1] = preSum[i] - k。
  • 核心技巧:在遍历计算 preSum[i] 时,用 HashMap 记录之前出现过的 preSum 值及其次数。每次只需查询 map 中是否存在 preSum[i] - k。

📝 本类题型总结

题目 核心转换 关键差异点 (vs 和为 K 的子数组)
560. 和为 K Pre[i] - Pre[j] = k 基准:Map 存 <前缀和,次数>。查 Pre[i] - k。
974. 被 K 整除 Pre[i] % k == Pre[j] % k Map 存 <余数,次数>。需处理负数余数 (a%k+k)%k。
523. 连续子数组和 同余 + 长度限制 Map 存 <余数,最早索引>。判断 i - index >= 2。初始化 put(0, -1)。

🏆 母题:LeetCode 560. 和为 K 的子数组 (Subarray Sum Equals K)

题目内容:给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的连续子数组的个数。
核心逻辑:

  1. 初始化 map,放入 (0, 1),表示前缀和为 0 出现了 1 次(处理从索引 0 开始的子数组)。
  2. 遍历数组,累加得到当前前缀和 preSum。
  3. 检查 map 中是否有 preSum - k。如果有,说明存在若干个子数组满足条件,累加其次数。
  4. 将当前 preSum 存入 map。
import java.util.HashMap;
import java.util.Map;

class Solution {
    public int subarraySum(int[] nums, int k) {
        // Key: 前缀和数值, Value: 该前缀和出现的次数
        Map<Integer, Integer> map = new HashMap<>();
        
        // ⚠️ [核心初始化] 前缀和为 0 出现 1 次
        // 作用:当 preSum == k 时,preSum - k = 0,能查到这个初始值,代表从开头到当前的子数组满足条件
        map.put(0, 1);
        
        int count = 0;
        int preSum = 0;
        
        for (int num : nums) {
            preSum += num;
            
            // ⚠️ [核心逻辑] 查找是否存在前缀和为 (preSum - k) 的历史记录
            if (map.containsKey(preSum - k)) {
                count += map.get(preSum - k);
            }
            
            // 更新当前前缀和的出现次数
            map.put(preSum, map.getOrDefault(preSum, 0) + 1);
        }
        
        return count;
    }
}

🔄 变体 1:LeetCode 974. 和可被 K 整除的子数组 (Subarray Sums Divisible by K)

题目内容:给定一个整数数组 nums 和一个整数 k ,返回其中元素之和可被 k 整除的非空 子数组 的数目。
相比母题的变化:

  1. 数学转换:
    • 母题要求 preSum[i] - preSum[j] = k。
    • 本题要求 (preSum[i] - preSum[j]) % k == 0。
    • 根据同余定理,这等价于 preSum[i] % k == preSum[j] % k。
    • 结论:我们不再存 preSum 本身,而是存 preSum % k (余数)。如果两个前缀和的余数相同,它们之间的子数组和一定能被 k 整除。
  2. 负数处理:Java 中 % 运算对负数会返回负余数(如 -5 % 3 = -2),但数学上我们需要正余数(1)。
    • 修正公式:remainder = (preSum % k + k) % k。
  3. 统计逻辑:如果某个余数出现了 n 次,那么从中任选两个都能组成合法子数组,贡献的数量是 n * (n-1) / 2。或者像母题一样,每遇到一次就累加之前出现的次数。
import java.util.HashMap;
import java.util.Map;

class Solution {
    public int subarraysDivByK(int[] nums, int k) {
        // Key: 余数, Value: 该余数出现的次数
        Map<Integer, Integer> map = new HashMap<>();
        
        // ⚠️ [变化点 1] 初始化:余数为 0 出现 1 次
        map.put(0, 1);
        
        int count = 0;
        int preSum = 0;
        
        for (int num : nums) {
            preSum += num;
            
            // ⚠️ [变化点 2] 计算余数,重点处理负数情况
            // Java 的 % 可能返回负数,例如 -5 % 3 = -2,我们需要的是 1
            // 公式:(a % n + n) % n 保证结果为正
            int remainder = (preSum % k + k) % k;
            
            // ⚠️ [变化点 3] 逻辑同母题:如果之前出现过相同的余数,说明中间这段和能被 k 整除
            if (map.containsKey(remainder)) {
                count += map.get(remainder);
            }
            
            map.put(remainder, map.getOrDefault(remainder, 0) + 1);
        }
        
        return count;
    }
}

🔄 变体 2:LeetCode 523. 连续的子数组和 (Continuous Subarray Sum)

题目内容:给你一个整数数组 nums 和一个整数 k ,如果 nums 有一个 好的子数组 返回 true ,否则返回 false 。
好的子数组定义:

  1. 长度 至少为 2。
  2. 子数组元素总和为 k 的倍数。
    相比母题的变化:
  3. 返回值:返回 boolean,找到即返回 true。
  4. 长度限制:这是最大的不同。母题不限制长度,本题要求长度 >= 2。
    • 实现技巧:map 中存储的不是“次数”,而是“该余数第一次出现的索引”。
    • 当再次遇到相同余数时,计算 currentIndex - firstIndex。如果差值 >= 2,则满足条件。
  5. 初始化细节:map.put(0, -1)。
    • 解释:如果前缀和本身就能被 k 整除(余数为 0),且长度为 2(即索引 0 和 1),那么 currentIndex (1) - firstIndex (-1) = 2,满足条件。所以初始索引设为 -1。
import java.util.HashMap;
import java.util.Map;

class Solution {
    public boolean checkSubarraySum(int[] nums, int k) {
        // Key: 余数, Value: 该余数第一次出现的索引
        Map<Integer, Integer> map = new HashMap<>();
        
        // ⚠️ [变化点 1] 初始化:余数 0 对应的索引为 -1
        // 目的:处理从索引 0 开始的子数组,且满足长度 >= 2 的情况
        map.put(0, -1);
        
        int preSum = 0;
        
        for (int i = 0; i < nums.length; i++) {
            preSum += nums[i];
            int remainder = (preSum % k + k) % k; // 处理负数
            
            if (map.containsKey(remainder)) {
                // ⚠️ [变化点 2] 检查长度限制
                // 获取该余数第一次出现的索引
                int firstIndex = map.get(remainder);
                
                // 如果当前索引 - 第一次出现的索引 >= 2,说明子数组长度至少为 2
                if (i - firstIndex >= 2) {
                    return true;
                }
                // 注意:如果长度不够,我们【不更新】map 中的索引,保留最早的索引以便后续匹配更长的子数组
            } else {
                // 只有当余数没出现过时,才记录索引(记录最早的)
                map.put(remainder, i);
            }
        }
        
        return false;
    }
}
posted @ 2026-03-08 21:23  scales123  阅读(62)  评论(0)    收藏  举报