个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创前缀和(6)_和可被k整除的子数组
收录于专栏【经典算法练习】
本专栏旨在分享学习算法的一点学习笔记,欢迎大家在评论区交流讨论💌
目录
[1. 题目链接 :](#1. 题目链接 :)
[2. 题目描述 :](#2. 题目描述 :)
[3. 解法(一维前缀和) :](#3. 解法(一维前缀和) :)
[算法思路 :](#算法思路 :)
[代码展示 :](#代码展示 :)
[结果分析 :](#结果分析 :)
1. 题目链接 :
OJ链接: 和可被k整除的子数组
2. 题目描述 :
给定一个整数数组 nums
和一个整数 k
,返回其中元素之和可被 k
整除的非空 子数组 的数目。
子数组 是数组中 连续 的部分。
示例 1:
输入:nums = [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]
示例 2:
输入: nums = [5], k = 9
输出: 0
提示:
1 <= nums.length <= 3 * 104
-104 <= nums[i] <= 104
2 <= k <= 104
3. 解法(一维前缀和) :
题目需要的前置知识:
同余定理:
如果(a - b) % n == 0, 那么我们就可以得到一个结论: a % n == b % n.用文字叙述就是,如果两个数相减的差能被n整除,那么这两个数取模的结果相同.
例如: (26 - 2) % 12 == 0,那么26 % 12 == 2 % 12.
C++中关于取模的结果,以及如何修正[负数取模]的结果
a. C++中关于负数的取模运算,结果是[把负数当成正数,取模之后的结果加上一个负号].
例如: -1 % 3 = -(1 % 3) = -1
b. 因为有负数,为了防止[出现负数]的结果,以(a % n + n) % n的形式输出保证为正.
例如: -1 % 3 = (-1 % 3 + 3) % 3 = 2;
算法思路 :
设i为数组中的任意位置,用sum[i]表示[0,i]区间内所有元素的和.
想知道有多少个[以i为结尾的可被k整除的子数组], 就要找到有多少个起始位置为x1,x2,x3......使得[x,i]区间内的所有元素的和可被k整除
设[0,x-1]区间内所有元素之和等于a,[0,i]区间内所有元素的和等于b,可得(b - a) % k == 0
由同余定理可得,[0,x - 1]区间与[0,i]区间内的前缀和同余.于是问题就变成:
找到在[0,i - 1]区间内,有多少前缀和的余数等于sum[i] % k的即可.
我们不需要真的初始化一个前缀和数组,因为我们只关心i位置之前,有多少个前缀和等于sum[i] - k.因此,我们仅需用一个哈希表,一边求当前位置的前缀和,一边存下之前每一种前缀和出现的次数.
示例说明
假设 nums = [4, 5, 0, -2, -3, 1]
,k = 5
。
- 初始状态:
dp = {0: 1}
,sum = 0
,ret = 0
- 逐步计算:
- 加
4
:sum = 4
,r = 4
, 更新dp = {0: 1, 4: 1}
- 加
5
:sum = 9
,r = 4
,ret += 1
(从0
到5
),更新dp = {0: 1, 4: 2}
- 加
0
:sum = 9
,r = 4
,ret += 2
(从0
到5
和4
到5
),更新dp = {0: 1, 4: 3}
- 加
-2
:sum = 7
,r = 2
, 更新dp = {0: 1, 4: 3, 2: 1}
- 加
-3
:sum = 4
,r = 4
,ret += 3
, 更新dp = {0: 1, 4: 4, 2: 1}
- 加
1
:sum = 5
,r = 0
,ret += 1
, 更新dp = {0: 2, 4: 4, 2: 1}
- 加
最后 ret
的值为 4
,表示总共有 4
个子数组的和是 k
的倍数。
也就是说:我们在一段区间sum中,找到有多少个前缀和余数使得(sum % k + k) % k
因为(sum - 前缀和) % k 需要 == 0,根据同余定理,sum % k == 前缀和 % k
代码展示 :
cpp
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
unordered_map<int, int> dp(nums.size());
dp[0] = 1; //0这个余数
int ret = 0, sum = 0;
for(int i = 0; i < nums.size(); i++)
{
sum += nums[i];
int r = (sum % k + k) % k;
if(dp.count(r)) ret += dp[r];
dp[r]++;
}
return ret;
}
};
初始化:
cpp
unordered_map<int, int> dp(nums.size());
dp[0] = 1;
int ret = 0, sum = 0;
创建一个哈希表 dp 用于存储不同余数的出现次数。
初始化 dp[0] = 1,表示初始状态(和为0的情况)。
ret 用于存储结果(符合条件的子数组个数)。
sum 用于存储当前遍历的前缀和。
遍历数组:
cpp
for (int i = 0; i < nums.size(); i++)
{
sum += nums[i];
int r = (sum % k + k) % k;
if (dp.count(r)) ret += dp[r];
dp[r]++;
}
使用 for 循环遍历数组 nums,计算前缀和。
对于当前的前缀和 sum,计算其对k的余数r。使用(sum % k + k) % k 确保余数为非负值。这是因为在 C++ 中,负数取模可能会返回负值。
如果 dp 中存在该余数r,则表示有一些前缀和的组合可以与当前前缀和形成可被k 整除的子数组,因此将这些组合的数量加到 ret 中。
最后,更新 dp 中余数r 的出现次数。
返回结果:
返回结果 ret,即符合条件的子数组数量。
dp[0] 的初始值
初始状态: 在代码的开头,我们将 dp[0] 初始化为 1,这意味着在开始时我们视为有一个和为 0 的"虚拟"子数组。这个初始化是为了帮助处理前缀和等于k 的倍数的情况。
**有效子数组的统计:**dp[0] 代表的是当前前缀和为 0 的子数组的出现次数。当我们在遍历过程中遇到新的前缀和,并且发现其余数为 0 时,就意味着从起始位置到当前的位置有一个有效的子数组。
结果分析 :
时间复杂度和空间复杂度
时间复杂度 :O(n),其中n 是数组的长度,因为我们只遍历数组一次。
空间复杂度 :O(k),因为哈希表 dp 最多存储k 个不同的余数。