|
| 1 | +--- |
| 2 | +title: 2025-11-27-LeetCode刷题笔记-3381-长度可被K整除的子数组的最大元素和 |
| 3 | +date: 2025-11-27 |
| 4 | +tags: |
| 5 | + - 算法学习 |
| 6 | + - LeetCode |
| 7 | +--- |
| 8 | + |
| 9 | +# 题目信息 |
| 10 | +- 平台:LeetCode |
| 11 | +- 题目:3381. 长度可被 K 整除的子数组的最大元素和 |
| 12 | +- 难度:Medium |
| 13 | +- 题目链接:[Maximum Subarray Sum with Length Divisible by K](https://leetcode.cn/problems/maximum-subarray-sum-with-length-divisible-by-k/) |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## 题目描述 |
| 18 | +> 给你一个整数数组 `nums` 和一个整数 `k`。返回 `nums` 中一个子数组的**最大**和,要求该子数组的长度可以**被** `k` **整除**。 |
| 19 | +
|
| 20 | +--- |
| 21 | + |
| 22 | +## 初步思路 |
| 23 | +1. 使用前缀和技巧:`pre[i]` 表示前 `i` 个元素的和。 |
| 24 | +2. 子数组 `[i, j]` 的和为 `pre[j+1] - pre[i]`,长度为 `j - i + 1`。 |
| 25 | +3. 若长度能被 `k` 整除,则 `(j - i + 1) % k == 0`,即 `(j - i) % k == k - 1`。 |
| 26 | +4. 对于每个位置 `j`,我们需要找到所有满足 `(j - i) % k == 0` 的 `i`(即 `i % k == j % k`),使得 `pre[j+1] - pre[i]` 最大。 |
| 27 | +5. 维护每种余数 `r = j % k` 下的最小前缀和 `min_s[r]`,答案即为 `max(pre[j+1] - min_s[j % k])`。 |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## 算法分析 |
| 32 | +- 核心:前缀和 + 模运算分类 + 贪心维护最小值。 |
| 33 | +- 关键技巧: |
| 34 | + - 将前缀和按 `位置 % k` 分类,同余类内的前缀和才能组成长度为 `k` 的倍数的子数组。 |
| 35 | + - 维护每个余数类下的最小前缀和,最大化子数组和。 |
| 36 | + - 初始化时,`min_s[k-1] = 0` 表示空前缀和(对应索引 -1)。 |
| 37 | +- 时间复杂度:`O(n)`,只需一次遍历。 |
| 38 | +- 空间复杂度:`O(k)`,存储 `k` 种余数的最小前缀和。 |
| 39 | + |
| 40 | +--- |
| 41 | + |
| 42 | +## 代码实现(Python) |
| 43 | + |
| 44 | +### 方法一:使用 accumulate 构建前缀和 |
| 45 | +```python |
| 46 | +from itertools import accumulate |
| 47 | +from math import inf |
| 48 | +from typing import List |
| 49 | + |
| 50 | +class Solution: |
| 51 | + def maxSubarraySum(self, nums: List[int], k: int) -> int: |
| 52 | + # 计算前缀和数组,pre[i] 表示 nums 的前 i 个元素之和,初始加上 0 方便处理前缀和 |
| 53 | + pre = list(accumulate(nums, initial=0)) |
| 54 | + # 记录每种余数下对应的最小前缀和,初值为无穷大 |
| 55 | + min_s = [inf] * k |
| 56 | + ans = -inf # 初始化答案为负无穷 |
| 57 | + for j, s in enumerate(pre): |
| 58 | + i = j % k # 当前前缀和的下标对 k 取模,代表这一类余数 |
| 59 | + # 更新答案,s - min_s[i] 表示以当前位置结尾的、长度可被 k 整除的子数组最大和 |
| 60 | + ans = max(ans, s - min_s[i]) |
| 61 | + # 维护每个余数对应的最小前缀和 |
| 62 | + min_s[i] = min(min_s[i], s) |
| 63 | + return ans |
| 64 | +``` |
| 65 | + |
| 66 | +### 方法二:手动维护前缀和 |
| 67 | +```python |
| 68 | +from math import inf |
| 69 | +from typing import List |
| 70 | + |
| 71 | +class Solution: |
| 72 | + def maxSubarraySum(self, nums: List[int], k: int) -> int: |
| 73 | + # 初始化每种余数下的最小前缀和数组,初值为正无穷大 |
| 74 | + min_s = [inf] * k |
| 75 | + # 特殊处理,让初始余数为 -1(实际对应 k-1)的位置为0,代表前缀和起点 |
| 76 | + min_s[-1] = s = 0 |
| 77 | + ans = -inf # 初始化最大和为负无穷 |
| 78 | + for j, x in enumerate(nums): |
| 79 | + s += x # 累加当前元素到前缀和 |
| 80 | + i = j % k # 当前下标对k取模,代表子数组长度模k的类型 |
| 81 | + # 以当前位置结尾、长度可被k整除的子数组最大和 |
| 82 | + ans = max(ans, s - min_s[i]) |
| 83 | + # 更新当前模k类型下最小前缀和 |
| 84 | + min_s[i] = min(min_s[i], s) |
| 85 | + return ans |
| 86 | +``` |
| 87 | + |
| 88 | +## 代码实现(Go) |
| 89 | +```go |
| 90 | +package main |
| 91 | + |
| 92 | +import "math" |
| 93 | + |
| 94 | +func maxSubarraySum(nums []int, k int) int64 { |
| 95 | + sum := make([]int, len(nums)+1) |
| 96 | + // calculate prefix sum |
| 97 | + for i, x := range nums { |
| 98 | + sum[i+1] = sum[i] + x |
| 99 | + } |
| 100 | + minSum := make([]int, k) |
| 101 | + for i := range minSum { |
| 102 | + minSum[i] = math.MaxInt64 / 2 |
| 103 | + } |
| 104 | + ans := math.MinInt |
| 105 | + for j, s := range sum { |
| 106 | + i := j % k |
| 107 | + ans = max(ans, s-minSum[i]) |
| 108 | + minSum[i] = min(minSum[i], s) |
| 109 | + } |
| 110 | + return int64(ans) |
| 111 | +} |
| 112 | + |
| 113 | +func max(a, b int) int { |
| 114 | + if a > b { |
| 115 | + return a |
| 116 | + } |
| 117 | + return b |
| 118 | +} |
| 119 | + |
| 120 | +func min(a, b int) int { |
| 121 | + if a < b { |
| 122 | + return a |
| 123 | + } |
| 124 | + return b |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +## 代码实现(C++) |
| 129 | +```cpp |
| 130 | +class Solution { |
| 131 | +public: |
| 132 | + long long maxSubarraySum(vector<int>& nums, int k) { |
| 133 | + vector<long long> min_s(k, LLONG_MAX / 2); |
| 134 | + min_s.back() = 0; |
| 135 | + |
| 136 | + long long ans = LLONG_MIN; |
| 137 | + long long s = 0; |
| 138 | + for (int j = 0; j < nums.size(); ++j) { |
| 139 | + s += nums[j]; |
| 140 | + int i = j % k; |
| 141 | + ans = max(ans, s - min_s[i]); |
| 142 | + min_s[i] = min(min_s[i], s); |
| 143 | + } |
| 144 | + return ans; |
| 145 | + } |
| 146 | +}; |
| 147 | +``` |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +## 测试用例 |
| 152 | +| 输入 | 输出 | 说明 | |
| 153 | +|------|------|------| |
| 154 | +| nums = [1,2], k = 1 | 3 | 子数组 [1,2] 长度为 2,可被 1 整除,和为 3 | |
| 155 | +| nums = [-1,-2,-3,-4,-5], k = 4 | -10 | 子数组 [-1,-2,-3,-4] 长度为 4,可被 4 整除,和为 -10 | |
| 156 | +| nums = [-5,1,2,-3,4], k = 2 | 4 | 子数组 [1,2,-3,4] 长度为 4,可被 2 整除,和为 4 | |
| 157 | + |
| 158 | +--- |
| 159 | + |
| 160 | +## 总结与反思 |
| 161 | +1. 本题是前缀和 + 模运算的经典应用,关键在于理解子数组长度能被 `k` 整除等价于起始位置和结束位置的前缀和索引模 `k` 同余。 |
| 162 | +2. 通过维护每种余数下的最小前缀和,可以在 `O(n)` 时间内找到答案,避免了暴力枚举所有子数组的 `O(n²)` 复杂度。 |
| 163 | +3. 初始化技巧:将 `min_s[k-1]` 设为 0,对应空前缀和(索引 -1),这样可以正确处理从数组开头开始的子数组。 |
| 164 | +4. 注意处理负数情况,答案可能为负数,初始化时不能设为 0。 |
| 165 | + |
| 166 | +--- |
0 commit comments