2026-01-22:删除可整除和后的最小数组和。用go语言,给定一个整数数组 nums 和一个整数 k。你可以反复挑选数组中相邻的一段元素——如果这段的元素和是 k 的倍数,就把它从数组中删掉,剩下的元素会自动合并成新的数组。经过任意次数这样的删除后,数组中剩下的所有数的和最少能是多少?请输出这个最小可能的总和。
1 <= nums.length <= 100000。
1 <= nums[i] <= 1000000。
1 <= k <= 100000。
输入: nums = [3,1,4,1,5], k = 3。
输出: 5。
解释:
首先删除子数组 nums[1..3] = [1, 4, 1],其和为 6(可以被 3 整除),剩余数组为 [3, 5]。
然后删除子数组 nums[0..0] = [3],其和为 3(可以被 3 整除),剩余数组为 [5]。
剩余数组的和为 5。
题目来自力扣3654。
算法步骤解析
-
初始化 算法首先初始化一个哈希表
minF,用于记录不同余数对应的最小前缀和。键是前缀和模k的余数,值是该余数出现位置所对应的最小“剩余和”。为了处理空前缀的情况,初始状态被设置为minF[0] = 0,这表示在考虑任何数组元素之前,剩余和为0,且前缀和0模k的余数也是0。 同时,初始化两个变量:sum:用于动态计算遍历到当前位置时的前缀和(模k)。f:表示在遍历到当前位置时,经过一系列删除操作后,所能得到的最小剩余和。
-
遍历数组 算法从左到右依次处理数组
nums中的每个元素x。对于每个元素,执行以下关键步骤:- 更新前缀和:将当前元素
x加到sum中,并对k取模,得到新的余数sum = (sum + x) % k。这个余数标识了当前前缀和的“同余类”。 - 情况一:保留当前元素:如果不删除以当前元素结尾的子数组,那么当前元素
x就必须加入到之前的剩余和f中。因此,计算f + x作为一个候选结果。 - 情况二:删除以当前元素结尾的子数组:根据同余定理,如果当前前缀和的余数
sum在哈希表minF中存在,意味着在之前某个位置j的前缀和余数也是sum。那么,子数组nums[j+1 ... i]的和(即当前前缀和 - 位置j的前缀和)必然是k的倍数,可以被合法删除。删除这个子数组后,剩余和将直接等于位置j记录的最小剩余和minF[sum]。这是另一个候选结果。 - 决策与更新:比较上述两种情况得到的候选值(
f + x和minF[sum]),将较小的那个赋值给当前的f。这一步确保了在当前位置能获得最小的可能剩余和。随后,用当前最新的f值去更新哈希表minF中对应余数sum的记录,始终保持该余数对应的是最小的剩余和。
- 更新前缀和:将当前元素
-
返回结果 当数组中的所有元素都处理完毕后,变量
f中存储的值就是整个数组经过任意次删除操作后能得到的最小剩余和,将其作为结果返回。
复杂度分析
-
总的时间复杂度:O(n)。 算法只需要对长度为
n的数组进行一次线性遍历。在遍历过程中,每次对哈希表的查询、插入或更新操作都可以认为是常数时间复杂度(O(1))。因此,整体时间复杂度是线性的,对于题目中n最大为 100,000 的约束条件非常高效。 -
总的额外空间复杂度:O(min(n, k))。 算法主要的额外空间消耗在于哈希表
minF。在最坏情况下,哈希表可能需要存储最多k个不同的余数(0 到 k-1)及其对应的最小剩余和。如果k > n,由于最多只有n+1个前缀和,实际存储的键值对数量不会超过n+1。因此,空间复杂度是n和k中较小者的线性级别。
Go完整代码如下:
package main
import (
"fmt"
)
func minArraySum(nums []int, k int) int64 {
minF := map[int]int{0: 0} // sum[0] = 0,对应的 f[0] = 0
f, sum := 0, 0
for _, x := range nums {
sum = (sum + x) % k
// 不删除 x
f += x
// 删除以 x 结尾的子数组,问题变成剩余前缀的最小和
// 其中剩余前缀的元素和模 k 等于 sum,对应的 f 值的最小值记录在 minF[sum] 中
if mn, ok := minF[sum]; ok {
f = min(f, mn)
}
// 维护前缀和 sum 对应的最小和,由于上面计算了 min,这里无需再计算 min
minF[sum] = f
}
return int64(f)
}
func main() {
nums := []int{3, 1, 4, 1, 5}
k := 3
result := minArraySum(nums, k)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-
def minArraySum(nums: list[int], k: int) -> int:
min_f = {0: 0} # sum[0] = 0,对应的 f[0] = 0
f = 0
prefix_sum = 0
for x in nums:
prefix_sum = (prefix_sum + x) % k
# 不删除 x
f += x
# 删除以 x 结尾的子数组
if prefix_sum in min_f:
f = min(f, min_f[prefix_sum])
# 维护前缀和 prefix_sum 对应的最小和
min_f[prefix_sum] = f
return f
if __name__ == "__main__":
nums = [3, 1, 4, 1, 5]
k = 3
result = minArraySum(nums, k)
print(result)
C++完整代码如下:
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;
long long minArraySum(vector<int>& nums, int k) {
// minF[0] = 0,表示前缀和模k为0时,最小的和为0
unordered_map<int, long long> minF;
minF[0] = 0;
long long f = 0; // 当前的最小和
int prefix_sum = 0; // 前缀和模k
for (int x : nums) {
prefix_sum = (prefix_sum + x) % k;
// 不删除当前元素x
f += x;
// 删除以x结尾的子数组
// 如果之前出现过相同模值的前缀和,可以尝试删除中间的区间
if (minF.count(prefix_sum)) {
f = min(f, minF[prefix_sum]);
}
// 更新当前模值对应的最小和
minF[prefix_sum] = f;
}
return f;
}
int main() {
vector<int> nums = {3, 1, 4, 1, 5};
int k = 3;
long long result = minArraySum(nums, k);
cout << result << endl;
return 0;
}