如何降低程序的时间复杂度,提高运行时效?
如何提高运行时效,解决程序运行太慢问题
- 引言
- 今天,我们深入剖析一个典型的多测试用例算法题目:
- 约束与挑战
- 优化关键:
- 代码实现
- 结论
引言
在信息爆炸的时代,人们需要更快速、更简洁地传递信息。网络语言缩写(如“OMG”代表“Oh my God”)正好满足了这一需求,它让复杂的表达浓缩成简短符号,提升了沟通效率。作为算法工程师,我总是被这种“精炼”精神启发:在编程中,如何用最少的代码解决最复杂的问题?
今天,我们深入剖析一个典型的多测试用例算法题目:
-
给定长度为 nnn 的数组 aaa,以及 qqq 个查询,每个查询提供 xxx 和 yyy,找出满足 ai+aj=xa_i + a_j = xai+aj=x 且 ai⋅aj=ya_i \cdot a_j = yai⋅aj=y 的数对 (i,j)(i, j)(i,j)(1≤i<j≤n1 \leq i < j \leq n1≤i<j≤n)的数量。该题目涉及多组测试,约束严格(∑n,∑q≤2×105\sum n, \sum q \leq 2 \times 10^5∑n,∑q≤2×105),考验高效数据结构和数学优化的结合。
这个问题的核心在于避免 O(n2)O(n^2)O(n2) 暴力枚举,通过二次方程根求解转化为 O(1)O(1)O(1) 查询。提供的 C++ 代码框架优秀,但存在浮点精度问题(sqrt 对大整数不精确),我会基于它进行优化,提供完整可运行版本。下面,从问题分析、数学推导,到代码实现,一步步拆解。
问题分析
问题描述 -
输入:
第一行:整数 ttt(测试用例数,1≤t≤1041 \leq t \leq 10^41≤t≤104)。
每个测试用例:
第一行:整数 nnn(1≤n≤2×1051 \leq n \leq 2 \times 10^51≤n≤2×105)。
第二行:nnn 个整数 a1,…,ana_1, \dots, a_na1,…,an(∣ai∣≤109|a_i| \leq 10^9∣ai∣≤109)。
第三行:整数 qqq(1≤q≤2×1051 \leq q \leq 2 \times 10^51≤q≤2×105)。
接下来 qqq 行:每个两个整数 x,yx, yx,y(∣x∣≤2×109|x| \leq 2 \times 10^9∣x∣≤2×109,∣y∣≤1018|y| \leq 10^{18}∣y∣≤1018)。 -
保证 ∑n≤2×105\sum n \leq 2 \times 10^5∑n≤2×105,∑q≤2×105\sum q \leq 2 \times 10^5∑q≤2×105。
-
输出:每个测试用例,对于 qqq 个查询,每行输出一个答案(数对数量)。
示例(单测试用例简化):
n=3n=3n=3,数组 [1,3,2][1, 3, 2][1,3,2],q=1q=1q=1,查询 x=3,y=2x=3, y=2x=3,y=2。
分析:
i=1,j=2i=1, j=2i=1,j=2: 1+3=4≠31+3=4 \neq 31+3=4=3,1×3=3≠21 \times 3=3 \neq 21×3=3=2。
i=1,j=3i=1, j=3i=1,j=3: 1+2=3=31+2=3 = 31+2=3=3,1×2=2=21 \times 2=2 = 21×2=2=2(满足)。
i=2,j=3i=2, j=3i=2,j=3: 3+2=5≠33+2=5 \neq 33+2=5=3,3×2=6≠23 \times 2=6 \neq 23×2=6=2。 -
答案:1
约束与挑战
-
多测试用例:需循环处理,但总规模 O(∑n+∑q)O(\sum n + \sum q)O(∑n+∑q)。
大范围:aia_iai 可负,x2−4yx^2 - 4yx2−4y 可达 ±8×1018\pm 8 \times 10^{18}±8×1018,需 64 位整数(long long)。
重复元素:需频率计数,处理组合 (f2)\binom{f}{2}(2f)。
挑战:判别式 d=x2−4yd = x^2 - 4yd=x2−4y 求整数平方根,避免浮点误差(double 精度仅至 101510^{15}1015)。
时间要求:总 O((∑n+∑q)logn)O((\sum n + \sum q) \log n)O((∑n+∑q)logn) 可接受,使用 map 或 unordered_map。 -
为什么这个题目有趣?
它将代数(二次方程)与数据结构(哈希计数)融合,体现了“缩写”般的算法美学:从海量数据中“浓缩”答案。实际场景如大数据匹配、密码学中的因式分解优化。
数学推导:从方程到根求解
给定 ai+aj=xa_i + a_j = xai+aj=x,ai⋅aj=ya_i \cdot a_j = yai⋅aj=y,则 ai,aja_i, a_jai,aj 为方程 t2−xt+y=0t^2 - x t + y = 0t2−xt+y=0 的根。 -
判别式:d=x2−4yd = x^2 - 4yd=x2−4y。
若 d<0d < 0d<0,无实根,答案 0。
若 d=0d = 0d=0,双根 r=x/2r = x / 2r=x/2。若 xxx 偶数且 rrr 在数组中出现 fff 次,答案 f(f−1)2\frac{f(f-1)}{2}2f(f−1)。
若 d>0d > 0d>0,根 r1=x+d2r_1 = \frac{x + \sqrt{d}}{2}r1=2x+d,r2=x−d2r_2 = \frac{x - \sqrt{d}}{2}r2=2x−d。
d\sqrt{d}d 须为整数(完美平方),且 x+dx + \sqrt{d}x+d 偶数(确保 r1,r2r_1, r_2r1,r2 整数)。
若 r1=r2r_1 = r_2r1=r2,同上;否则,答案 f1×f2f_1 \times f_2f1×f2。
优化关键:
- 预处理:用 map<long long, long long> 统计频率(支持负键)。
查询:计算 ddd,用整数二分求 d\sqrt{d}d(避免浮点)。
验证:r1+r2=xr_1 + r_2 = xr1+r2=x,r1×r2=yr_1 \times r_2 = yr1×r2=y(虽理论成立,但防计算误差)。
此法每个查询 O(log109)O(\log 10^9)O(log109)(二分求 sqrt),总时间 O((∑n+∑q)logn)O((\sum n + \sum q) \log n)O((∑n+∑q)logn),高效。
解决方案设计
整体框架
-
预处理:每个测试用例清空 map,读 nnn 个 aia_iai,计数频率 O(nlogn)O(n \log n)O(nlogn)。
查询处理:读 qqq,对每个:
计算 d=x∗x−4∗yd = x*x - 4*yd=x∗x−4∗y(注意符号)。
若 d<0d < 0d<0,输出 0。
二分求 sd=⌊d⌋sd = \lfloor \sqrt{d} \rfloorsd=⌊d⌋,检查 sd×sd==dsd \times sd == dsd×sd==d。
若 (x+sd)%2==0(x + sd) \% 2 == 0(x+sd)%2==0,计算 r1=(x+sd)/2r_1 = (x + sd)/2r1=(x+sd)/2,r2=(x−sd)/2r_2 = (x - sd)/2r2=(x−sd)/2。
根据 r1==r2r_1 == r_2r1==r2 计算组合数。 -
边界处理:
d=0d = 0d=0: 特殊处理 sd=0sd = 0sd=0。
负 ddd: 跳过。
溢出:C++ long long 乘法安全(2×109×2×109=4×1018<9×10182 \times 10^9 \times 2 \times 10^9 = 4 \times 10^{18} < 9 \times 10^{18}2×109×2×109=4×1018<9×1018)。
map vs unordered_map:map 稳定,支持负键;unordered_map 更快但需哈希。
时间与空间复杂度
- 时间:预处理 O(∑nlogn)O(\sum n \log n)O(∑nlogn),查询 O(∑qlog109)≈O(2×105×30)=6×106O(\sum q \log 10^9) \approx O(2 \times 10^5 \times 30) = 6 \times 10^6O(∑qlog109)≈O(2×105×30)=6×106,优秀。
空间:O(∑n)O(\sum n)O(∑n),最坏 2×1052 \times 10^52×105。
潜在扩展
- 若 aia_iai 非整数:退化为近似匹配,复杂度飙升。
分布式:用 Redis 存频率。
代码实现
- 基于提供的代码框架,我优化了 sqrt 计算:引入整数二分求完美平方根,避免浮点误差。同时,添加 ios_base::sync_with_stdio(false) 加速 I/O,处理多测试用例。完整代码如下(使用 map 确保稳定性):
#include <bits/stdc++.h>
using namespace std;long long integer_sqrt(long long n) {if (n < 0) return -1;if (n == 0) return 0;long long l = 1, r = min(n, 3000000000LL); // sqrt(8e18) ~ 2.8e9while (l <= r) {long long m = l + (r - l) / 2;if (m <= n / m) {l = m + 1;} else {r = m - 1;}}return r; // floor(sqrt(n))
}int main() {ios_base::sync_with_stdio(false);cin.tie(NULL);int t;cin >> t;while (t--) {int n;cin >> n;map<long long, long long> cnt;for (int i = 0; i < n; i++) {long long number;cin >> number;cnt[number]++;}long long q;cin >> q;while (q--) {long long x, y, res = 0;cin >> x >> y;long long d = x * x - 4 * y;if (d < 0) {cout << 0 << '\n';continue;}long long sd = integer_sqrt(d);if (sd * sd != d) {cout << 0 << '\n';continue;}if ((x + sd) % 2 == 0) {long long r1 = (x + sd) / 2;long long r2 = (x - sd) / 2;if (r1 == r2) {res = cnt[r1] * (cnt[r1] - 1) / 2;} else {res = cnt[r1] * cnt[r2];}}cout << res << '\n';}}return 0;
}
代码解释
integer_sqrt:二分求 ⌊n⌋\lfloor \sqrt{n} \rfloor⌊n⌋,时间 O(log109)O(\log 10^9)O(log109)。边界 r=3×109r = 3 \times 10^9r=3×109 覆盖最大 ddd。
精确检查:sd * sd != d 过滤非完美平方(乘法安全,无溢出)。
I/O 优化:cin.tie(NULL) 加速大输入。
验证省略:数学上 r1,r2r_1, r_2r1,r2 必满足原方程,但可加 if(r1 + r2 != x || r1 * r2 != y) res=0; 增强鲁棒。
测试:对于示例输入(t=1, n=3, a=[1,3,2], q=1, x=3 y=2),输出 1。适用于大 ddd 如 x=2×109,y=0x=2 \times 10^9, y=0x=2×109,y=0,d=4×1018d=4 \times 10^{18}d=4×1018,sd=2e9。
结论
- 计算机的一切都是人赋予的,别人能做的事情,我也可以!没有天赋,就一直重复!
