给定一个长度为n的整数数组,要求统计其中和等于x的连续子数组的数量。连续子数组指的是数组中一段连续的元素序列。例如,对于数组[1,2,3],其连续子数组包括[1]、[2]、[3]、[1,2]、[2,3]和[1,2,3]。
最直观的解法是使用双重循环枚举所有可能的子数组:
cpp复制#include<bits/stdc++.h>
using namespace std;
int main() {
int n, x, ans = 0;
int arr[100005];
cin >> n >> x;
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
for (int i = 0; i < n; i++) {
int sum = 0;
for (int j = i; j < n; j++) {
sum += arr[j];
if (sum == x) ans++;
}
}
cout << ans;
return 0;
}
这种暴力解法的时间复杂度为O(n²),当n较大时(比如n=10^5),计算量会达到10^10次操作,这在竞赛环境下肯定会超时。因此我们需要寻找更高效的算法。
提示:在实际编程竞赛中,通常要求算法在1秒内完成,对应的操作次数上限约为10^7-10^8次。
前缀和是一种预处理技术,可以快速计算任意子数组的和。定义前缀和数组prefix,其中prefix[i]表示数组前i个元素的和。有了前缀和数组后,计算子数组arr[i..j]的和可以简化为prefix[j+1]-prefix[i]。
我们可以利用前缀和的性质来优化算法:
cpp复制#include<bits/stdc++.h>
using namespace std;
int main() {
int n, x, sum = 0, ans = 0;
int arr[100005];
unordered_map<int, int> prefix_count;
cin >> n >> x;
prefix_count[0] = 1; // 初始状态:前缀和为0出现1次
for (int i = 0; i < n; i++) {
cin >> arr[i];
sum += arr[i];
int target = sum - x;
ans += prefix_count[target];
prefix_count[sum]++;
}
cout << ans;
return 0;
}
这种优化后的算法只需要一次遍历数组,每次操作哈希表的时间复杂度平均为O(1),因此整体时间复杂度降为O(n),可以高效处理大规模数据。
这是因为我们需要考虑从数组开头开始的子数组。当某个前缀和正好等于x时,sum-x=0,此时应该能够找到对应的计数。初始化prefix_count[0]=1确保了这种情况能被正确处理。
这个算法可以完美处理数组中包含负数的情况。传统的滑动窗口方法在处理负数时会失效,但前缀和+哈希表的方法不受负数影响。
我们使用unordered_map而不是map,因为:
这种前缀和+哈希表的技术可以解决一系列相关问题:
如果要找和最长为x的子数组,可以修改哈希表记录方式:
cpp复制unordered_map<int, int> first_occurrence;
first_occurrence[0] = -1; // 初始化
int max_len = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
if (first_occurrence.count(sum - x)) {
max_len = max(max_len, i - first_occurrence[sum - x]);
}
if (!first_occurrence.count(sum)) {
first_occurrence[sum] = i;
}
}
对于二维数组,可以先将问题转化为多个一维问题,再使用前缀和技术。具体方法是:
当数组元素较大或较多时,前缀和可能会超出int范围。解决方法:
虽然unordered_map平均时间复杂度是O(1),但在最坏情况下可能退化到O(n)。解决方法:
设计测试用例时应考虑:
例如:
code复制// 测试用例1:普通情况
5 3
1 2 3 -1 2
// 测试用例2:全x数组
4 5
5 5 5 5
// 测试用例3:包含负数
6 4
1 -1 2 3 -2 1
在C++中,对于大规模数据,可以添加以下优化:
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
对于性能关键的应用,可以自定义哈希函数:
cpp复制struct custom_hash {
size_t operator()(uint64_t x) const {
static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count();
x ^= FIXED_RANDOM;
return x ^ (x >> 16);
}
};
unordered_map<int, int, custom_hash> safe_map;
如果只需要知道是否存在特定和的子数组,而不需要计数,可以用unordered_set代替unordered_map来节省空间。
在实际编程竞赛中,选择算法的考虑因素:
对于本题,当n≤1000时,两种方法都可以;当n>10000时,必须使用优化方法。在蓝桥杯等竞赛中,通常n会设置得较大,以区分不同水平的选手。