Initial commit: C++ courseware

This commit is contained in:
xiaolinquan committed 2026-11-05 22:51:02 +08:00
commit 4a4ee7c287
1789 files changed
+373489

No files matched your search

@@ -0,0 +1,128 @@
# C++ 算法讲义:前缀和
### 第一部分:问题引
**1. 场景模拟:高频区间查询**
假设这样一个数据处理场景:班级里有 100,000 名学生站成一排,已知每个人的考试分数。
此时,班主任频繁提出查询需求:“第 3 名到第 25 名学生的总分是多少?”
最直观的解法是编写一个 `for` 循环,将区间内的分数逐一累加。
但这仅仅是单次查询。如果系统在极短时间内(例如 1 秒内)发起了 10 万次不同区间的查询请求(如“第 10 到 80 名”、“第 2 到 99,999 名”),会发生什么?
**2. 暴力解法的缺陷分析**
如果每次询问都通过 `for` 循环遍历相加,单次查询的时间复杂度为 $O(N)$。如果有 $M$ 次查询,总时间复杂度将高达 $O(N \cdot M)$。
在 $N$ 和 $M$ 均为 100,000 的量级下,程序需要执行约 100 亿次运算,这在任何算法竞赛都会导致超时(TLE)。
除时间复杂度高之外,暴力解法的核心缺陷在于**存在大量的重复计算**。
例如,程序刚刚计算完“第 2 名到第 6 名”的总和,紧接着又要计算“第 2 名到第 5 名”的总和。在暴力解法中,程序会无视之前的计算成果,将第 2 到第 5 名的数据重新遍历累加一遍。
**思考:**
是否存在一种算法结构,能够将计算过的中间结果缓存下来,消除重复遍历?无论查询的区间跨度有多大,都能在常数时间 **$O(1)$** 内直接返回答案?
---
### 第二部分:类比思考
想象你正驾驶汽车行驶在一条笔直的高速公路上。这条公路被划分为了多个**路段**。
1. **真实路段(原数组 $a$)**:每一段路都有其独立的长度。例如第 1 段路长 10 公里,第 2 段路长 20 公里。
2. **累计里程碑(前缀和数组 $S$)**:在每一段路结束的终点处,都设立了一块里程碑,上面记录着“从公路起点行驶至此的总里程”。
- 驶出第 1 段路时,里程碑标记为 10。
- 驶出第 2 段路时,里程碑标记为 30(即 10 + 20)。
**第 $i$ 个里程碑的位置,对应第 $i$ 段路的终点。**
---
### 第三部分:推导公式
* 现在,我们需要计算**“第 3 段路 + 第 4 段路 + 第 5 段路”**的总长度(即求原数组区间 `[3, 5]` 的和)。
依据直觉,许多初学者会尝试直接使用两端的里程碑相减:**用第 5 个碑的读数减去第 3 个碑的读数。**
**直觉陷阱剖析:**
如果你以“第 3 个碑”作为起点进行计算,请注意你当前所处的位置:你在**第 3 段路的终点**!
如果你从这里开始计算距离,你所包含的第一段路将是**第 4 段路**。这样一来,第 3 段路的长度被完全遗漏了。
为了确保计算结果**完整包含**第 3 段路,我们的计算基准点必须设置在第 3 段路的**起点**。
而在累计里程碑系统中,第 3 段路的起点,在物理位置上等同于**第 2 段路的终点**,即**第 2 个里程碑**所在的位置。
因此,计算第 3 到第 5 段路总距离的正确等式为:
**第 5 个里程碑的读数 $-$ 第 2 个里程碑的读数。**
**抽象为通用算法公式:**
若需求取原数组从第 $L$ 个元素到第 $R$ 个元素的总和,设前缀和数组为 $S$:
- 区间的终点标记为:$S[R]$。
- 区间的起点标记(必须是第 $L$ 段开始前的位置)为:$S[L-1]$。
- 区间查询公式:
$$Sum(L, R) = S[R] - S[L-1]$$
---
### 第四部分:前缀和数组 与 如何高效求取?
而我们刚才反复提到的那个用来记录里程碑的数组 𝑆[],其实就叫做:
**—— 前缀和数组 (Prefix Sum Array)**
简单的来说,前缀和数组就是一个用来存储累加和的辅助数组。
那么如何在程序中高效地生成这个 $S$ 数组(即建立所有的里程碑)呢?
在构建第 $i$ 个里程碑时,我们无需从第 1 段路重新累加。只需查阅紧邻的前一个里程碑的数值,再加上当前这一段路的长度即可。
**公式:**
当前前缀和 $=$ 前一个前缀和 $+$ 当前元素的值
$$S[i] = S[i-1] + a[i]$$
---
### 第五部分:C++ 代码
```cpp
long long a[100005]; // 原数组
long long S[100005]; // 前缀和数组(里程碑)
// 第一步:处理前缀和数组
for (int i = 1; i <= n; i++) {
cin >> a[i];
// 递推:当前总和 = 前一个总和 + 当前数字
S[i] = S[i-1] + a[i];
}
// 第二步:查询第 L 个元素到第 R 个元素的区间和
for (int i = 0; i < q; i++) {
int L, R;
cin >> L >> R;
// 终点 - 起点的前一个
cout << S[R] - S[L-1] << endl;
}
```
1. **数组下标从 1 开始!**
* 如果要求区间 `[1, 5]` 的和,套公式是 $S[5] - S[0]$。我们刚好就默认 $S[0] = 0$,非常完美。
* 如果你非要从下标 0 开始存数据(求 `[0, 4]`),公式就会变成 $S[4] - S[-1]$。数组下标变成负数,程序直接越界崩溃!**所以,写前缀和,数组从 1 开始!否则你就要特判下标为 -1 的情况了。**
2. **前缀和数组务必开 `long long`!**
* 原数组 $a$ 的数字可能不大,但十万个数加在一起很有可能超过 `int` 的上限(约 21 亿)。为了不爆数据,前缀和数组请开 `long long`。
Loaded 100 of 1789 files, more files were not shown because too many files have changed in this diff. Show more