第67章 算法优化

第67章 算法优化

算法优化是程序设计中的核心能力,旨在通过改进算法逻辑、数据结构或实现细节,降低时间复杂度或空间复杂度,提升程序的执行效率。

67.1 算法优化的基本概念

67.1.1 优化目标

  1. 时间复杂度优化:减少程序运行时间,例如把O(n2)O(n^2)优化为O(nlog⁡n)O(n\log n);
  2. 空间复杂度优化:减少内存占用,避免内存溢出,例如O(n)O(n)优化为O(1)O(1);
  3. 可读性与可维护性:优化同时保证代码清晰易懂。

67.1.2 优化原则

  1. 先保证正确性,再追求效率;
  2. 针对性优化,定位程序瓶颈再改进;
  3. 时间与空间往往需要权衡,存在空间换时间、时间换空间两种思路。
优化第一原则:先对再快

很多同学一上来就改算法,结果"快但错"。正确顺序是:先写出保证正确的朴素版本 → 用样例和小数据验证 → 再针对性优化瓶颈。八级判分按测试点给分,朴素分(部分分)也比错解的 0 分强。

67.2 时间复杂度优化策略

67.2.1 更换更高效的算法与数据结构

  • 排序场景:冒泡排序O(n2)O(n^2) → 快速/归并排序O(nlog⁡n)O(n\log n)
  • 查找场景:顺序查找O(n)O(n) → 二分查找O(log⁡n)O(\log n)(有序前提下)
  • 单源最短路:Bellman-Ford O(nm)O(nm) → 堆优化Dijkstra O(mlog⁡n)O(m\log n)
  • 频繁增删查:数组 → unordered_map/哈希表(平均O(1)O(1))

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];
}

缩小循环范围:判断素数只需循环至n\sqrt{n},不用遍历到nn。

67.2.3 预处理数组

前缀和、前缀最值数组,把区间查询从O(n)O(n)降至O(1)O(1)。

67.2.4 剪枝(搜索类优化)

DFS、回溯中提前舍弃不可能得到最优解的分支,减少搜索范围。

67.3 空间复杂度优化策略

67.3.1 滚动数组(动态规划专用)

仅保存上一层状态,二维DP压缩一维。示例斐波那契空间O(1)O(1):

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 代码层面细节优化

  1. 循环优化:将循环内固定计算提到循环外部;适度循环展开;
  2. 内存访问:数组优先顺序遍历,利用缓存局部性;
  3. 传参优化:大型容器用引用&传递,避免拷贝;简单函数inline;
  4. 位运算替代乘除2、奇偶判断:
    • x >> 1等价x / 2;
    • x & 1判断奇偶。

67.5 经典优化案例

  1. DP优化:01背包二维数组改为一维逆序遍历;
  2. 字符串匹配:暴力O(nm)O(nm) → KMP O(n+m)O(n+m);
  3. 图算法:朴素Dijkstra O(n2)O(n^2) → 小根堆优化O(mlog⁡n)O(m\log n)。

67.6 真题示例(用知识/数据结构优化)

67.6.1 等差数列求和:循环 vs 公式

【GESP 八级风格·编程题】 输入 nn(1≤n≤1091 \le n \le 10^9),输出 1+2+⋯+n1+2+\dots+n 的值。

朴素循环(O(n)O(n),nn 大时超时):

long long sum = 0;
for (long long i = 1; i <= n; ++i) sum += i; // n=10^9 时约 10 亿次,超时

公式优化(O(1)O(1),一步出结果):

long long ans = n * (n + 1) / 2; // 等差数列求和公式
大数乘法先除还是先乘?

n*(n+1) 在 n=109n=10^9 时约 101810^{18},恰好卡在 long long 上界附近,可能溢出。稳妥写法:

long long ans = n % 2 == 0 ? (n / 2) * (n + 1) : n * ((n + 1) / 2);

先除后乘,避免中间乘积溢出。这正体现"用知识优化算法"时也要注意实现细节。

67.6.2 多次区间求和:前缀和

【GESP 八级风格·编程题】 给定长度为 nn 的数组和 qq 次询问,每次问区间 [l,r][l,r] 的和。n,q≤105n,q \le 10^5。

朴素(每次 O(n)O(n),总 O(nq)O(nq) 超时): 每次重新累加。

前缀和优化(预处理 O(n)O(n),每次询问 O(1)O(1)):

#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 表/线段树(最值)把每次询问降到 O(1)O(1) 或 O(log⁡n)O(\log n)。这是八级"算法优化"最高频的考点之一。


练习题

说明

下列选择题考查优化思路、典型优化手段与复杂度变化,参考答案默认折叠,点击"参考答案"展开。

1.(递归优化) 朴素斐波那契递归 fib(n)=fib(n-1)+fib(n-2) 的时间复杂度是?记忆化后变为?

  • A. O(n)O(n),O(2n)O(2^n)
  • B. O(2n)O(2^n),O(n)O(n)
  • C. O(n2)O(n^2),O(n)O(n)
  • D. O(n)O(n),O(n)O(n)
参考答案

B。朴素递归存在大量重复子问题,指数级 O(2n)O(2^n);记忆化后每个状态只算一次,降到 O(n)O(n)。

2.(空间优化) 01 背包用二维 dp[i][j] 可优化为一维数组,关键技巧是?

  • A. 把 j 循环改为正序
  • B. 把 j 循环改为逆序(从大到小)
  • C. 去掉 i 这一维即可,顺序随意
  • D. 用递归替代
参考答案

B。一维背包必须逆序遍历容量,才能保证更新当前物品时用到的是"上一轮(i-1)"的状态,否则会重复使用同一物品(变成完全背包)。

3.(公式优化) 求 1+2+⋯+n1+2+\dots+n,用公式 n(n+1)/2n(n+1)/2 的复杂度是?

  • A. O(n)O(n)
  • B. O(1)O(1)
  • C. O(n2)O(n^2)
  • D. O(log⁡n)O(\log n)
参考答案

B。公式一步计算,与 nn 大小无关,常数时间 O(1)O(1);循环累加才是 O(n)O(n)。

4.(空间换时间) 把"数组里有没有出现过 x"从 O(n)O(n) 查找优化到平均 O(1)O(1),常用?

  • A. 排序后二分
  • B. 哈希表(unordered_map / 数组标记)
  • C. 前缀和
  • D. 并查集
参考答案

B。哈希表平均 O(1)O(1) 查/插,是用"额外空间"换"查询时间"的典型;排序+二分是 O(log⁡n)O(\log n),前缀和与并查集不解决此类查找。

5.(前缀和) 对长度为 nn 的数组做 qq 次区间求和,用前缀和预处理 + 查询的复杂度是?

  • A. 预处理 O(n)O(n),每次 O(1)O(1)
  • B. 预处理 O(n2)O(n^2),每次 O(1)O(1)
  • C. 预处理 O(n)O(n),每次 O(n)O(n)
  • D. 预处理 O(1)O(1),每次 O(n)O(n)
参考答案

A。前缀和数组一遍扫出(O(n)O(n)),区间和用两个前缀值相减(O(1)O(1)),把总复杂度从朴素 O(nq)O(nq) 降到 O(n+q)O(n+q)。

SShowball OI知识手册