第67章 算法优化
算法优化是程序设计中的核心能力,旨在通过改进算法逻辑、数据结构或实现细节,降低时间复杂度或空间复杂度,提升程序的执行效率。
67.1 算法优化的基本概念
67.1.1 优化目标
- 时间复杂度优化:减少程序运行时间,例如把优化为;
- 空间复杂度优化:减少内存占用,避免内存溢出,例如优化为;
- 可读性与可维护性:优化同时保证代码清晰易懂。
67.1.2 优化原则
- 先保证正确性,再追求效率;
- 针对性优化,定位程序瓶颈再改进;
- 时间与空间往往需要权衡,存在空间换时间、时间换空间两种思路。
很多同学一上来就改算法,结果"快但错"。正确顺序是:先写出保证正确的朴素版本 → 用样例和小数据验证 → 再针对性优化瓶颈。八级判分按测试点给分,朴素分(部分分)也比错解的 0 分强。
67.2 时间复杂度优化策略
67.2.1 更换更高效的算法与数据结构
- 排序场景:冒泡排序 → 快速/归并排序
- 查找场景:顺序查找 → 二分查找(有序前提下)
- 单源最短路:Bellman-Ford → 堆优化Dijkstra
- 频繁增删查:数组 → unordered_map/哈希表(平均)
67.2.2 消除重复计算、记忆化缓存
重叠子问题缓存结果,避免重复递归计算,典型如斐波那契:
// 低效递归(指数复杂度)
int fibBad(int n){
if(n <= 2) return 1;
return fibBad(n-1) + fibBad(n-2);
}
// 记忆化优化 O(n)
vector<int> memo;
int fibOpt(int n){
if(n <= 2) return 1;
if(memo[n] != 0) return memo[n];
memo[n] = fibOpt(n-1) + fibOpt(n-2);
return memo[n];
}
缩小循环范围:判断素数只需循环至,不用遍历到。
67.2.3 预处理数组
前缀和、前缀最值数组,把区间查询从降至。
67.2.4 剪枝(搜索类优化)
DFS、回溯中提前舍弃不可能得到最优解的分支,减少搜索范围。
67.3 空间复杂度优化策略
67.3.1 滚动数组(动态规划专用)
仅保存上一层状态,二维DP压缩一维。示例斐波那契空间:
int fibSpaceOpt(int n){
if(n <= 1) return n;
int a = 0, b = 1, c;
for(int i = 2; i <= n; i++){
c = a + b;
a = b;
b = c;
}
return b;
}
67.3.2 变量复用、类型精简
能用int不用long long;复用临时变量,不重复创建数组。
67.3.3 位运算压缩布尔标记
用单个int存储32个开关状态,代替bool数组。
67.4 代码层面细节优化
- 循环优化:将循环内固定计算提到循环外部;适度循环展开;
- 内存访问:数组优先顺序遍历,利用缓存局部性;
- 传参优化:大型容器用引用
&传递,避免拷贝;简单函数inline; - 位运算替代乘除2、奇偶判断:
x >> 1等价x / 2;x & 1判断奇偶。
67.5 经典优化案例
- DP优化:01背包二维数组改为一维逆序遍历;
- 字符串匹配:暴力 → KMP ;
- 图算法:朴素Dijkstra → 小根堆优化。
67.6 真题示例(用知识/数据结构优化)
67.6.1 等差数列求和:循环 vs 公式
【GESP 八级风格·编程题】 输入 (),输出 的值。
朴素循环(, 大时超时):
long long sum = 0;
for (long long i = 1; i <= n; ++i) sum += i; // n=10^9 时约 10 亿次,超时
公式优化(,一步出结果):
long long ans = n * (n + 1) / 2; // 等差数列求和公式
n*(n+1) 在 时约 ,恰好卡在 long long 上界附近,可能溢出。稳妥写法:
long long ans = n % 2 == 0 ? (n / 2) * (n + 1) : n * ((n + 1) / 2);
先除后乘,避免中间乘积溢出。这正体现"用知识优化算法"时也要注意实现细节。
67.6.2 多次区间求和:前缀和
【GESP 八级风格·编程题】 给定长度为 的数组和 次询问,每次问区间 的和。。
朴素(每次 ,总 超时): 每次重新累加。
前缀和优化(预处理 ,每次询问 ):
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, q; cin >> n >> q;
vector<long long> a(n), pre(n + 1, 0);
for (int i = 0; i < n; ++i) {
cin >> a[i];
pre[i + 1] = pre[i] + a[i]; // pre[i+1] = 前 i 个元素和
}
while (q--) {
int l, r; cin >> l >> r; // 假设下标从 1 开始
cout << pre[r] - pre[l - 1] << "\n"; // 区间和 = 前缀差
}
return 0;
}
凡是"很多次区间求和/最值",几乎都可以用前缀和(求和)或 ST 表/线段树(最值)把每次询问降到 或 。这是八级"算法优化"最高频的考点之一。
练习题
下列选择题考查优化思路、典型优化手段与复杂度变化,参考答案默认折叠,点击"参考答案"展开。
1.(递归优化) 朴素斐波那契递归 fib(n)=fib(n-1)+fib(n-2) 的时间复杂度是?记忆化后变为?
- A. ,
- B. ,
- C. ,
- D. ,
参考答案
B。朴素递归存在大量重复子问题,指数级 ;记忆化后每个状态只算一次,降到 。
2.(空间优化) 01 背包用二维 dp[i][j] 可优化为一维数组,关键技巧是?
- A. 把 j 循环改为正序
- B. 把 j 循环改为逆序(从大到小)
- C. 去掉 i 这一维即可,顺序随意
- D. 用递归替代
参考答案
B。一维背包必须逆序遍历容量,才能保证更新当前物品时用到的是"上一轮(i-1)"的状态,否则会重复使用同一物品(变成完全背包)。
3.(公式优化) 求 ,用公式 的复杂度是?
- A.
- B.
- C.
- D.
参考答案
B。公式一步计算,与 大小无关,常数时间 ;循环累加才是 。
4.(空间换时间) 把"数组里有没有出现过 x"从 查找优化到平均 ,常用?
- A. 排序后二分
- B. 哈希表(unordered_map / 数组标记)
- C. 前缀和
- D. 并查集
参考答案
B。哈希表平均 查/插,是用"额外空间"换"查询时间"的典型;排序+二分是 ,前缀和与并查集不解决此类查找。
5.(前缀和) 对长度为 的数组做 次区间求和,用前缀和预处理 + 查询的复杂度是?
- A. 预处理 ,每次
- B. 预处理 ,每次
- C. 预处理 ,每次
- D. 预处理 ,每次
参考答案
A。前缀和数组一遍扫出(),区间和用两个前缀值相减(),把总复杂度从朴素 降到 。