1. 问题分析与解法思路
这道题目要求我们统计数组中满足A-B=C的数对数量。乍一看似乎很简单,但仔细分析数据规模后会发现,直接暴力枚举所有可能的数对显然不可行——当N=2×10^5时,O(N^2)的时间复杂度会导致超时。
1.1 关键观察点
首先我们需要将等式变形:A-B=C可以改写为B=A-C。也就是说,对于数组中的每一个元素A,我们需要统计数组中等于A-C的元素B的数量。
这个变形让我们意识到,问题的核心其实可以转化为:对于每个元素A,快速查询数组中A-C出现的次数。这提示我们需要一种高效的查找方法。
1.2 解法选择
基于上述观察,我们可以考虑以下几种解法:
-
哈希表法:使用unordered_map记录每个数字出现的次数,然后遍历数组统计A-C的出现次数。时间复杂度O(N),空间复杂度O(N)。
-
排序+二分法:先对数组排序,然后对于每个元素A,使用二分查找确定A-C的起始和结束位置。时间复杂度O(NlogN),空间复杂度O(1)。
-
双指针法:排序后使用双指针同时遍历数组。时间复杂度O(NlogN),空间复杂度O(1)。
考虑到题目中N可以达到2×10^5,哈希表法虽然理论复杂度最优,但实际运行中常数较大;而双指针法实现起来较为复杂。因此,排序+二分法是一个平衡了实现难度和效率的选择。
2. 排序+二分法详细实现
2.1 算法步骤
-
排序数组:首先将输入数组进行升序排序。这是后续二分查找的基础。
-
遍历数组:对于数组中的每个元素A(从第二个元素开始),计算目标值B=A-C。
-
二分查找:
- 使用二分查找确定第一个等于B的元素位置(左边界)
- 使用二分查找确定第一个大于B的元素位置(右边界)
- 这两个位置之差就是B的出现次数
-
累加结果:将所有A对应的B出现次数累加,得到最终结果。
2.2 代码实现解析
cpp复制#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int n, c;
vector<int> a;
int l, r, mid, pos1, pos2;
long ans; // 注意使用long类型防止溢出
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n >> c;
a.resize(n + 2);
for(int i = 1; i <= n; i++) cin >> a[i];
// 升序排序,注意排序范围是[1,n]
sort(a.begin() + 1, a.end() - 1);
for(int i = 2; i <= n; i++){
// 查找第一个B + C >= A的位置(左边界)
pos1 = a.size() - 1;
l = 1, r = i;
while(l <= r){
mid = l + (r - l) / 2;
if(a[mid] + c >= a[i]){
pos1 = mid;
r = mid - 1;
}
else
l = mid + 1;
}
// 查找第一个B + C > A的位置(右边界)
pos2 = a.size() - 1;
l = 1, r = i;
while(l <= r){
mid = l + (r - l) / 2;
if(a[mid] + c > a[i]){
pos2 = mid;
r = mid - 1;
}
else
l = mid + 1;
}
ans += pos2 - pos1;
}
cout << ans;
return 0;
}
2.3 关键点说明
-
排序范围:注意我们只对数组的有效部分(从1到n)进行排序,因此使用
sort(a.begin()+1, a.end()-1)。 -
二分查找实现:这里实现了两种二分查找:
- 查找第一个≥目标值的位置(左边界)
- 查找第一个>目标值的位置(右边界)
两者之差就是目标值的出现次数。
-
数据类型选择:结果可能很大,必须使用long类型存储,否则会导致溢出。
-
输入输出优化:使用
ios::sync_with_stdio(false)和cin.tie(nullptr)来加速输入输出。
3. 算法优化与变种
3.1 使用STL简化代码
我们可以利用STL中的lower_bound和upper_bound函数来简化二分查找的实现:
cpp复制for(int i = 2; i <= n; i++){
int target = a[i] - c;
auto left = lower_bound(a.begin()+1, a.begin()+i, target);
auto right = upper_bound(a.begin()+1, a.begin()+i, target);
ans += right - left;
}
这种实现更加简洁,但原理与手动实现的二分查找相同。
3.2 哈希表解法
虽然我们选择了排序+二分法,但哈希表解法在某些情况下可能更优:
cpp复制#include<unordered_map>
using namespace std;
unordered_map<int, int> count;
long ans = 0;
for(int num : a){
ans += count[num - c];
count[num]++;
}
哈希表解法的时间复杂度是O(N),但实际运行效率受哈希表实现影响较大,在极端情况下可能退化为O(N^2)。
4. 边界条件与注意事项
4.1 常见错误
-
数组越界:在排序和二分查找时,必须确保只操作数组的有效范围(1到n)。
-
整数溢出:结果可能很大,必须使用long类型存储。
-
重复元素处理:题目明确说明不同位置的相同数字算作不同数对,因此必须统计所有出现的位置。
-
C为0的情况:虽然题目中C≥1,但实际编程时应考虑C=0的情况,此时需要特殊处理(统计每个元素的出现次数)。
4.2 性能优化
-
输入优化:对于大规模数据,使用快速输入方法可以显著提高性能。
-
二分查找范围:对于每个A,只需要在A之前的元素中查找B,因为B必须小于A(因为C>0)。
-
提前终止:如果数组已排序且所有元素都大于C,可以提前终止查找。
5. 复杂度分析
5.1 时间复杂度
- 排序阶段:O(NlogN)
- 二分查找阶段:对于每个元素进行一次lower_bound和upper_bound,各O(logN),总共O(NlogN)
总体时间复杂度:O(NlogN)
5.2 空间复杂度
除了输入数组外,只使用了常数个额外变量,因此空间复杂度为O(1)。
6. 测试用例设计
为了验证代码的正确性,应该设计以下几类测试用例:
-
基本测试用例:
code复制4 1 1 1 2 3预期输出:3
-
无解情况:
code复制4 5 1 2 3 4预期输出:0
-
所有元素相同:
code复制5 0 2 2 2 2 2预期输出:10(如果允许C=0)
-
大规模数据:生成包含2×10^5个元素的随机数组,验证程序是否能在合理时间内完成。
-
边界值测试:
code复制1 1 1预期输出:0
7. 实际应用场景
这类问题在实际中有多种应用场景:
-
数据分析:统计满足特定差值条件的数据对,例如找出价格差为特定值的商品对。
-
生物信息学:在基因序列分析中,寻找具有特定差异的模式。
-
金融分析:识别具有特定价格差的股票对,用于配对交易策略。
-
网络安全:检测具有特定时间差的事件对,可能指示某种攻击模式。
理解这类问题的解法,不仅可以帮助我们解决编程竞赛题目,还能为实际工程问题提供解决思路。