leetcode 974. 和可被 K 整除的子数组 详解
给定一个整数数组 A,返回其中元素之和可被 K 整除的(连续、非空)子数组的数目。
示例:
输入:A = [4,5,0,-2,-3,1], K = 5
输出:7
解释:
有 7 个子数组满足其元素之和可被 K = 5 整除:
[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]
提示:
1 <= A.length <= 30000
-10000 <= A[i] <= 10000
2 <= K <= 10000
1 暴力解法
开始拿到题,第一反应是两层循环,外层 i 从 0-N,内层 j 从 i 到 N,外层里用一个sum来求和,用sum%K==0来判断能否整除。这样时间复杂度是O(n^2),果然超时了。
看了别人的答案,依然理解不了,之后才知道要用前缀和的方法来达到O(n)的复杂度。
2 前缀和解法
我们首先定义一个前缀和数组 prefix,prefix[0]=0,prefix[1]=A[0],prefix[2]=prefix[1]+A[1]……总之,prefix就是从第0个元素开始加和的结果。
我们这样得到 prefix :
int subarraysDivByK(vector<int>& A, int K) {
int N = A.size();
vector<int> prefix(N+1);
for (int i=0;i<N;++i)
{
prefix[i+1]=prefix[i]+A[i];
}
以 A=[4,5,0,-2,-3,1],K=5 为例,然后我们得到下表:
index | 0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|---|
A | 4 | 5 | 0 | -2 | -3 | 1 | |
prefix | 0 | 4 | 9 | 9 | 7 | 4 | 5 |
prefix%5 | 0 | 4 | 4 | 4 | 3 | 4 | 0 |
其中 prefix%K 就是 prefix 对 5 求余的结果。
诀窍就在这里,我们观察 index 1-3 中 prefix%5 出现的3个4:
prefix[1] = ΣA[0]
prefix[2] = ΣA[0-1]
而ΣA[0]%5 = ΣA[0-1]%5,这就表明Σ0和Σ0-1的差值必然是5的倍数,得到
ΣA[0-1] - ΣA[0] → A[1]%5==0
→ {5}是一个符合集合。
同理,在第1个4和第3个4之间:
prefix[1] = ΣA[0]
prefix[3] = ΣA[0-2]
→ ΣA[0-2] - ΣA[0] → ΣA[1-2]%5==0
→ {5,0}是一个符合集合。
同理,在第2个4和第3个4之间,夹了一个A[2]=0:
→ {0}是一个符合集合。
这样,我们可以知道,当同样的数字出现2次时,表明1个符合集合出现了;当同样的数字出现3次时,表明3个符合集合出现了。这说明在4出现的3次中,两两一对,共有3对代表了3个符合的集合。
设出现4的次数为n,则符合的集合数量为C(n,2),C(n,2)=n!/((n-2)!*2)=n*(n-1)/2。
在上面的例子中:
- 0出现了2次,result加上1;
- 3出现了1次,不能配对,所以result不变;
- 4出现了4次,result加上C(4,2)=6;
最终符合的集合数量为7。
代码:
vector<int> cnt(K);
for (auto n:prefix)
{
int num=(n%K+K)%K;
cnt[num]++;
}
int ret=0;
for (auto n:cnt)
ret+=n*(n-1)/2;
return ret;
这里我们用数组 cnt 来记录余数为 0-4 的次数。
至于 (n%K+K)%K 的目的在于将余数为负数的转化为正数,例如 n%5 in {-4,-3,-2,-1,0,1,2,3,4},则 (n%5+5)%5 in {0,1,2,3,4}。
这段代码和上面的那段连起来就是完整的解答了。
参考文献
[1] LeetCode 525.连续数组
https://www.cnblogs.com/geek1116/p/9389236.html