🛗

电梯 vs 爬楼梯:同样的事,换条路就快百倍

「算 1 加到 1 亿」——循环一个个加要 1 亿步,高斯公式一步出答案。今天学「同样的事做得更快」:数学公式、前缀和、滚动数组、预处理——四条路把 O(n)、O(n²) 的活压到 O(1)。

💡 打个比方

去 30 楼:爬楼梯要一级一级走 30 步,坐电梯「叮」一声直达——目的地一样,路径不同,快慢差百倍。算法也一样:答案只有一个,做法可以有很多种,选对路径就是优化。

路径一:数学公式。1+2+…+n 一个个加要 n 步;小时候的高斯把首尾配对:(1+n)、(2+n−1)……每对都是 n+1,一共 n/2 对 → n(n+1)/2,一步算完。n = 10^8 时:循环 1 亿步 ≈ 1 秒,公式 1 步 ≈ 0 秒。

路径二:前缀和——提前造一张「累计总和表」:pre[i] = a[1]+…+a[i]。建表花 n 步;之后任意区间 [l, r] 的和 = pre[r] − pre[l−1],1 步出答案。查一次不赚,查 10000 次就把 10000 次 O(n) 变成了 10000 次 O(1)——「预处理换查询」。

路径三:空间优化。算斐波那契 f(n) = f(n−1) + f(n−2),每一步只用前两个值——开 n 格数组是浪费,两个变量滚着算,空间 O(n) → O(1)(七级 C07 的滚动数组)。还有「复用数组」:用完的表接着用,别每件事都开新表;「别开比需要的更大的表」:n 最多 500 就别开 1000 的表。

四条路合起来就是「一般技巧」:减循环层数(O(n²) → O(n) → O(1))、数学公式(求和、快速幂)、预处理换查询(前缀和、查表)、空间换时间(哈希表 O(1) 查 vs 顺序找 O(n))。

术语:等差数列求和公式 n(a₁+aₙ)/2;前缀和 pre[i](前 i 个数的累计和);滚动数组(只保留最近需要的几格,空间 O(n) → O(1));预处理(提前建表,O(n) 一次);空间换时间 / 时间换空间(哈希表查 O(1) vs 顺序找 O(n))。

🖼️ 大图看明白

到 30 楼:两条路 30 楼 电梯:1 声「叮」O(1) 爬楼梯:30 步 O(n) 目的地相同,路径不同快慢差百倍 同一任务:算 1+2+…+10^8 方法 1:循环一个个加 s = 0 for i = 1..n: s = s + i 步数 = n = 10^8 ≈ 1 秒 ✗ 方法 2:高斯公式 s = n * (n + 1) / 2 步数 = 1 ≈ 0 秒 ✓ 10^8 步 vs 1 步——公式快 1 亿倍 同款套路:前缀和 / 滚动数组(见子任务 2、3)

🧩 子任务 1:数学优化——高斯公式把 O(n) 变 O(1)

🎯 小目标:记住等差求和公式 n(a₁+aₙ)/2,会手推,会判断什么时候必须用它。
// ── 手推:1+2+…+100 ──
// 循环:s = 0;  for i = 1..100:  s += i     →  100 步
// 公式:(1 + 100) x 100 / 2 = 5050            →  1 步

// 配对的由来(小时候的高斯):
//   1 + 100 = 101
//   2 +  99 = 101
//   3 +  98 = 101
//   ...
//   50 + 51 = 101        (一共 50 对)
// 总和 = 101 x 50 = 5050
// 通用公式:1+2+…+n = n(n+1)/2
// 更一般:a1+a2+…+an(等差)= n(a1+an)/2

// ── 手推:1+2+…+10^8 ──
// 循环:10^8 步 ≈ 1 秒(擦边,循环里再干点别的就超时)
// 公式:10^8 x (10^8+1) / 2 = 5000050000000000 → 1 步
// 防溢出:10^8 x 10^8 = 10^16,超过 int 上限 2.1x10^9 → long long
✅ 小检查:① 1+2+…+50 = ? ② 10+11+…+20(共 11 项)= ? ③ 算 1+…+10^6,循环和公式各多少步?
看看答案 ① 50 × 51 / 2 = 1275。② 首项 10、末项 20、11 项:(10 + 20) × 11 / 2 = 165。③ 循环 10^6 步 ≈ 0.01 秒(能过);公式 1 步——n = 10^6 时循环其实够用,但公式永远更快;n ≥ 10^8 时循环必须换公式。
易错:「求和用循环就行」——n = 10^8 时循环正好 1 秒擦边,循环体里多一句判断就超时;「公式结果放 int 里」——n(n+1) 到 10^16,int 只到 2.1×10^9,必须 long long。

🧩 子任务 2:前缀和——「累计总和表」,查任意区间 1 步

🎯 小目标:会建 pre 表,会写 pre[r] − pre[l−1],知道什么时候值得建表。
// ── 手推:a = 3, 1, 4, 1, 5(5 个数,下标 1..5)──
// 第 1 步:建「累计总和表」(n 步)
//   pre[1] = 3
//   pre[2] = 3 + 1        = 4
//   pre[3] = 3 + 1 + 4    = 8
//   pre[4] = 3 + 1 + 4 + 1    = 9
//   pre[5] = 3 + 1 + 4 + 1 + 5 = 14

// 第 2 步:答区间和(每次查询 1 步)
//   a[2..4] 的和 = pre[4] - pre[1] = 9 - 3  = 6   (1+4+1 ✓)
//   a[3..5] 的和 = pre[5] - pre[2] = 14 - 4 = 10  (4+1+5 ✓)
//   a[1..5] 的和 = pre[5]          = 14          (整个数组)

// 对比:查上面 3 个区间
//   循环逐个加:3 + 3 + 5 = 11 步
//   前缀和:    建表 5 步 + 3 次查询 x 1 步 = 8 步
// 查询越多越赚:查 10000 个区间,前缀和 = 5 + 10000 步,
// 循环 = 10000 x 平均长度(几十步)= 几十万方
✅ 小检查:① a = 2, 0, 4, 2,a[2..3] 的和 = ? ② 10 万个数建表要多少步?建好后每个区间查询多少步?③ 查「整个数组的和」要几步?
看看答案 ① pre = [2, 2, 6, 8];pre[3] − pre[1] = 6 − 2 = 4(0+4 ✓)。② 建表 10 万步 ≈ 0.001 秒;之后每次查询 1 步。③ pre[n] 直接读,1 步——pre[n] 就是整个数组的和。
易错:「区间 [l, r] 的和 = pre[r] − pre[r−l]」——正确是 pre[r] − pre[l−1]:「到 r 的累计」减「到 l 前一步的累计」;「只查一次,前缀和没用」——查一次确实都 O(n),但表建好后反复用,查询越多越赚。

🧩 子任务 3:空间优化——滚动数组与「别开大表」

🎯 小目标:认出「递推只用最近几格」→ 滚动数组 O(n) → O(1);数组别开比需要的更大。
// ── 手推:斐波那契 f(n) = f(n-1) + f(n-2),求 f(10) ──
// 数组版(开 10 格):f = 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
// 滚动版(只留 3 个变量,滚着算):
//   起步:a = 1, b = 1            (f1, f2)
//   i=3:  c = a+b = 2    →  a, b = b, c   (1, 2)
//   i=4:  c = a+b = 3    →  (2, 3)
//   i=5:  c = 5   →  (3, 5)
//   i=6:  c = 8   →  (5, 8)
//   i=7:  c = 13  →  (8, 13)
//   i=8:  c = 21  →  (13, 21)
//   i=9:  c = 34  →  (21, 34)
//   i=10: c = 55  →  答案 f(10) = 55
// 每一步只用「前两个值」,更早的格子再也不会被用到 → 可以滚掉

// ── 空间账本(1 个 int = 4 B)──
// n = 10^6:数组版 10^6 x 4 B = 4 MB;滚动版 3 个 int = 12 B
// n = 10^8:数组版 400 MB → 爆(128 MB 限制);滚动版还是 12 B

// ── 另外两条空间纪律 ──
// ① 复用数组:用完的表接着干下一件事,别每件事都开新表
// ② 别开比需要的更大的表:n 最多 500,就别开 g[1000][1000]
✅ 小检查:① 按滚动流程,i = 6 那一圈结束时 (a, b) 是多少?② 求「前 n 个数之和」需要滚动数组吗?为什么?③ n = 10^8 的斐波那契,数组版和滚动版各占多少空间?
看看答案 ① i=6 结束 (a, b) = (5, 8)(a 是 f5、b 是 f6)。② 不需要——求和只滚一个累加器 s(s += a[i]),本来就是 O(1) 空间;滚动数组是给「递推依赖最近几格」的题用的。③ 数组版 10^8 × 4 B = 400 MB 爆;滚动版 12 B——同一个答案,空间差 3000 万倍。
易错:「滚动数组万能」——只有递推只用最近几格才能滚(斐波那契滚 2 格、爬楼梯滚 2 格);「01 背包滚动 j 要从大往小」——从前往后会把同一件物品重复装(七级 C07 讲过);「复用数组省时间」——复用省的是空间,不省时间。

🧩 子任务 4:四条优化路——看到慢代码先对一遍

🎯 小目标:拿到一段「能过但慢」的代码,10 秒说出该用哪条路优化。
// ── 四条路速查 ──
// ① 减循环层数:O(n^2) → O(n) → O(1)
//    循环在「求和/求幂/计数」?→ 找数学公式
//    求和 n(a1+an)/2;幂用快速幂 O(log n)(C05)
// ② 预处理换查询:建表一次 O(n),之后每次查询 O(1)
//    前缀和:区间和;查表:n 小时把答案全算好
// ③ 空间换时间:哈希表查询 O(1) vs 顺序找 O(n)
//    visited 数组:标「到过没」,免重复算(DFS/BFS 都在用)
// ④ 省空间:滚动数组 O(n) → O(1);数组别开大

// ── 决策树 ──
// 代码有慢循环?
//   ├─ 在反复答同类查询?→ 前缀和 / 查表(路 ②)
//   ├─ 在逐个查「在不在集合里」?→ 哈希(路 ③)
//   ├─ 在算和/幂?→ 数学公式(路 ①)
//   └─ 数组太大?→ 滚动 / 缩小(路 ④)
✅ 小检查:① 双循环算「所有配对 (a[i], a[j]) 的 a[i]+a[j] 总和」(O(n²)),能优化吗?② 列表 1 万个数,要回答 1 万次「某数在不在列表里」,顺序找 vs 哈希各多少步?③ 一段 O(n) 的代码限时 1 秒、n = 10^5,还需要优化吗?
看看答案 ① 能!设 S = a[1]+…+a[n]:每个 a[i] 作为「左项」出现 n 次、作为「右项」也出现 n 次,总和 = 2nS——先 O(n) 算出 S,一步出答案,O(n²) → O(n)。② 顺序找最坏 10000 × 10000 = 1 亿步 ≈ 1 秒擦边;哈希建表 10000 步 + 每次查询 1 步 = 2 万步,快 5000 倍。③ 不用——10^5 步 ≈ 0.001 秒,绰绰有余;优化要付「代码变复杂」的代价,够用就好。
易错:「越优化越好」——O(n) 能过就别硬上 O(1),代码越绕越容易写错;「哈希一定比顺序找快」——n 很小(几十个)时顺序找代码更简单,建哈希表反而亏;优化的前提是先算账(上一课 10^8 步/秒)。

💻 完整代码:1+2+…+n 三种方法,结果一样,步数差 1 亿倍

// 任务:算 1+2+…+n(n = 10^8),循环 / 公式 / 前缀和 三路对照
#include <iostream>
using namespace std;

int main() {
    long long n = 100000000LL;          // n = 10^8

    // ── 方法 1:循环一个个加(O(n) = 10^8 步 ≈ 1 秒)──
    long long s1 = 0;
    for (long long i = 1; i <= n; i++) s1 += i;
    cout << "循环:和 = " << s1 << "(10^8 步)" << endl;

    // ── 方法 2:等差求和公式(O(1) = 1 步)──
    long long s2 = n * (n + 1) / 2;
    cout << "公式:和 = " << s2 << "(1 步)" << endl;
    cout << "结果一致:" << (s1 == s2 ? "是" : "否") << endl;
    // 注意:n*(n+1) 到 10^16,超 int 上限(约 2.1x10^9)→ 必须 long long

    // ── 方法 3:前缀和(建表 O(n),之后每个区间查询 O(1))──
    // n = 10^8 的数组放不进内存,用 5 个数演示方法
    int a[6];
    a[1] = 3; a[2] = 1; a[3] = 4; a[4] = 1; a[5] = 5;
    int pre[6] = {0};
    for (int i = 1; i <= 5; i++) pre[i] = pre[i - 1] + a[i];
    cout << "前缀和表:";
    for (int i = 1; i <= 5; i++) cout << pre[i] << " ";
    cout << endl;
    // 区间 [2, 4] 的和 = pre[4] - pre[1] = 9 - 3 = 6(1 步)
    cout << "a[2..4] 的和 = " << pre[4] - pre[1] << "(1 步)" << endl;
    return 0;
}
// 运行结果:
// 循环:和 = 5000050000000000(10^8 步)
// 公式:和 = 5000050000000000(1 步)
// 结果一致:是
// 前缀和表:3 4 8 9 14
// a[2..4] 的和 = 6(1 步)

✋ 你来试试

第 1 题:1+2+…+50000000(5000 万)= ? 用公式算。 n = 5×10^7:n(n+1)/2 = 50000000 × 50000001 / 2 = 1250000025000000(1 步出答案;循环要 5000 万步 ≈ 0.05 秒——这里两种都能过,但公式更快)。
第 2 题:a = 2, 5, 3, 8, 1, 7(下标 1..6),a[3..6] 的和 = ? 写 pre 表再答。 pre = [2, 7, 10, 18, 19, 26];a[3..6] = pre[6] − pre[2] = 26 − 7 = 19(验算:3+8+1+7 = 19 ✓)。
第 3 题:用滚动数组(3 个变量)算斐波那契 f(20)(f1 = f2 = 1)。 滚着算:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765——f(20) = 6765。全程只用 3 个变量,空间 12 B;开 20 格数组要 80 B,开 10 万格要 400 KB,n = 10^8 直接 400 MB 爆。
第 4 题(加餐):一段代码有双循环 O(n²),n = 10^5,限时 1 秒——会 TLE 吗?你按决策树怎么优化? 10^10 = 100 亿步 ≈ 100 秒 → TLE。按决策树:先问双循环在干什么——在「求和/计数」→ 找数学公式(子任务 4 第 ① 题:2nS,O(n));在「反复答同类区间查询」→ 前缀和(子任务 2);在「逐个查在不在集合里」→ 哈希(O(1) 查)。先算账、再选路,别盲目改。
🎯 一句话记住:算法优化 = 同样的事换条路——求和:循环 O(n) → 高斯公式 O(1);区间和:逐个加 O(n) → 前缀和 pre[r] − pre[l−1] 一步;空间:数组 O(n) → 滚动数组 O(1);四条路 = 减循环层数 / 数学公式 / 预处理换查询 / 空间换时间。先算账(10^8 步/秒)再选路,够用就别硬优化。