LeetCode 1590:使数组和能被 p 整除(前缀和 + 哈希表优化)

文章标签 :LeetCode、前缀和、哈希表、取模运算、Go 语言、算法优化
难度 :中等
适合人群:算法刷题、Go 开发者、面试准备


📌 前言

本文详细讲解 LeetCode 1590 题「使数组和能被 p 整除」的解题思路与实现。该题综合考察前缀和取模运算哈希表的应用,是一道经典的中等难度算法题。

核心思路:通过前缀和余数 + 哈希表,将 O(n²) 暴力枚举优化为 O(n) 时间复杂度。


一、问题描述

1.1 题目要求

给你一个正整数数组 nums,请你移除最短 子数组(可以为空,但不允许移除全部元素 ),使得剩余元素的和能被 p 整除。

返回需要移除的最短子数组的长度,如果无法满足要求,返回 -1

子数组定义:原数组中连续的一组元素。

1.2 示例

示例 1:

text 复制代码
输入:nums = [3,1,4,2], p = 6
输出:1
解释:数组和为 10,10 % 6 = 4。移除 [4] 后,剩余 [3,1,2],和为 6 能被 6 整除。

示例 2:

text 复制代码
输入:nums = [6,3,5,2], p = 9
输出:2
解释:移除 [5,2],剩余 [6,3],和为 9。

示例 3:

text 复制代码
输入:nums = [1,2,3], p = 3
输出:0
解释:和为 6,已经能被 3 整除,无需移除。

1.3 数据范围

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^9
  • 1 <= p <= 10^9

二、解题思路

2.1 问题分析

看到"子数组的和"这种关键词,应该立即联想到前缀和 技术。但如果暴力枚举所有子数组 [i, j],时间复杂度为 O(n²) ,在 n = 10^5 的数据范围下会超时。

2.2 核心观察

设数组总和为 total,令 q = total % p

  • 如果 q == 0,说明总和已经能被 p 整除,直接返回 0
  • 如果 q != 0,我们需要找到一个最短子数组,其和模 p 的余数恰好等于 q

数学证明 :假设移除子数组的和为 subSum,剩余部分和为 total - subSum。要使剩余部分能被 p 整除:

text 复制代码
(total - subSum) % p == 0
=> total % p == subSum % p
=> q == subSum % p

2.3 优化算法:前缀和 + 哈希表

关键思路

  1. 使用前缀和的余数 cur = prefix % p 来避免大数运算
  2. 哈希表记录每个余数最近一次出现的位置
  3. 对于当前位置 i,计算目标余数 need = (cur - q + p) % p
  4. 如果之前出现过余数 need(位置为 j),则子数组 [j+1, i] 的和模 p 等于 q

算法要点

  • 使用 map/哈希表 而非数组(p 可能高达 10^9,无法开数组)
  • 初始化 map[0] = -1,支持从数组开头移除
  • 先查询后更新:避免用当前位置覆盖自己导致长度为 0 的错误结果
  • 若最短长度等于 n,返回 -1(题目不允许移除全部元素)

三、算法实现

3.1 算法流程(伪代码)

text 复制代码
sum = sum(nums)
q = sum % p
if q == 0: return 0

map = {0: -1}  // 初始化:余数0对应索引-1
cur = 0
ans = INF
for i in 0..n-1:
    cur = (cur + nums[i]) % p
    need = (cur - q + p) % p  // 目标余数
    if need in map:
        ans = min(ans, i - map[need])  // 更新最短长度
    map[cur] = i  // 记录当前余数的位置

if ans == INF or ans == n: return -1
return ans

3.2 Go 语言实现

go 复制代码
func minSubarray(nums []int, p int) int {
    n := len(nums)
    
    // 1. 计算总和的余数
    total := 0
    for _, v := range nums {
        total = (total + v) % p
    }
    q := total % p
    if q == 0 {
        return 0  // 已满足条件
    }

    // 2. 哈希表记录余数 -> 最近索引
    last := make(map[int]int)
    last[0] = -1  // 初始化

    // 3. 遍历数组,维护前缀和余数
    cur := 0
    ans := n + 1
    for i, v := range nums {
        cur = (cur + v) % p
        need := (cur - q + p) % p  // 防止负数
        
        // 先查询是否存在目标余数
        if j, ok := last[need]; ok {
            if i-j < ans {
                ans = i - j
            }
        }
        
        // 再更新当前余数的位置
        last[cur] = i
    }

    // 4. 边界处理
    if ans == n+1 || ans == n {
        return -1  // 无法满足或需要移除全部
    }
    return ans
}

代码说明

  • 第 1 步:计算总和余数 q,提前判断 q == 0 的情况
  • 第 2 步 :初始化哈希表,last[0] = -1 允许从数组开头移除
  • 第 3 步:核心循环,先查询后更新,确保不会错误匹配自己
  • 第 4 步:若答案为 n 或未更新(INF),返回 -1

四、复杂度分析

时间复杂度

O(n) --- 只需遍历一次数组,每次哈希表操作均为 O(1)。

空间复杂度

O(min(n, p)) --- 哈希表最多存储 n 个不同的余数,且余数范围为 [0, p-1]。


五、常见陷阱与注意事项

⚠️ 易错点

  1. 取模负数问题

    • 某些语言中 (a - b) % p 可能为负数
    • 解决方法:(a - b + p) % p 保证结果非负
  2. 空间陷阱

    • ❌ 不要用大小为 p 的数组(p 可达 10^9)
    • ✅ 使用哈希表,只存储出现过的余数
  3. 更新顺序

    • ❌ 先更新 map[cur] = i 再查询会导致长度为 0 的错误
    • ✅ 先查询 map[need],再更新 map[cur]
  4. 边界条件

    • 当最短长度等于 n 时,必须返回 -1(不能移除全部)
    • 初始化 map[0] = -1 是为了支持从头移除

🎯 测试用例建议

go 复制代码
// 特殊边界
minSubarray([]int{1}, 2)           // -1,单元素无法满足
minSubarray([]int{1,2}, 4)         // -1,必须移除全部
minSubarray([]int{1,1,1}, 3)       // 0,已满足条件
minSubarray([]int{3,1,4,2}, 6)     // 1,示例 1
minSubarray([]int{6,3,5,2}, 9)     // 2,示例 2

六、单元测试

项目中包含完整的单元测试文件 main_test.go,覆盖了示例用例和边界情况。

运行测试

bash 复制代码
go test -v

测试输出示例

text 复制代码
=== RUN   TestMinSubarray
=== RUN   TestMinSubarray/example1
=== RUN   TestMinSubarray/example2
=== RUN   TestMinSubarray/example3
=== RUN   TestMinSubarray/single_impossible
=== RUN   TestMinSubarray/entire_array_candidate
=== RUN   TestMinSubarray/all_same
--- PASS: TestMinSubarray (0.00s)
PASS
ok <module-path> 0.00s

七、总结

核心要点

  1. 算法本质:将"移除子数组"问题转化为"寻找前缀余数差"问题
  2. 关键技巧:前缀和 + 取模 + 哈希表,实现 O(n) 时间复杂度
  3. 实现细节
    • 用哈希表而非数组(避免空间溢出)
    • 初始化 map[0] = -1(支持从头移除)
    • 先查询后更新(避免自我匹配)
    • 处理边界条件(q == 0、ans == n)

适用场景

这道题的解法可以推广到类似问题:

  • 子数组和模运算相关问题
  • 需要找最短/最长满足某种条件的子数组
  • 前缀和 + 哈希表优化的典型应用

扩展练习

  • LeetCode 560:和为 K 的子数组
  • LeetCode 974:和可被 K 整除的子数组
  • LeetCode 523:连续的子数组和

📚 参考资源


如果本文对你有帮助,欢迎点赞收藏!有问题欢迎在评论区讨论交流 👇

相关推荐
喜欢吃燃面38 分钟前
算法竞赛中的堆
c++·学习·算法
CoderYanger1 小时前
递归、搜索与回溯-综合练习:27.黄金矿工
java·算法·leetcode·深度优先·1024程序员节
zs宝来了1 小时前
HOT100系列-堆类型题
数据结构·算法·排序算法
Christo31 小时前
ICML-2019《Optimal Transport for structured data with application on graphs》
人工智能·算法·机器学习·数据挖掘
sin_hielo1 小时前
leetcode 1590
数据结构·算法·leetcode
吃着火锅x唱着歌1 小时前
LeetCode 2748.美丽下标对的数目
数据结构·算法·leetcode
做怪小疯子1 小时前
LeetCode 热题 100——二叉树——二叉树的中序遍历
算法·leetcode·职场和发展
一只乔哇噻1 小时前
java后端工程师+AI大模型进修ing(研一版‖day57)
java·开发语言·人工智能·算法·语言模型
晨曦夜月2 小时前
笔试强训day4
算法