markdown复制## 1. 题目背景与核心问题解析
这道来自Codeforces 1857的F题"Sum and Product"乍看是个数学问题,实际上考察的是对数据结构与算法思维的灵活运用。题目给出一个包含n个整数的数组a,以及q次查询,每次查询给出两个数x和y,要求找出数组中满足a_i + a_j = x且a_i * a_j = y的(i,j)对数(i<j)。
这个问题的本质可以转化为求解二元一次方程组的整数解。根据韦达定理,如果设a_i和a_j为方程t^2 - xt + y = 0的两个根,那么问题就转化为在数组中统计特定数值对的出现次数。这种将组合查询转化为数学方程的思路,是解决此类问题的关键突破口。
## 2. 数学原理与算法选择
### 2.1 韦达定理的应用
对于给定的x和y,我们可以建立以下方程组:
a + b = x
a * b = y
code复制根据韦达定理,a和b必然是方程t^2 - xt + y = 0的实数根。因此,解题的第一步是解这个二次方程:
判别式Δ = x² - 4y
当Δ < 0时,无实数解,直接返回0
当Δ ≥ 0时,解为:
t1 = (x + √Δ)/2
t2 = (x - √Δ)/2
### 2.2 解的有效性验证
得到t1和t2后,需要验证它们是否满足:
1. 是整数(因为数组元素为整数)
2. 存在于原始数组中
这里有个关键细节:即使t1和t2是整数,也需要检查它们在数组中实际存在的次数。例如,当t1 == t2时,组合数为C(cnt, 2) = cnt*(cnt-1)/2;当t1 != t2时,组合数为cnt[t1] * cnt[t2]。
## 3. 高效实现方案
### 3.1 预处理阶段
为了快速查询,我们需要预先处理数组:
```cpp
unordered_map<int, int> freq;
for (int num : a) {
freq[num]++;
}
这个频率统计哈希表将后续查询的时间复杂度降为O(1)级别。
3.2 查询处理流程
对于每个查询(x,y):
- 计算判别式Δ = xx - 4y
- 如果Δ < 0 → 返回0
- 计算sqrtΔ = sqrt(Δ)
- 如果sqrtΔ不是整数 → 返回0
- 计算t1 = (x + sqrtΔ)/2
- 如果t1不是整数 → 返回0
- 计算t2 = (x - sqrtΔ)/2
- 如果t2不是整数 → 返回0
- 如果t1 == t2:
- 返回C(freq[t1], 2) = freq[t1]*(freq[t1]-1)/2
- 否则:
- 返回freq[t1] * freq[t2]
3.3 边界情况处理
需要特别注意几种特殊情况:
- x和y为0的情况
- 解为负数的情况
- 32位整数溢出的情况(使用long long)
- 浮点数精度问题(避免直接比较浮点数)
4. 复杂度分析与优化
4.1 时间复杂度
- 预处理:O(n)构建频率哈希表
- 每个查询:O(1)数学运算 + O(1)哈希查找
- 总体:O(n + q)
4.2 空间复杂度
- O(n)存储频率表
- 无其他额外空间消耗
4.3 可能的优化方向
- 使用更快的哈希表实现(如gp_hash_table)
- 预先计算所有可能的平方值(针对x范围有限时)
- 并行处理查询(当q非常大时)
5. 代码实现示例
cpp复制#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
int n;
cin >> n;
unordered_map<ll, ll> freq;
for (int i = 0; i < n; ++i) {
ll num;
cin >> num;
freq[num]++;
}
int q;
cin >> q;
while (q--) {
ll x, y;
cin >> x >> y;
ll delta = x * x - 4 * y;
if (delta < 0) {
cout << "0 ";
continue;
}
ll sqrt_delta = sqrt(delta);
if (sqrt_delta * sqrt_delta != delta) {
cout << "0 ";
continue;
}
ll t1 = (x + sqrt_delta);
ll t2 = (x - sqrt_delta);
if (t1 % 2 != 0 || t2 % 2 != 0) {
cout << "0 ";
continue;
}
t1 /= 2;
t2 /= 2;
if (t1 == t2) {
cout << freq[t1] * (freq[t1] - 1) / 2 << " ";
} else {
cout << freq[t1] * freq[t2] << " ";
}
}
cout << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
6. 常见错误与调试技巧
6.1 典型错误案例
-
整数溢出:没有使用long long导致中间计算结果溢出
- 修复:确保所有涉及x²和4*y的计算使用64位整数
-
浮点精度问题:直接比较浮点数是否相等
- 修复:检查平方根是否为整数时,先取整再平方比较
-
边界条件遗漏:未处理t1或t2为负数的情况
- 修复:增加检查步骤,确保解在数组的可能取值范围内
6.2 调试建议
-
制作小规模测试用例:
- 数组:[1,1,2,3,3]
- 查询:(4,4)→应返回1(1+3=4,1*3=3)
-
打印中间变量:
- 输出delta、sqrt_delta、t1、t2的值验证计算过程
-
极端情况测试:
- 大数测试(如x=2e9, y=1e18)
- 全零数组测试
- 重复元素测试
7. 算法扩展与变种思考
这个问题可以有几种有趣的变体:
-
三元组版本:找满足a+b+c=x, abc=y的三元组
- 解法:转化为三次方程求根
-
模数版本:所有运算在模M下进行
- 需要处理模逆元和模平方根
-
范围查询:在数组子区间内进行查询
- 可能需要使用莫队算法或线段树
在实际比赛中,这类数学与数据结构结合的问题非常常见。掌握将组合条件转化为方程求解的思路,能够有效解决一大类相似问题。我个人的经验是,遇到这种"找满足特定关系的元素对"的问题,首先考虑能否建立数学模型,其次才是暴力枚举的优化方案。
code复制