蓝桥杯真题——k倍区间
目录
题目链接:2.k倍区间 - 蓝桥云课
注:下述题目描述和示例均来自蓝桥云客
题目描述
给定一个长度为 NN 的数列,A1,A2,⋯ANA1,A2,⋯AN,如果其中一段连续的子序列 Ai,Ai+1,⋯AjAi,Ai+1,⋯Aj ( i≤ji≤j ) 之和是 KK 的倍数,我们就称这个区间 [i,j][i,j] 是 K 倍区间。
你能求出数列中总共有多少个 KK 倍区间吗?
输入描述
第一行包含两个整数 NN 和 KK( 1≤N,K≤1051≤N,K≤105 )。
以下 N 行每行包含一个整数 AiAi ( 1≤Ai≤1051≤Ai≤105 )
输出描述
输出一个整数,代表 K 倍区间的数目。
输入输出样例
示例
输入
5 2
1
2
3
4
5
输出
6
运行限制
- 最大运行时间:2s
- 最大运行内存: 256M
解法一:暴力(一点分是一点分)
思路:
- 暴力解法:通过两层嵌套循环,遍历所有可能的子数组区间
A[i]
到A[j]
,逐个计算每个子数组的和,然后判断是否为K
的倍数。 - 外层循环用于选择子数组的起点
i
,内层循环用于遍历从i
开始的所有子数组并计算其和total
,判断total % K == 0
是否成立。
具体流程:
- 输入处理:首先从输入中读取数列的长度
N
和倍数K
,然后将数列的元素存储在数组nums
中。 - 两层循环:
- 外层循环遍历每一个起点
i
。 - 内层循环从
i
开始,向后遍历,累加当前区间的和total
。 - 每次计算和
total
后,判断其是否为K
的倍数,如果是,则增加结果res
。
- 外层循环遍历每一个起点
- 输出结果:输出符合条件的子数组的数量
res
。
优缺点:
-
时间复杂度:暴力解法的时间复杂度为
O(N^2)
,因为外层循环遍历N
次,每次内层循环最多遍历N
次,导致总时间复杂度为O(N^2)
。当N
最大为10^5
时,这种复杂度不可接受,可能导致超时。不巧,这里的N最大还真是10^5 -
空间复杂度:空间复杂度为
O(N)
,因为只使用了一个大小为N
的数组来存储输入数据。
Java写法:
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long n = sc.nextLong();
long k = sc.nextLong();
long[] nums = new long[(int)n];
for(int i = 0; i < n; i++){
// 将所有的整数都存储到数组之中
nums[i] = sc.nextLong();
}
long res = 0;
for(int i = 0; i < n; i++){
long total = 0;
for(int j = i; j >= 0; j--){
if((total += nums[j]) % k == 0){
// System.out.println(nums[i] + ":" + total);
res++;
}
}
}
System.out.print(res);
sc.close();
}
}
C++写法:
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
int main() {
// 输入 N 和 K
long long n, k;
cin >> n >> k;
// 存储输入的数列
vector<long long> nums(n);
for (int i = 0; i < n; i++) {
cin >> nums[i]; // 输入每个数字
}
long long res = 0; // 结果计数器
// 遍历所有的子数组区间
for (int i = 0; i < n; i++) {
long long total = 0; // 当前子数组的和
for (int j = i; j >= 0; j--) {
total += nums[j]; // 计算从 i 到 j 的子数组和
if (total % k == 0) { // 如果当前和是 K 的倍数
res++; // 增加结果计数器
}
}
}
cout << res << endl; // 输出结果
return 0;
}
AC情况
时间复杂度和空间复杂度
- 时间复杂度:
O()
,遍历一次数组,每次操作都在常数时间内完成。 - 空间复杂度:
O(K)
,哈希表最多存储K
个不同的模值。
解法二:前缀和+哈希表优化
要优化这个问题,可以利用 前缀和 和 哈希表 来减少时间复杂度,从 O(N^2)
降到 O(N)
。具体思路如下:
优化思路:
-
前缀和:用一个数组
prefix_sum
存储从数组起始到当前位置的累加和。对于任意两个位置i
和j
(i <= j
),子数组的和可以通过前缀和的差来计算:
-
如果
prefix_sum[j] - prefix_sum[i-1]
是K
的倍数,那么prefix_sum[j] % K == prefix_sum[i-1] % K(也就是说倍数减倍数也就是倍数少了几倍而已还是倍数)
。 -
哈希表:通过哈希表记录每个前缀和的模
K
的出现次数。当计算到位置j
时,检查当前prefix_sum[j] % K
是否在哈希表中出现过。如果出现过,说明有一些区间的和是K
的倍数。
算法实现:
- 使用一个哈希表
mod_count
来记录每个前缀和对K
取模的结果出现的次数。 - 遍历数组时,计算当前的前缀和并取模,判断是否之前出现过相同的模值,若出现过,说明存在一个或多个区间的和是
K
的倍数。 - 对于每个
prefix_sum[j] % K
,如果它在哈希表中已经存在,说明之前的某些前缀和与当前位置的前缀和之间的区间和是K
的倍数。
代码解释:
- 输入:首先输入
n
和k
,然后读取数组nums
。 - 初始化:
prefix_sum
用来存储当前的前缀和。mod_count
哈希表用于存储每个前缀和对k
取模的结果及其出现次数。初始时mod_count[0] = 1
,因为一个前缀和为0
的区间本身就符合条件。
- 遍历:
- 在遍历每个元素时,更新
prefix_sum
,然后计算当前prefix_sum % k
。 - 如果该模值已经出现在哈希表中,说明存在从某个位置到当前的位置的子数组和是
k
的倍数。每次找到一个符合条件的模值,结果计数器res
增加。 - 最后更新哈希表中该模值的计数。
- 在遍历每个元素时,更新
- 输出:遍历结束后输出结果。
Java写法:
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 输入 N 和 K
long n = sc.nextLong();
long k = sc.nextLong();
// 存储输入的数列
long[] nums = new long[(int) n];
// 输入每个数字
for (int i = 0; i < n; i++) {
nums[i] = sc.nextLong();
}
// 结果计数器
long res = 0;
// 当前前缀和
long prefixSum = 0;
// 哈希表,用于存储 prefixSum % K 的计数
Map<Long, Long> modCount = new HashMap<>();
// 初始条件:前缀和为 0 时的模为 0,表示从数组开始到当前的子数组和为 K 的倍数
modCount.put(0L, 1L);
// 遍历数列
for (int i = 0; i < n; i++) {
// 更新前缀和
prefixSum += nums[i];
// 计算当前前缀和对 K 的模
long mod = prefixSum % k;
// 如果当前 mod 在哈希表中出现过,则说明存在前缀和差为 K 的倍数的子数组
if (modCount.containsKey(mod)) {
// 增加结果计数,将其+1
res += modCount.get(mod);
}
// 更新当前 mod 的计数,将其+1
modCount.put(mod, modCount.getOrDefault(mod, 0L) + 1);
}
// 输出结果
System.out.println(res);
sc.close();
}
}
C++写法:
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
int main() {
// 输入 N 和 K
long long n, k;
cin >> n >> k;
// 存储输入的数列
vector<long long> nums(n);
for (int i = 0; i < n; i++) {
cin >> nums[i]; // 输入每个数字
}
long long res = 0; // 结果计数器
long long prefix_sum = 0; // 当前前缀和
unordered_map<long long, long long> mod_count; // 哈希表,用于存储 prefix_sum % K 的计数
// 初始条件:前缀和为 0 时的模为 0,表示从数组开始到当前的子数组和为 K 的倍数
mod_count[0] = 1;
// 遍历数列
for (int i = 0; i < n; i++) {
prefix_sum += nums[i]; // 更新前缀和
// 计算当前前缀和对 K 的模
long long mod = prefix_sum % k;
// 如果 mod 为负数,调整为非负数(在 C++ 中,负数取模会得到负数)
if (mod < 0) mod += k;
// 如果当前 mod 在哈希表中出现过,则说明存在前缀和差为 K 的倍数的子数组
if (mod_count.find(mod) != mod_count.end()) {
res += mod_count[mod]; // 增加结果计数
}
// 更新当前 mod 的计数
mod_count[mod]++;
}
cout << res << endl; // 输出结果
return 0;
}
这行代码:modCount.put(0L, 1L);
modCount.put(0L, 1L);
作用是在哈希表中初始化前缀和为 0
的情况,它表示从数组的开始位置到当前位置之间的子数组和恰好是 K
的倍数。为了理解为什么这么设置,可以从以下几个角度来解释:
1. 前缀和的含义:
前缀和 prefixSum[i]
是从数组起始位置到第 i
个元素的和。假设数组 nums
为 [A1, A2, ..., An]
,那么前缀和是:
prefixSum[i] = A1 + A2 + ... + Ai
如果一个子数组 A[i, j]
的和是 K
的倍数,那么 prefixSum[j] - prefixSum[i-1]
必须是 K
的倍数。也就是说:
↓↓↓↓这里很重要↓↓↓↓
这等价于:
2. 初始化模值为 0:
假设我们从数组的开始位置就能够找到一个子数组和为 K
的倍数。为了方便这个情况的处理,我们需要在哈希表 modCount
中提前设置 modCount[0] = 1
,表示前缀和为 0
时的模已经出现过一次。
为什么要设置为 1
呢?这表明如果我们在遍历过程中遇到一个前缀和的模值为 0
,它意味着从数组的起始位置到当前位置之间的子数组和正好是 K
的倍数。这相当于一个“空的”子数组(从数组的开始位置到当前位置)的和是 K
的倍数。
3. 实际含义:
- 当遍历数组时,
prefixSum[i]
表示当前数组从起始位置到第i
个位置的元素和。 - 如果在某个位置
i
,prefixSum[i] % K == 0
,意味着从数组的第一个元素到当前元素的和就是K
的倍数。因此,我们需要提前设定modCount[0] = 1
,表示从数组开始到当前子数组的和是K
的倍数。
4. 避免漏掉边界情况:
例如,假设 nums = [5, 10, 15]
,K = 5
,此时从第一个元素到第三个元素的和 5 + 10 + 15 = 30
是 5
的倍数。如果没有 modCount[0] = 1
作为初始条件,我们可能会漏掉这个情况。
运行时间
时间复杂度和空间复杂度
- 时间复杂度:
O(N)
,遍历一次数组,每次操作都在常数时间内完成。 - 空间复杂度:
O(K)
,哈希表最多存储K
个不同的模值。
总结
我们讲解了首先使用暴力的算法可以的一点分,但是对于N^5的数来说O(N^2)就无法通过全部的用例了,接下来介绍了如何通过使用前缀和与哈希表优化计算子数组和为 K
的倍数的个数问题,最终将时间复杂度从暴力解法的 O(N^2)
降到 O(N)
,使得算法能够高效地处理大规模数据。
1. 题目描述
给定一个长度为 N
的数列,要求统计其中有多少个连续子数组,其和是 K
的倍数。数列和 K
的值都可能较大,因此不能采用暴力枚举的方式来解决。
2. 暴力解法
暴力解法的思路是:通过两层嵌套循环,遍历所有可能的子数组区间,计算每个子数组的和,并判断其是否为 K
的倍数。这种方式的时间复杂度为 O(N^2)
,对于 N
最大可达 10^5
的情况,效率低下,不适用于大规模数据。
3. 优化思路:前缀和 + 哈希表
为了将暴力解法优化到 O(N)
,可以利用前缀和与哈希表的组合。
前缀和的思想:
前缀和是指数组中从第 1
个元素到第 i
个元素的累加和。对于任意两个位置 i
和 j
,prefixSum[j] - prefixSum[i-1]
即为 A[i]
到 A[j]
的子数组和。如果这个差值是 K
的倍数,那么 prefixSum[j] % K == prefixSum[i-1] % K
。
使用哈希表:
我们通过一个哈希表 mod_count
来记录每个前缀和对 K
取模的结果出现的次数。每次计算新的前缀和时,查看是否存在相同的模值,若存在,说明从某个位置到当前的位置之间的子数组和是 K
的倍数。
关键点:
咱就是说:
↓↓↓↓这里很重要↓↓↓↓
这等价于:
原文地址:https://blog.csdn.net/DDDDWJDDDD/article/details/143746397
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!