问题描述:
小M是一个五年级的小学生,今天他学习了整除的知识,想通过一些练习来巩固自己的理解。他写下了一个长度为 n 的正整数序列 a_0, a_1, ..., a_{n-1},然后想知道有多少个连续子序列的和能够被一个给定的正整数 b 整除。你能帮小M解决这个问题吗?
测试样例:
样例1:
输入:
n = 3,b = 3,sequence = [1, 2, 3]
输出:3
样例2:
输入:
n = 4,b = 5,sequence = [5, 10, 15, 20]
输出:10
样例3:
输入:
n = 5,b = 2,sequence = [1, 2, 3, 4, 5]
输出:6
解题思路:
不知道大家看到这道题,会不会联想到一道相似的题目叫作【和为k的子数组】。 在和为k的子数组中,我们利用了前缀和以及哈希的思想。其实这道题的思路也是一样的。
那为什么取模也是同样的思路呢?
假设已经有一个前缀和prefix[i]是s1,当前我遍历到的前缀和prefix[j]是s2(j>i)。
那么从下标i到j-1这个子数组的和就为s2-s1。当我们得到和了,就可以进行取模运算了。
模运算有如下公式
我们需要保证当前子数组和能被b整除,也就是要得到。 所以我们可以用一个哈希来记录子数组和模b的结果出现了几次即可。
步骤详细说明
-
前缀和的定义:
- 前缀和
prefix[i]定义为从数组开头到第i个元素的和。这样,任意子数组从j到i的和可以表示为prefix[i] - prefix[j-1]。
- 前缀和
-
构建前缀和数组:
- 通过迭代计算前缀和
prefix[i] = prefix[i-1] + sequence[i-1]。
- 通过迭代计算前缀和
-
计算模值:
- 对于每个前缀和
prefix[i],计算其 mod 值mod = (prefix[i] % b + b) % b,确保结果是非负的。 - 这个模值表示当前前缀和对于整数
b的余数。
- 对于每个前缀和
-
使用哈希表记录模值的频次:
- 使用
HashMap记录每个模值出现的次数。 - 如果当前模值
mod在cnt中已经存在,那么有cnt.get(mod)个子数组和是b的倍数(因为这些子数组与当前子数组组合形成一个新的满足条件的子数组)。 - 将当前结果增加
1次出现。
- 使用
-
更新哈希表:
- 更新当前模值
mod的计数cnt.put(mod, cnt.getOrDefault(mod, 0) + 1)。
- 更新当前模值
这种方法适用于解决一类问题:
-
问题类别: 查找数组中满足某种和条件的子数组个数。
-
常见应用:
- 子数组和等于某一固定值。
- 子数组和是某一整数的倍数。
- 子数组和落在某一范围内。
在代码中我对前缀和进行了优化,因为我们只需要记录一个sum值来保存当前前缀和即可。 另外,我们观察到,我们在这个方法中需要在遍历中记录模值相等的前缀和并计算出可累加的数量。我们也可以利用组合的思想来解决。
具体的步骤如下:
- 遍历完所有结果,得到我们的哈希表cnt。
- 遍历哈希表cnt,每个前缀和对应的value也就是出现的次数。
- 然后我们只需要利用 就能得到这个前缀和对应的答案。
- 计算完所有的cnt,累加到我们最终输出的值res就可以了。
代码:
public static int solution(int n, int b, List<Integer> sequence) {
Map<Integer, Integer> cnt = new HashMap<>();
cnt.put(0, 1);
int res = 0;
int sum=0;
for (int i = 0; i < n; ++i) {
sum += sequence.get(i);
int mod = ((sum % b) + b) % b;
res += cnt.getOrDefault(mod, 0);
cnt.put(mod, cnt.getOrDefault(mod, 0) + 1);
}
return res;
}
算法复杂度:
时间复杂度:O(n),因为只需要一次遍历数组即可。
空间复杂度:O(min(m,k)),这个为哈希表的空间。