GESP C++ 八级考试大纲知识点梳理系列文章:
作为一名优秀的 C++ 程序员,仅仅会写代码让程序跑起来是不够的。如果你的程序在处理大量数据时慢如蜗牛(TLE),或者直接内存溢出(MLE),那么这依然是一个不及格的程序。
算法复杂度分析在 GESP 考纲中反复出现,从四级到八级,要求逐级递进。到了八级,我们需要掌握的是“较为复杂算法”的分析能力。
考纲要求: (7) 算法的时间和空间效率分析。能够掌握 较为复杂算法 的时间和空间复杂度分析方法,能够分析各类算法(包括排序算法、查找算法、树和图的遍历算法、搜索算法、分治及 动态规划算法 等)的时间和空间复杂度。
本人也是边学、边实验、边总结,且对考纲深度和广度的把握属于个人理解。因此本文更多的不是一个教程,而是个人知识梳理,如有遗漏、疏忽,欢迎指正、交流。
这其实已经是大纲中 第三次 明确提出对算法复杂度的要求了。我们先来回顾一下前序等级的要求,以便明确八级的 “进阶” 到底体现在哪里。
重点:初步认识 $O(N)$、$O(N^2)$ 与 $O(2^N)$ 的巨大差异。
重点:引入了 $O(\log N)$ 和 $O(N \log N)$,主要结合 二分查找 和 高效排序(快排、归并)。
对比八级考纲要求可见,八级的核心在 “复杂算法” 和 “空间”:
既然已学过四五级,这里我们只做快速回顾。
在 C++ 竞赛中(通常 1秒时限),一般认为计算机能执行 $10^7 \sim 10^8$ 次基本运算。
| N 的规模 | 复杂度上限 | 对应算法 |
|---|---|---|
| $\le 20$ | $O(2^N), O(N!)$ | 搜索、状压 DP |
| $\le 100 \sim 500$ | $O(N^3)$ | Floyd、区间 DP |
| $\le 2000 \sim 5000$ | $O(N^2)$ | 朴素 DP、最短路(稠密) |
| $\le 10^5 \sim 10^6$ | $O(N \log N)$ | 排序、堆、二分、线段树 |
| $\ge 10^7$ | $O(N)$ | 线性筛、双指针 |
这是八级的重头戏。我们针对考纲提到的几类算法分别解析。
在图论算法中,时间复杂度通常与 点数 $V$ (Vertices) 和 边数 $E$ (Edges) 有关。我们要看算法到底遍历了什么。
核心逻辑:无论用什么存图,算法的步骤都是:
G[N][N] 存图。G[i][j] = 1 表示 $i$ 到 $j$ 有边。if (G[u][v] == 1)。代码体现:
1
2
3
4
// 访问点 u,试图找它的邻居 v
for (int v = 0; v < V; v++) { // 必须循环 V 次!
if (G[u][v] != INF) { ... }
}
vector<int> adj[N] 存图。adj[u] 里只存 $u$ 实际相连的邻居。adj[u] 即可,不需要扫描不存在的边。1
2
3
4
// 访问点 u,试图找它的邻居 v
for (int v : adj[u]) { // 只循环实际边数次
...
}
adj[u] 大小之和等于总边数 (无向图 $2E$,有向图 $E$)。总复杂度 = $V$ (访问所有点) + $E$ (扫描所有边) = $O(V + E)$。推导结论:稀疏图 ($E \ll V^2$) 邻接表完胜;稠密图 ($E \approx V^2$) 两者相当。
Dijkstra 算法是解决单源最短路径的经典算法,它的效率取决于 如何找到当前距离最小的那个点。
dist[N] 记录起点到各点的最短距离(初始化为无穷大,起点为 0)。visited[N] 记录该点的最短路是否已经确定。!visited) 的点中,扫描 dist[] 数组,找到值最小的那个点 $u$,标记它,并用它去更新邻居。{距离, 点} 扔进堆里。这一步需要维持堆序,耗时 $O(\log V)$。priority_queue 无法直接修改堆中间的元素,我们允许堆里存在同一个点的多个“旧数据”。当我们从堆顶取出一个点时,如果发现它的距离比 dist[] 记录的实际最短距离大,说明它是“过期的”,直接扔掉即可。搜索算法(DFS/BFS/回溯)的复杂度通常是指数级的。估算核心模型是 分支系数 $b$ (Branching Factor) 与 搜索深度 $d$ (Depth)。
\[\text{总状态数} \approx O(b^d)\]
fib(n) = fib(n-1) + fib(n-2)。分析分治算法(如归并排序、快速排序)的最佳方法是 递归树法 (Recursion Tree Method)。
核心公式: \(\text{总工作量} = \sum (\text{每一层的节点数} \times \text{每个节点的工作量})\)
核心思想:
复杂度推导:
核心思想:
复杂度推导 (平均情况):
DP 的复杂度分析其实最简单,想象你在 填一张表格。
DP 的复杂度分析其实最简单,想象你在 填一张表格。不管是几维的表格,道理都一样:
\[\text{时间复杂度} = \text{表格总格子数 (状态总数)} \times \text{填好一个格子需要的操作次数 (状态转移代价)}\]
我们套用这个公式来分析几个经典模型:
问题简介:给定两个字符串(如 “ABCDE” 和 “ACE”),求它们最长的公共子序列的长度(这里是 “ACE”,长度 3)。注意子序列可以不连续,但顺序不能乱。
核心策略:
A[i] == B[j],说明这个字符可以作为公共子序列的一部分,长度加 1。A[i] != B[j],说明这两个字符不能同时选,那就在“A 缩短一点”或者“B 缩短一点”的情况里挑个好的。复杂度分析:
dp[i][j] 为 A 串前 $i$ 个和 B 串前 $j$ 个的 LCS。dp[i][j] 时,要么继承 dp[i-1][j],要么继承 dp[i][j-1],要么是 dp[i-1][j-1] + 1。问题简介:给定一个序列(如 [10, 9, 2, 5, 3, 7]),求它的最长上升子序列的长度(这里是 [2, 3, 7],长度 3)。
核心策略:
a[i],我们想知道:如果以它结尾,能组成多长的上升子序列?a[i] 小的前面的数 a[j],取它们对应的 LIS 长度的最大值,再加上 1(即 a[i] 本身)。复杂度分析:
dp[i] 为以第 $i$ 个数结尾的 LIS 长度。dp[i],我们需要回头看 $1 \sim i-1$ 的所有位置 $j$。a[i] > a[j],尝试更新 dp[i] = max(dp[i], dp[j] + 1)。问题简介:给定一个带权有向图(允许负权边,但无负权环),求任意两点之间的最短路径。
核心策略:
dp[k][i][j] 为:只允许经过编号在 $1 \sim k$ 之间的点作为中转站,从点 $i$ 到点 $j$ 的最短路径长度。dp[k-1][i][j]。dp[k-1][i][k] + dp[k-1][k][j]。复杂度分析:
dp[k][i][j] (只允许经过前 $k$ 个点,从 $i$ 到 $j$ 的最短路)。dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j])。八级考纲中特别强调了空间。在高级算法中,MLE (Memory Limit Exceeded) 是非常容易出现的错误。
这部分比较显性,主要看开了多大的数组或容器。
int a[N]:$O(N)$。int dp[N][N]:$O(N^2)$。vector<int> adj[N]:$O(V+E)$。警惕:$10^8$ 个
int大约占用 381 MB。 GESP 或 CSP 常见的内存限制是 128MB 或 256MB(有的题甚至 512MB)。
- 128MB 安全界限:
int数组大小不超过 $3 \times 10^7$。- 256MB 安全界限:
int数组大小不超过 $6 \times 10^7$。如果你需要
int dp[10000][10000],那就是 $10^8$ 个 int,必 MLE。
这部分是隐性的,也是初学者容易“爆栈”的原因。
dp[i] 只依赖于 dp[i-1],可以将 $O(N^2)$ 空间降为 $O(N)$ 甚至 $O(1)$。| 算法类型 | 关键分析点 | 时间复杂度参考 | 空间复杂度参考 |
|---|---|---|---|
| 二分/分治 | 每次规模减半 | $O(\log N)$ / $O(N \log N)$ | $O(1)$ / $O(\log N)$ |
| 排序 | 快排/归并/堆排 | $O(N \log N)$ | $O(\log N)$ / $O(N)$ |
| 图遍历 (DFS/BFS) | 点数 V,边数 E | $O(V+E)$ (邻接表) | $O(V)$ |
| 最短路 (Dijkstra) | 堆优化 | $O(E \log V)$ | $O(V+E)$ |
| 最短路 (Floyd) | 三重循环 | $O(V^3)$ | $O(V^2)$ |
| 树的遍历 | 节点数 N | $O(N)$ | $O(N)$ 或 $O(\log N)$ |
| 动态规划 | 状态数 $\times$ 转移 | 视状态而定 ($N^2, N^3…$) | 视状态而定 (可滚动优化) |
| 全排列 | 阶乘 | $O(N!)$ | $O(N)$ |
备考建议:
通过八级的考核,你应当不仅是一个代码的“翻译工”,更是一个懂得计算代价、懂得权衡资源的“架构师”。
所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code
GESP 学习专题站:GESP WIKI
“luogu-”系列题目可在洛谷题库进行在线评测。
“bcqm-”系列题目可在编程启蒙题库进行在线评测。
欢迎加入:Java、C++、Python技术交流QQ群(982860385),大佬免费带队,有问必答
欢迎加入:C++ GESP/CSP认证学习QQ频道,考试资源总结汇总
欢迎加入:C++ GESP/CSP学习交流QQ群(688906745),考试认证学员交流,互帮互助
GESP C++ 八级考试大纲知识点梳理系列文章:
作为一名优秀的 C++ 程序员,仅仅会写代码让程序跑起来是不够的。如果你的程序在处理大量数据时慢如蜗牛(TLE),或者直接内存溢出(MLE),那么这依然是一个不及格的程序。
算法复杂度分析在 GESP 考纲中反复出现,从四级到八级,要求逐级递进。到了八级,我们需要掌握的是“较为复杂算法”的分析能力。
考纲要求: (7) 算法的时间和空间效率分析。能够掌握 较为复杂算法 的时间和空间复杂度分析方法,能够分析各类算法(包括排序算法、查找算法、树和图的遍历算法、搜索算法、分治及 动态规划算法 等)的时间和空间复杂度。
本人也是边学、边实验、边总结,且对考纲深度和广度的把握属于个人理解。因此本文更多的不是一个教程,而是个人知识梳理,如有遗漏、疏忽,欢迎指正、交流。
这其实已经是大纲中 第三次 明确提出对算法复杂度的要求了。我们先来回顾一下前序等级的要求,以便明确八级的 “进阶” 到底体现在哪里。
重点:初步认识 $O(N)$、$O(N^2)$ 与 $O(2^N)$ 的巨大差异。
重点:引入了 $O(\log N)$ 和 $O(N \log N)$,主要结合 二分查找 和 高效排序(快排、归并)。
对比八级考纲要求可见,八级的核心在 “复杂算法” 和 “空间”:
既然已学过四五级,这里我们只做快速回顾。
在 C++ 竞赛中(通常 1秒时限),一般认为计算机能执行 $10^7 \sim 10^8$ 次基本运算。
| N 的规模 | 复杂度上限 | 对应算法 |
|---|---|---|
| $\le 20$ | $O(2^N), O(N!)$ | 搜索、状压 DP |
| $\le 100 \sim 500$ | $O(N^3)$ | Floyd、区间 DP |
| $\le 2000 \sim 5000$ | $O(N^2)$ | 朴素 DP、最短路(稠密) |
| $\le 10^5 \sim 10^6$ | $O(N \log N)$ | 排序、堆、二分、线段树 |
| $\ge 10^7$ | $O(N)$ | 线性筛、双指针 |
这是八级的重头戏。我们针对考纲提到的几类算法分别解析。
在图论算法中,时间复杂度通常与 点数 $V$ (Vertices) 和 边数 $E$ (Edges) 有关。我们要看算法到底遍历了什么。
核心逻辑:无论用什么存图,算法的步骤都是:
G[N][N] 存图。G[i][j] = 1 表示 $i$ 到 $j$ 有边。if (G[u][v] == 1)。代码体现:
1
2
3
4
// 访问点 u,试图找它的邻居 v
for (int v = 0; v < V; v++) { // 必须循环 V 次!
if (G[u][v] != INF) { ... }
}
vector<int> adj[N] 存图。adj[u] 里只存 $u$ 实际相连的邻居。adj[u] 即可,不需要扫描不存在的边。1
2
3
4
// 访问点 u,试图找它的邻居 v
for (int v : adj[u]) { // 只循环实际边数次
...
}
adj[u] 大小之和等于总边数 (无向图 $2E$,有向图 $E$)。总复杂度 = $V$ (访问所有点) + $E$ (扫描所有边) = $O(V + E)$。推导结论:稀疏图 ($E \ll V^2$) 邻接表完胜;稠密图 ($E \approx V^2$) 两者相当。
Dijkstra 算法是解决单源最短路径的经典算法,它的效率取决于 如何找到当前距离最小的那个点。
dist[N] 记录起点到各点的最短距离(初始化为无穷大,起点为 0)。visited[N] 记录该点的最短路是否已经确定。!visited) 的点中,扫描 dist[] 数组,找到值最小的那个点 $u$,标记它,并用它去更新邻居。{距离, 点} 扔进堆里。这一步需要维持堆序,耗时 $O(\log V)$。priority_queue 无法直接修改堆中间的元素,我们允许堆里存在同一个点的多个“旧数据”。当我们从堆顶取出一个点时,如果发现它的距离比 dist[] 记录的实际最短距离大,说明它是“过期的”,直接扔掉即可。搜索算法(DFS/BFS/回溯)的复杂度通常是指数级的。估算核心模型是 分支系数 $b$ (Branching Factor) 与 搜索深度 $d$ (Depth)。
\[\text{总状态数} \approx O(b^d)\]
fib(n) = fib(n-1) + fib(n-2)。分析分治算法(如归并排序、快速排序)的最佳方法是 递归树法 (Recursion Tree Method)。
核心公式: \(\text{总工作量} = \sum (\text{每一层的节点数} \times \text{每个节点的工作量})\)
核心思想:
复杂度推导:
核心思想:
复杂度推导 (平均情况):
DP 的复杂度分析其实最简单,想象你在 填一张表格。
DP 的复杂度分析其实最简单,想象你在 填一张表格。不管是几维的表格,道理都一样:
\[\text{时间复杂度} = \text{表格总格子数 (状态总数)} \times \text{填好一个格子需要的操作次数 (状态转移代价)}\]
我们套用这个公式来分析几个经典模型:
问题简介:给定两个字符串(如 “ABCDE” 和 “ACE”),求它们最长的公共子序列的长度(这里是 “ACE”,长度 3)。注意子序列可以不连续,但顺序不能乱。
核心策略:
A[i] == B[j],说明这个字符可以作为公共子序列的一部分,长度加 1。A[i] != B[j],说明这两个字符不能同时选,那就在“A 缩短一点”或者“B 缩短一点”的情况里挑个好的。复杂度分析:
dp[i][j] 为 A 串前 $i$ 个和 B 串前 $j$ 个的 LCS。dp[i][j] 时,要么继承 dp[i-1][j],要么继承 dp[i][j-1],要么是 dp[i-1][j-1] + 1。问题简介:给定一个序列(如 [10, 9, 2, 5, 3, 7]),求它的最长上升子序列的长度(这里是 [2, 3, 7],长度 3)。
核心策略:
a[i],我们想知道:如果以它结尾,能组成多长的上升子序列?a[i] 小的前面的数 a[j],取它们对应的 LIS 长度的最大值,再加上 1(即 a[i] 本身)。复杂度分析:
dp[i] 为以第 $i$ 个数结尾的 LIS 长度。dp[i],我们需要回头看 $1 \sim i-1$ 的所有位置 $j$。a[i] > a[j],尝试更新 dp[i] = max(dp[i], dp[j] + 1)。问题简介:给定一个带权有向图(允许负权边,但无负权环),求任意两点之间的最短路径。
核心策略:
dp[k][i][j] 为:只允许经过编号在 $1 \sim k$ 之间的点作为中转站,从点 $i$ 到点 $j$ 的最短路径长度。dp[k-1][i][j]。dp[k-1][i][k] + dp[k-1][k][j]。复杂度分析:
dp[k][i][j] (只允许经过前 $k$ 个点,从 $i$ 到 $j$ 的最短路)。dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j])。八级考纲中特别强调了空间。在高级算法中,MLE (Memory Limit Exceeded) 是非常容易出现的错误。
这部分比较显性,主要看开了多大的数组或容器。
int a[N]:$O(N)$。int dp[N][N]:$O(N^2)$。vector<int> adj[N]:$O(V+E)$。警惕:$10^8$ 个
int大约占用 381 MB。 GESP 或 CSP 常见的内存限制是 128MB 或 256MB(有的题甚至 512MB)。
- 128MB 安全界限:
int数组大小不超过 $3 \times 10^7$。- 256MB 安全界限:
int数组大小不超过 $6 \times 10^7$。如果你需要
int dp[10000][10000],那就是 $10^8$ 个 int,必 MLE。
这部分是隐性的,也是初学者容易“爆栈”的原因。
dp[i] 只依赖于 dp[i-1],可以将 $O(N^2)$ 空间降为 $O(N)$ 甚至 $O(1)$。| 算法类型 | 关键分析点 | 时间复杂度参考 | 空间复杂度参考 |
|---|---|---|---|
| 二分/分治 | 每次规模减半 | $O(\log N)$ / $O(N \log N)$ | $O(1)$ / $O(\log N)$ |
| 排序 | 快排/归并/堆排 | $O(N \log N)$ | $O(\log N)$ / $O(N)$ |
| 图遍历 (DFS/BFS) | 点数 V,边数 E | $O(V+E)$ (邻接表) | $O(V)$ |
| 最短路 (Dijkstra) | 堆优化 | $O(E \log V)$ | $O(V+E)$ |
| 最短路 (Floyd) | 三重循环 | $O(V^3)$ | $O(V^2)$ |
| 树的遍历 | 节点数 N | $O(N)$ | $O(N)$ 或 $O(\log N)$ |
| 动态规划 | 状态数 $\times$ 转移 | 视状态而定 ($N^2, N^3…$) | 视状态而定 (可滚动优化) |
| 全排列 | 阶乘 | $O(N!)$ | $O(N)$ |
备考建议:
通过八级的考核,你应当不仅是一个代码的“翻译工”,更是一个懂得计算代价、懂得权衡资源的“架构师”。
所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code
GESP 学习专题站:GESP WIKI
“luogu-”系列题目可在洛谷题库进行在线评测。
“bcqm-”系列题目可在编程启蒙题库进行在线评测。
欢迎加入:Java、C++、Python技术交流QQ群(982860385),大佬免费带队,有问必答
欢迎加入:C++ GESP/CSP认证学习QQ频道,考试资源总结汇总
欢迎加入:C++ GESP/CSP学习交流QQ群(688906745),考试认证学员交流,互帮互助