电子学会 🐍 Python 🔧 C++ GESP 🐍 Python 🔧 C++
C/C++ 等级考试 / 知识点手册

青少年软件编程(C/C++)等级考试 · 知识点手册

依据《青少年软件编程(C/C++)等级考试标准(2025 年修订版)》整理。全册共十大等级,从 C++ 编程基础入门到竞赛级专题逐级递进。每个知识点均配有详细讲解、C++ 代码示例、易错点提示,并配模拟题讲解;难懂处附适合小学生、初中生的生活化例子。

共 10 个等级上机作答以实践应用能力为主附各等级模拟题

考纲总览

等级递进 · 能力要求 · 试题结构说明

一、等级逻辑关系总览

《青少年软件编程(C/C++)等级考试标准(2025 年修订版)》将 C++ 由低到高分为一级至十级,适用年龄 8 周岁(建议 10 周岁)以上。考试由考试系统集成的编程环境完成作答,以实践应用能力为主。

十级内容呈现明显的螺旋递进关系:基础语法 → 函数与算法 → 数据结构与动态规划 → 图论与数论 → 竞赛级专题。

1
一级 · 编程入门
变量与基本运算、Turtle 等价物=cout/cin
2
二级 · 进阶语法
文件操作、数组
3
三级 · 函数与算法起步
函数、递归、模拟、枚举
4
四级 · 指针与排序
指针、结构体、排序、位运算
5
五级 · 算法进阶
快速幂、贪心、二分、STL
6
六级 · 数据结构与动态规划起步
搜索、背包、欧几里得、排列组合
7
七级 · 树图与算法深化
树的遍历、最短路、最小生成树
8
八级 · 高级数据结构
并查集、线段树、KMP
9
九级 · 图论与数论进阶
强连通、矩阵、容斥
10
十级 · 高级专题
平衡树、CRT、线性基

二、各级别核心能力要求总览

[['1', '一级 · 编程入门', '变量与基本运算、Turtle 等价物=cout/cin', '顺序、分支、循环结构程序'], ['2', '二级 · 进阶语法', '文件操作、数组', '多层结构与数组的综合程序'], ['3', '三级 · 函数与算法起步', '函数、递归、模拟、枚举', '函数封装与简单算法'], ['4', '四级 · 指针与排序', '指针、结构体、排序、位运算', '指针与多类排序算法'], ['5', '五级 · 算法进阶', '快速幂、贪心、二分、STL', '常用高效算法'], ['6', '六级 · 数据结构与动态规划起步', '搜索、背包、欧几里得、排列组合', '数据结构与简单 DP'], ['7', '七级 · 树图与算法深化', '树的遍历、最短路、最小生成树', '图论与树形算法'], ['8', '八级 · 高级数据结构', '并查集、线段树、KMP', '高级数据结构的应用'], ['9', '九级 · 图论与数论进阶', '强连通、矩阵、容斥', '图论与数论的深入应用'], ['10', '十级 · 高级专题', '平衡树、CRT、线性基', '竞赛级专题']]
说明:C++ 考纲以能力要求(a/b/c…)形式给出,未提供分值比例与样题;本手册按能力要求整理知识点卡片,并以贴合考点的模拟题代替真题进行讲解。

三、使用说明与备考建议总览

  • 本手册按考纲 10 级顺序,将知识点拆分为卡片,左侧导航可快速跳转;
  • 每个知识点包含概念讲解 → C++ 代码示例 → 易错点 → 模拟题的完整闭环,难懂处配“生活小例子”;
  • 代码块右上角有“复制”按钮,例题答案可点击“查看答案”展开;
  • 顶部可一键切换到 Python 版手册(同一考纲体系下的另一语言)。
1

一级 · 编程入门

开发环境 · 变量 · 输入输出 · 顺序/分支/循环
能力目标:顺序、分支、循环结构程序核心:变量与基本运算、Turtle 等价物=cout/cin

1.1 开发环境与程序框架一级

① C++ 开发环境

C++ 是一门需要先编译、再运行的编程语言。常见开发环境有 Dev-C++Code::Blocks 等。写好的代码要先“翻译”成电脑能懂的机器语言(这一步叫编译),翻译成功才能运行。

② 程序的基本框架

每个 C++ 程序几乎都有固定的“骨架”。下面的代码会在屏幕上输出 Hello World:

C++
#include <iostream>     // 头文件:引入输入输出的功能
using namespace std;      // 使用标准命名空间
int main(){               // 主函数:程序从这里开始执行
    cout << "Hello World" << endl;   // 输出并换行
    return 0;             // 告诉系统程序正常结束
}
  • #include <iostream>:引入“输入/输出”功能(就像打开工具箱);
  • using namespace std;:声明使用标准命名空间(cin、cout 都在里面);
  • int main():主函数,程序执行的起点;
  • return 0;:正常结束程序。
易错点:① 每句语句末尾要有分号 ;(漏掉会报错);② 主函数名是 main,不能写成 mian;③ 头文件用 #include不加分号

1.2 变量与数据类型一级

① 变量的概念

变量就是给数据起一个名字、在内存里占一块地方用来存值。C++ 是强类型语言——使用变量前必须先声明类型

② 常见数据类型

类型占位含义示例
int
4
int age = 10;
C++
int age = 10;            // 声明并赋值
age = 11;                // 修改变量的值
double score = 95.5;
char grade = 'A';
bool pass = true;
cout << age << " " << score << endl;   // 输出 11 95.5
生活小例子:不同容量的杯子

变量想成不同容量的杯子

  • int 是"整杯水"(只能装整数,装不下 3.14);
  • double 是大水杯(能装小数,更精确);
  • char 是小茶杯(只装一个字符);
  • 声明类型就是先挑好杯子,再往里倒水——所以 C++ 用变量前必须说清类型!
易错点:① 变量未初始化就使用,值是随机的“垃圾值”;② int 装不下小数,int x = 3.9; 会截断成 3;③ 变量名不能以数字开头,不能用 intmain 等保留字。

1.3 输入输出语句一级

① cout 输出

cout << 内容 用于输出,<< 像“把数据推向屏幕”,endl 表示换行。

② cin 输入

cin >> 变量 用于从键盘读取数据存入变量,>> 像“把输入推进变量”。

C++
#include <iostream>
using namespace std;
int main(){
    int a, b;
    cin >> a >> b;          // 输入两个数(空格或回车隔开)
    cout << a + b << endl;  // 输出它们的和
    cout << "a=" << a << " b=" << b << endl;
    return 0;
}
生活小例子:问路和喊话

cincout 想成两个人:

  • cin >> a有人问路,你告诉他把信息“塞”给变量 a(箭头指向变量,信息流进去);
  • cout << a你对着喇叭喊话,把 a 里存的值“喊”到屏幕上(箭头指向屏幕,信息流出去);
  • 口诀:cin 箭头朝变量(进),cout 箭头朝屏幕(出),方向别写反!
易错点:>><< 方向写反会报错;② cin 读字符串时遇空格就停(想读整行要用 getline);③ 末尾记得 return 0;

1.4 基本运算与数学函数一级

① 算术运算符

运算符含义示例结果
+
7 + 3
10
最大的坑:两个 int 相除结果是整除5 / 2 = 2,想要 2.5 必须先转成 double:(double)5 / 2

② 数学函数(需 #include <cmath>)

C++
#include <iostream>
#include <cmath>
using namespace std;
int main(){
    cout << sqrt(16) << endl;    // 平方根 → 4
    cout << pow(2, 10) << endl;  // 2 的 10 次方 → 1024
    cout << abs(-7) << endl;     // 绝对值 → 7
    cout << ceil(2.1) << endl;   // 向上取整 → 3
    cout << floor(2.9) << endl;  // 向下取整 → 2
    return 0;
}
生活小例子:分糖果

/% 想成分糖果:7 颗糖分给 2 个小朋友:

  • 7 / 2 = 3:每人先分到 3 颗(整除=每人几颗);
  • 7 % 2 = 1:还剩 1 颗(取余=剩下几颗);
  • 所以判断一个数是奇数还是偶数,就看它 % 2 是 1 还是 0!
易错点:% 只能用于整数;② 5 / 2 结果是 2 不是 2.5(整数除法陷阱);③ pow 返回 double,结果可能有细微误差。

1.5 顺序结构一级

① 什么是顺序结构

程序按从上到下、一句一句执行的顺序,就是顺序结构——像做菜按菜谱一步一步来。

② 综合例子

C++
#include <iostream>
using namespace std;
int main(){
    int a, b;
    cout << "请输入两个整数:";
    cin >> a >> b;
    int sum = a + b;         // 处理:求和
    cout << "和是:" << sum << endl;   // 输出
    return 0;
}
三步法:输入(cin)→ 处理(计算)→ 输出(cout),是顺序结构程序的基本套路。

1.6 逻辑运算与选择结构一级

① 关系与逻辑运算符

类别符号含义
< > <= >= == !=
/////

② if-else 选择结构

C++
int score = 85;
if (score >= 60) {
    cout << "及格" << endl;
} else {
    cout << "不及格" << endl;
}
// 三目运算符:条件 ? 结果1 : 结果2
cout << (score >= 60 ? "pass" : "fail") << endl;

③ switch 多分支

C++
int day = 3;
switch (day) {
    case 1: cout << "星期一"; break;
    case 2: cout << "星期二"; break;
    case 3: cout << "星期三"; break;
    default: cout << "其他"; break;
}
生活小例子:游乐场的三个门卫

把逻辑运算想成游乐场的门卫

  • &&(并且)= 要同时满足才放行:门票 50 元 并且 身高 1.2 米以上才能玩;
  • ||(或者)= 满足一个就行:带学生证 或者 今天生日都能半价;
  • !(非)= 反着说!true 就是 false,“不是周末”就是工作日。
易错点:== 是“判断相等”,= 是“赋值”,写混是超高频错误!② if 后面不要加分号;③ 判断条件要加圆括号

1.7 循环结构一级

① for 循环(知道次数)

C++
// 从 1 加到 100
int sum = 0;
for (int i = 1; i <= 100; i++) {
    sum += i;
}
cout << sum << endl;   // 5050

② while 循环(看条件)

C++
int n = 12345, cnt = 0;
while (n > 0) {         // 不断取个位
    cnt++;
    n /= 10;
}
cout << "位数:" << cnt << endl;   // 5

③ do-while(至少执行一次)

C++
int x;
do {
    cout << "请输入一个正数:";
    cin >> x;
} while (x <= 0);      // 直到输入正数才停
生活小例子:体育课跑圈

循环想成体育课跑圈

  • for 像老师规定“跑 5 圈”——次数已知,用 for;
  • while 像“一直跑,直到下课铃响”——看条件;
  • do-while 像“先跑一圈再说”——不管怎样至少跑一圈。

口诀:知道几圈用 for,看情况用 while,先跑再判用 do-while。

易错点:① 循环变量忘记更新(i++)会死循环;② 注意边界:i <= 100i < 100 差一次;③ for 的变量 i 只在循环内有效。

一级模拟题一级

以下例题贴合一级考纲(环境、变量、运算、分支、循环)设计:

选择题以下哪个是正确的 C++ 程序开头?
A. #include <iostream>B. include iostreamC. #include iostream;D. #include(stdio)
答案:A
B 少了 # 号;C 头文件不能加分号;D 写法错误。正确写法是 #include <iostream>
选择题已知 int a = 7, b = 2;a / b 的值是?
A. 3.5B. 3C. 2D. 1
答案:B
两个 int 相除是整除,7 / 2 = 3(余 1 被丢弃)。想要 3.5 需先转 double。
选择题以下循环会输出几个“你好”?
for (int i = 0; i < 5; i++) cout << "你好";
A. 4 次B. 5 次C. 6 次D. 无限次
答案:B
i 从 0 到 4 共 5 次(i=0,1,2,3,4),i=5 时不满足 i<5 退出。
编程题输入一个整数 n,输出 1 到 n 的和。
答案:参考程序见解析
int main(){ int n, sum = 0; cin >> n; for (int i = 1; i <= n; i++) sum += i; cout << sum; }
用 for 从 1 累加到 n,注意初始化 sum = 0。
编程题输入一个整数,判断它是奇数还是偶数。
答案:参考程序见解析
if (n % 2 == 0) cout << "偶数"; else cout << "奇数";
n % 2:余 0 是偶数,余 1 是奇数。
2

二级 · 进阶语法

文件读写 · 类型转换 · 多层分支/循环 · 数组
能力目标:多层结构与数组的综合程序核心:文件操作、数组

2.1 文件读写二级

① 文件重定向 freopen

竞赛/考试中常用 freopen 把输入输出重定向到文件:程序从 in.txt 读,结果写到 out.txt。

C++
#include <cstdio>
int main(){
    freopen("in.txt", "r", stdin);    // 输入从 in.txt 读
    freopen("out.txt", "w", stdout);  // 输出写到 out.txt
    int a, b;
    scanf("%d %d", &a, &b);           // 从文件读入
    printf("%d\n", a + b);           // 写入文件
    fclose(stdin); fclose(stdout);
    return 0;
}

② 用 fstream 读写文件

C++
#include <fstream>
using namespace std;
int main(){
    ifstream fin("in.txt");       // 读
    ofstream fout("out.txt");     // 写
    int a, b;
    fin >> a >> b;
    fout << a + b << endl;
    fin.close(); fout.close();
    return 0;
}
易错点:① 文件名要与真实文件完全一致(大小写、后缀);② 用完后记得 close();③ freopen 后 cin/cout 和 scanf/printf 混用可能出错

2.2 变量类型转换二级

① 隐式转换与强制转换

不同类型参与运算时会自动转换(如 int 转 double),也可以强制转换(类型)表达式

C++
int a = 5, b = 2;
double x = (double)a / b;      // 2.5(先转再除)
int y = (int)3.99;             // 3(小数被截断)
double z = a / b;              // 2.0!a/b 先整除成 2 再转 double
char c = 'A';
int ascii = (int)c;            // 65(字符转 ASCII 码)
生活小例子:翻译官换称呼

类型转换想成翻译官

  • (double)a / b 像先让 a“翻译成小数”,再去做除法 → 2.5;
  • a / b 没翻译,两个整数先整除了 → 2,之后想变小数已经来不及;
  • 口诀:要先转换,再运算;先运算就来不及了!
易错点:(int)3.99截断不是四舍五入;② 强制转换 float→int 可能丢失精度;③ 字符和整数可互转(ASCII 码)。

2.3 多层分支结构二级

① 嵌套 if 与 else if

条件里再套条件叫嵌套;用 else if 可以写多档判断。

C++
int score = 85;
if (score >= 90)      cout << "优秀";
else if (score >= 80) cout << "良好";
else if (score >= 60) cout << "及格";
else                  cout << "不及格";

② 分支里套分支

C++
int a, b;  cin >> a >> b;
if (a == b) cout << "相等";
else {
    if (a > b) cout << "a 大";
    else       cout << "b 大";
}
易错点:else 总是和最近的 if 配对;② 多档判断用 else if 优于连续 if(一旦命中就跳过后面)。

2.4 多层循环结构二级

① 嵌套循环

循环里面再套循环,外层循环每走一步,内层循环完整走一遍。

C++
// 打印直角三角形
for (int i = 1; i <= 5; i++) {
    for (int j = 1; j <= i; j++) {
        cout << "*";
    }
    cout << endl;
}
/* 输出:
*
**
***
****
***** */
C++
// 九九乘法表
for (int i = 1; i <= 9; i++) {
    for (int j = 1; j <= i; j++) {
        cout << j << "*" << i << "=" << j*i << "\t";
    }
    cout << endl;
}
生活小例子:值日表排座位

嵌套循环想成排值日表:外层是“星期几”,内层是“第几节课”,星期一要把每节课都排一遍,星期二再排一遍……外层转一次,内层转一圈

所以“打印 5 行 × 每行 5 个星号”就是外层管行、内层管列。

易错点:① 内层循环要记得每行末尾换行;② 循环变量别重名(外层 i 内层 j);③ 嵌套太深注意总执行次数 = 外层×内层。

2.5 数组二级

① 一维数组

数组是一排同类型的变量,用下标访问,下标从 0 开始。

C++
int a[5];               // 声明 5 个整数的数组
a[0] = 10; a[1] = 20;     // 赋值
for (int i = 0; i < 5; i++) cin >> a[i];   // 读入
// 求最大值
int mx = a[0];
for (int i = 1; i < 5; i++)
    if (a[i] > mx) mx = a[i];
cout << mx << endl;

② 二维数组(表格)

C++
int g[3][4];            // 3 行 4 列的“表格”
// 双层循环读入 3×4 个数
for (int i = 0; i < 3; i++)
    for (int j = 0; j < 4; j++)
        cin >> g[i][j];
生活小例子:一排带编号的储物柜

数组想成一排带编号的储物柜

  • int a[5] 是 5 个柜子,编号从 0 号4 号
  • a[2] = 30 就是往 2 号柜放东西;
  • 关键:编号从 0 开始!第 1 个柜子是 a[0],不是 a[1];
  • 二维数组就像教室里 3 排 × 4 列的课桌,用“第几排第几列”定位。
易错点:① 下标越界(访问 a[5])不会立刻报错,但会读到垃圾值或导致崩溃;② 数组声明大小必须是常数;③ 未初始化就访问元素,值是随机的。

二级模拟题二级

选择题声明 int a[10]; 后,下面哪个下标是合法的?
A. a[10]B. a[-1]C. a[9]D. a[10.5]
答案:C
数组下标从 0 到 9(共 10 个),a[9] 是最后一个。a[10] 越界,下标必须是整数。
选择题以下嵌套循环共输出多少个 *
for(i=0;i<3;i++) for(j=0;j<4;j++) cout<<"*";
A. 3 个B. 4 个C. 7 个D. 12 个
答案:D
外层 3 次 × 内层 4 次 = 12 次。外层每走一次,内层完整走 4 次。
编程题输入 n 个整数,把这 n 个数倒序输出。
答案:参考程序见解析
int a[100]; int n; cin >> n; for(i=0;i<n;i++) cin >> a[i]; for(i=n-1;i>=0;i--) cout << a[i] << " ";
用数组存下来,再从最后一个下标 n-1 倒着输出到 0。
编程题用二维数组存储 3 个同学 4 门课的成绩,输出每个同学的总分。
答案:参考思路见解析
用 g[i][j] 存第 i 个同学第 j 门课成绩,外层循环 i 对每个同学,内层循环 j 累加 g[i][j]。
二维数组常用“外层管行、内层管列”的双层循环。
3

三级 · 函数与算法起步

函数 · 参数传递 · 递归 · 字符数组 · 模拟法 · 枚举法
能力目标:函数封装与简单算法核心:函数、递归、模拟、枚举

3.1 函数定义与调用三级

① 函数的概念

函数是一段可以反复调用的代码块,由“返回类型 + 函数名 + 参数 + 函数体”组成。

C++
// 返回两数中较大者
int max2(int a, int b) {
    if (a > b) return a;
    return b;
}
int main(){
    cout << max2(3, 7) << endl;   // 7(调用)
    cout << max2(10, 4) << endl;  // 10
    return 0;
}
生活小例子:妈妈的拿手菜谱

函数想成妈妈的拿手菜谱

  • “做番茄炒蛋”这个菜谱被写好后,客人来了随时照着做一遍,不用每次重新发明;
  • 参数 = 每次做菜放的配料量(放几个番茄、几个蛋);
  • return = 做好后端上桌的那盘菜(把结果交给调用的人);
  • 好处:一次写好,反复使用,还不用看内部怎么做。
易错点:① 有返回值要用 returnvoid 函数不需要 return;② 函数必须先声明/定义再调用;③ return 后面的语句不会执行。

3.2 函数参数传递三级

① 值传递(复制一份)

默认是值传递:把变量的值复制一份传给函数,函数里改参数不影响原变量

② 引用传递(&,共享一份)

&引用传递:把变量本身传给函数,函数里改它,原变量也变

C++
void swap1(int a, int b){   // 值传递:改的是副本
    int t = a; a = b; b = t;
}
void swap2(int &a, int &b){  // 引用传递:改的是原变量
    int t = a; a = b; b = t;
}
int main(){
    int x = 3, y = 5;
    swap1(x, y);  cout << x << y;   // 35(没交换!)
    swap2(x, y);  cout << x << y;   // 53(成功交换)
}
生活小例子:复印和共享

把参数传递想成两种方式:

  • 值传递 = 把试卷复印一份给同学写,同学改自己的复印件,你的原卷不变
  • 引用传递(加 &)= 大家共用同一张试卷,谁改了大家都看到;
  • 想“换内容”(交换、修改),就要用引用传递,不然白改!
易错点:经典陷阱:swap1(x, y) 用值传递交换不了,因为只换了副本。数组作参数时自动按“引用”效果传递(能改原数组)。

3.3 递归三级

① 递归的概念

递归就是函数调用自己。必须有递归出口(停止条件),否则会无限递归导致崩溃。

C++
int fact(int n){           // n 的阶乘
    if (n <= 1) return 1;   // 出口
    return n * fact(n - 1); // 调用自己
}
// 斐波那契:1 1 2 3 5 8 ...
int fib(int n){
    if (n <= 2) return 1;
    return fib(n - 1) + fib(n - 2);
}
生活小例子:俄罗斯套娃排队报数

递归想成俄罗斯套娃:打开大娃里面有中娃,打开中娃有小娃……直到最小的打不开(这就是递归出口!)。

或者想象最后一排同学问自己是第几个:一直往前问,问到第一排说“我是第 1 个”,再一个个传回来。先一路问下去,到出口,再一路算回来,就是递归。

注意:没有出口会一直问下去 = 死循环,程序会崩溃!

易错点:① 递归必须有出口;② 每调用一次自己都要占内存,递归太深可能栈溢出;③ 很多递归问题可以用循环(递推)更高效地改写。

3.4 字符数组与字符串函数三级

① 字符数组

C++ 里字符串可以用 char 数组存,末尾自动带 \0 表示结束。

C++
char s[20] = "hello";      // 实际占 6 个位置(含 \0)
strlen(s);                  // 长度 5
strcmp(s, "hello");         // 0(相等)
strcpy(s, "world");         // 把 s 改成 world

② string 类型(推荐)

C++
#include <string>
string s = "abc";
s.length();                 // 3
s += "d";                   // "abcd"
s.substr(1, 2);             // "bc"(从下标 1 取 2 个)
s.find("bc");               // 返回位置 1
易错点:① 字符数组要留足空间(含 \0);② strcmp 相等返回 0,不是 1;③ string 用 + 拼接、用 == 比较更方便。

3.5 数学库函数三级

① cmath 常用函数

C++
#include <cmath>
abs(-3)      // 3    整数绝对值
fabs(-3.5)   // 3.5  小数绝对值
sqrt(25)     // 5    平方根
pow(2, 3)    // 8    乘方
log(1)       // 0    自然对数
round(3.6)   // 4    四舍五入
ceil(2.1)    // 3    向上取整
floor(2.9)   // 2    向下取整
易错点:abs 是整数、fabs 是小数,别用混;② pow 返回 double,算整数幂可能不精确。

3.6 模拟法三级

① 模拟法的概念

模拟法就是“照着题意一步一步做”,把题目描述的过程用代码原样模拟一遍。

C++
// 模拟:小智每天存 1 元、第 2 天存 2 元……问 n 天后共存多少
int n = 10, total = 0;
for (int day = 1; day <= n; day++)
    total += day;          // 每天按规则加
cout << total << endl;     // 55
生活小例子:按说明书做科学小实验

模拟法想成照着说明书做实验:说明书写“第一步加 1 滴,第二步加 2 滴,第三步……”,你就一步一步照做,绝不跳步。程序也一样——题目说怎么做,代码就怎么写。

关键是别自作聪明,先把过程老老实实模拟出来再说。

易错点:模拟法要注意循环次数和边界,模拟错了就整题错。先把题意读懂,用纸笔推一遍再写代码。

3.7 枚举法三级

① 枚举法的概念

枚举法就是把所有可能的情况一个一个试,找出满足条件的。

C++
// 水仙花数:三位数,各位立方和等于它本身
for (int n = 100; n <= 999; n++) {
    int a = n / 100, b = n / 10 % 10, c = n % 10;
    if (a*a*a + b*b*b + c*c*c == n)
        cout << n << " ";   // 153 370 371 407
}
生活小例子:翻遍抽屉找钥匙

枚举想成钥匙不见了,挨个抽屉翻

  • 从第一个抽屉翻到最后一个,不重不漏
  • 翻到钥匙就“验证通过”;
  • 循环就是“挨个抽屉”,if 就是“看看是不是钥匙”。

枚举的两个要求:范围不漏判断条件正确

易错点:① 枚举范围要完整(漏了范围就漏答案);② 判断条件写错会得到错误答案;③ 数据很大时枚举太慢,要换算法。

三级模拟题三级

选择题以下函数调用后,x 的值是?
void f(int a){ a = 99; } int x = 5; f(x);
A. 99B. 5C. 不确定D. 报错
答案:B
这是值传递,函数里改的是副本,原变量 x 仍是 5。想改原变量要加 &
选择题fib(6) 的值是?(fib 为斐波那契,fib(1)=fib(2)=1)
A. 5B. 8C. 13D. 6
答案:B
斐波那契:1,1,2,3,5,8 → fib(6)=8。
编程题用枚举法找出 100~999 中所有“回文数”(如 121、242)。
答案:参考程序见解析
for(n=100;n<=999;n++){ int a=n/100, c=n%10; if(a==c) cout<<n<<" "; }
三位数回文:百位 == 个位。用 /100 取百位、%10 取个位。
编程题用递归求 1+2+...+n。
答案:参考程序见解析
int sum(int n){ if(n<=1) return n; return n + sum(n-1); }
递归出口:n<=1 时返回 n;否则返回 n 加上 sum(n-1)。
4

四级 · 指针与排序

指针 · 结构体/类 · 进制 · 位运算 · 排序 · 高精度
能力目标:指针与多类排序算法核心:指针、结构体、排序、位运算

4.1 指针四级

① 指针的概念

指针就是存“地址”的变量。&变量 取地址,*指针 通过地址访问它指向的变量。

C++
int a = 10;
int *p = &a;        // p 存的是 a 的地址
cout << p << endl;  // 输出一个地址(十六进制)
cout << *p << endl; // 10(*p 通过地址取到 a 的值)
*p = 99;            // 修改 a 的值!
cout << a << endl;  // 99
生活小例子:门牌号和屋里的人

指针想成地址门牌号

  • &a 是“小明家的门牌号”;
  • 指针 p = 门牌号(记下小明住哪);
  • *p 是“敲开门,见到屋里的小明”——通过门牌号找到真正的人;
  • 所以 *p = 99 是“进屋把小明改成 99”,a 当然就变了。

指针的威力:函数可以透过指针修改外面的变量(前面 swap 也可以用指针实现)。

易错点:① 指针必须先初始化再使用,否则是“野指针”很危险;② & 取地址、* 解引用,别混淆;③ 空指针 nullptr 不能解引用。

4.2 结构体与类四级

① 结构体 struct

结构体把多个不同类型的数据打包成一种新类型,方便管理。

C++
struct Student {
    string name;
    int score;
};
int main(){
    Student s;
    s.name = "小明";
    s.score = 95;
    cout << s.name << " " << s.score << endl;
    Student a[3];            // 结构体数组
    return 0;
}

② 类 class

在结构体基础上,还能包含函数(方法)和访问权限(public/private)。

C++
class Counter {
private:
    int value = 0;
public:
    void add(){ value++; }
    int get(){ return value; }
};
int main(){
    Counter c;
    c.add(); c.add();
    cout << c.get() << endl;   // 2
    return 0;
}
生活小例子:学生档案袋

结构体想成一个学生档案袋:里面可以同时放姓名卡、成绩单、班级卡……一个结构体就是“一个人”的所有信息打包在一起。

更进一步,档案袋还能自带“盖章、整理”的功能(方法),并且有些内容不让人随便看(private 私有)。

易错点:① 结构体变量用 . 访问成员;结构体指针用 ->;② 类的成员默认 private(私有),结构体默认 public(公有);③ 用 struct Student 后,类型名是 Student

4.3 进制转换四级

① 常见进制

进制基数数字例子
2
0 1
1010 = 10

② 十进制转二进制(除 2 取余)

C++
int n = 13;
string bin = "";
while (n > 0) {
    bin = char('0' + n % 2) + bin;   // 余数往前拼
    n /= 2;
}
cout << bin << endl;   // 1101

③ C++ 内置进制

C++
cout << hex << 16 << endl;   // 10(十六进制)
cout << oct << 8 << endl;    // 10(八进制)
cout << dec << 10 << endl;   // 10(十进制)
生活小例子:4 盏灯的开关

二进制想成4 盏灯(亮=1,灭=0),灯的“分量”从左到右是 8、4、2、1:

  • 想表示 13:亮“8+4+1”三盏 → 1101;
  • 想表示 10:亮“8+2”两盏 → 1010;
  • 电脑里一切数据,本质都是这些灯的亮灭(0 和 1)!
易错点:① 除 2 取余要把余数倒着排;② 十六进制 A~F 对应 10~15;③ 二进制转十进制:各位 × 2 的次方相加。

4.4 位运算四级

① 位运算符

符号含义例子结果
&
5 & 3 (101&011)
1

② 常用技巧

C++
// 判断奇偶:看最低位
if (n & 1) cout << "奇数"; else cout << "偶数";
// 交换两个数(不用临时变量)
a ^= b; b ^= a; a ^= b;
// 乘 2 / 除 2(比乘除快)
x = x << 1;   // x * 2
x = x >> 1;   // x / 2
生活小例子:开关的组合拳

位运算想成一排开关(1=开 0=关):

  • &(与)= 两个都开才开;
  • |(或)= 有一个开就开;
  • ^(异或)= 一开一关才开(不一样才亮);
  • <<(左移)= 整排开关往左挪一位,数值就 ×2

位运算飞快,是高手常用的“加速术”。

易错点:① 位运算优先级低于比较运算,要加括号:(n & 1);② 负数用补码存储,取反结果要理解;③ 左移乘 2 只对整数有效。

4.5 排序算法(桶/冒泡/选择/插入)四级

① 桶排序(最快,但有范围限制)

C++
int a[5] = {3, 1, 4, 1, 5};
int bucket[10] = {0};
for (int i = 0; i < 5; i++) bucket[a[i]]++;
for (int v = 0; v < 10; v++)
    while (bucket[v]--) cout << v << " ";

② 冒泡排序(相邻比较)

C++
for (int i = 0; i < n - 1; i++)      // 每轮把一个最大值“沉底”
    for (int j = 0; j < n - 1 - i; j++)
        if (a[j] > a[j + 1])
            swap(a[j], a[j + 1]);

③ 选择排序(每轮挑最小)

C++
for (int i = 0; i < n - 1; i++){
    int mn = i;
    for (int j = i + 1; j < n; j++)
        if (a[j] < a[mn]) mn = j;
    swap(a[i], a[mn]);
}

④ 插入排序(像理扑克牌)

C++
for (int i = 1; i < n; i++){
    int key = a[i], j = i - 1;
    while (j >= 0 && a[j] > key){ a[j+1] = a[j]; j--; }
    a[j + 1] = key;
}
生活小例子:三种排队的办法
  • 冒泡 = 体育课相邻两个人比个子,高的往后挪,一轮下来最高的沉到最后;
  • 选择 = 老师每轮点名挑出最矮的站到最前面
  • 插入 = 打扑克抓一张牌,插到手里已排好序的牌中合适的位置
易错点:① 冒泡外层 n-1 轮,内层 n-1-i;② 三者都是 O(n²),n 很大时慢;③ 桶排序要求数据范围小。

4.6 快速排序与归并排序四级

① 快速排序(分治思想)

C++
void qsort(int a[], int l, int r){
    if (l >= r) return;
    int x = a[(l + r) / 2], i = l, j = r;
    while (i <= j){
        while (a[i] < x) i++;
        while (a[j] > x) j--;
        if (i <= j){ swap(a[i], a[j]); i++; j--; }
    }
    qsort(a, l, j);
    qsort(a, i, r);
}

② 归并排序(先分再合,稳定)

C++
void merge_sort(int a[], int l, int r){
    if (l >= r) return;
    int mid = (l + r) / 2;
    merge_sort(a, l, mid);
    merge_sort(a, mid + 1, r);
    // 合并两个有序段
    int tmp[1000], k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        tmp[k++] = (a[i] <= a[j]) ? a[i++] : a[j++];
    while (i <= mid) tmp[k++] = a[i++];
    while (j <= r) tmp[k++] = a[j++];
    for (int t = 0; t < k; t++) a[l + t] = tmp[t];
}
易错点:① 快排平均 O(n log n),但数据有序时可能退化;② 归并排序稳定,还能用来求逆序对;③ C++ 直接用 sort(a, a+n) 更省事。

4.7 高精度运算四级

① 高精度的思想

数字太大(超过 long long 范围)时,用数组一位一位存,像列竖式一样逐位计算。

C++
// 高精度加法:a、b 为两个数组(低位在前),返回结果
string add(string x, string y){
    // 先把两个字符串反转,低位在前
    reverse(x.begin(), x.end());
    reverse(y.begin(), y.end());
    string r; int carry = 0;
    int n = max(x.size(), y.size());
    for (int i = 0; i < n; i++){
        int d = carry;
        if (i < x.size()) d += x[i] - '0';
        if (i < y.size()) d += y[i] - '0';
        r += char('0' + d % 10);
        carry = d / 10;
    }
    if (carry) r += char('0' + carry);
    reverse(r.begin(), r.end());
    return r;
}
生活小例子:列竖式笔算

高精度想成列竖式做加法

  • 个位对齐,一位一位加;
  • 满十就进位(carry)到上一位;
  • 数字太长写不下,就每位写一格(数组),像列竖式一样一格一格算。

电脑里也是一样:把大数拆成一格一格(一位一位),逐位相加、处理进位。

易错点:① 高精度要用字符串/数组存,不能直接用 int;② 进位处理是核心,别忘了最后可能多一位;③ 大数比较大小要先比位数。

四级模拟题四级

选择题已知 int a = 5, *p = &a;,执行 *p = 10; 后 a 的值是?
A. 5B. 10C. 不确定D. 报错
答案:B
*p 通过地址访问 a 本身,*p = 10 就是把 a 改成 10。
选择题十进制 13 转成二进制是?
A. 1010B. 1101C. 1110D. 1001
答案:B
13 = 8+4+1 = 二进制 1101。用“8 4 2 1”分量法最方便。
选择题6 & 3 的结果是?
A. 7B. 5C. 2D. 0
答案:C
6=110,3=011,按位与:110 & 011 = 010 = 2。
编程题用结构体存 3 个学生的姓名和成绩,输出成绩最高者的姓名。
答案:参考思路见解析
struct St{ string name; int score; }; St a[3]; 读入后,用一个变量记录最高分的下标,最后输出。
结构体把“姓名+成绩”打包,用循环比较 score 找最大。
5

五级 · 算法进阶

快速幂 · 递推 · 贪心 · 前缀和/差分 · 二分 · 双指针 · STL
能力目标:常用高效算法核心:快速幂、贪心、二分、STL

5.1 快速幂五级

① 快速幂的思想

计算 a^b,如果一个个乘要 b 次;快速幂利用 b 的二进制分解,把次数降到 O(log b)

C++
long long qpow(long long a, long long b){
    long long r = 1;
    while (b > 0){
        if (b & 1) r = r * a;   // 当前二进制位是 1 就乘
        a = a * a;              // a 翻倍:a^1, a^2, a^4, a^8...
        b >>= 1;                // b 右移一位
    }
    return r;
}
// 例:qpow(2, 10) = 1024
生活小例子:折纸翻倍

快速幂想成折纸:一张纸对折 1 次厚度 ×2,对折 2 次 ×4,对折 3 次 ×8……

想算 2 的 10 次方,不用一张一张叠 10 次,而是每次让厚度翻倍:2→4→8→16→…→1024,只要翻 10 次“×2”。

快速幂就是把“连乘”改成“不断翻倍”,快得多!

易错点:① 结果可能超大,用 long long 或取模;② 常配合取模:r = r * a % mod;③ 底数 a 每次要平方。

5.2 递推算法五级

① 递推的思想

递推是从已知的初始项出发,用循环从前往后一步步推出后面的项。

C++
// 斐波那契:f(1)=1, f(2)=1, f(n)=f(n-1)+f(n-2)
long long f[100];
f[1] = f[2] = 1;
for (int i = 3; i <= n; i++)
    f[i] = f[i - 1] + f[i - 2];
生活小例子:爬楼梯

递推想成爬楼梯:每次可以走 1 级或 2 级,问 n 级台阶有几种走法?

  • 到第 1 级:1 种(走 1 级);
  • 到第 2 级:2 种(1+1 或 2);
  • 到第 n 级:只能从 n-1 级走 1 步,或从 n-2 级走 2 步 → f(n) = f(n-1) + f(n-2)

先算小的,一步步推大的,就是递推!

易错点:① 递推要先定初始值;② 大数用 long long 或高精度;③ 递推比递归快(避免重复计算)。

5.3 贪心算法五级

① 贪心的思想

贪心就是每一步都选当前看起来最优的做法,希望最后得到全局最优。

C++
// 找零钱:用最少的硬币凑出 n 元(面值 1, 5, 10, 50)
int coins[] = {50, 10, 5, 1};
int n, cnt = 0;
cin >> n;
for (int i = 0; i < 4; i++){
    cnt += n / coins[i];     // 尽量用大面值
    n %= coins[i];
}
cout << cnt << endl;
生活小例子:挑最大苹果

贪心想成从一筐苹果里挑出最大的:每次只看眼前,选当前最大(最大的苹果)——只要标准选对,这样最后往往就是全局最优。

但注意:贪心不是万能的!有时眼前最优会让后面更糟(比如钱不够找零的情况)。能用贪心的问题,都要能证明“每步最优 = 全局最优”。

易错点:① 贪心不一定总是正确,要先想清楚为什么每一步最优;② 找零钱要面值从大到小排序;③ 常用贪心:活动安排、哈夫曼编码、最小硬币数。

5.4 前缀和五级

① 前缀和的思想

pre[i] 表示前 i 个数的和。有了它,区间 [l, r] 的和 = pre[r] - pre[l-1],一次就算出任意区间和。

C++
int a[6] = {0, 1, 3, 2, 4, 5};   // a[1]~a[5]
int pre[6] = {0};
for (int i = 1; i <= 5; i++)
    pre[i] = pre[i - 1] + a[i];
// pre = {0, 1, 4, 6, 10, 15}
// 求 a[2]~a[4] 的和:pre[4] - pre[1] = 10 - 1 = 9
cout << pre[4] - pre[1] << endl;   // 9
生活小例子:存钱罐累计账本

前缀和想成记账本:你每天存钱,账本上记“到这天为止一共存了多少”。

想知道“第 3 天到第 5 天共存了多少”?不用重新加,用 第 5 天累计 - 第 2 天累计 就行!

这就是前缀和的威力:先把累计算好,任意区间一减就出来

易错点:① 前缀和数组下标从 1 开始更方便;② 区间和公式 pre[r] - pre[l-1];③ 算二维前缀和要注意容斥(加左上减两块)。

5.5 差分五级

① 差分的思想

差分是前缀和的“逆运算”。多次给区间 [l, r] 整体加同一个数,用差分能一次遍历完成

C++
int diff[1000] = {0};
// 区间 [l, r] 整体 +v
void add(int l, int r, int v){
    diff[l] += v;
    diff[r + 1] -= v;
}
// 多次 add 后,前缀和还原出每个位置的最终值
for (int i = 1; i <= n; i++){
    diff[i] += diff[i - 1];      // 累加还原
    cout << diff[i] << " ";      // 第 i 个位置的值
}
生活小例子:给一排花浇水

差分想成给一排花浇水:老师宣布“从第 3 盆到第 7 盆各浇一壶水”。

不用真的走到每一盆浇一次,而是在第 3 盆处放一个“+1”标记,第 8 盆处放一个“-1”标记。最后从第 1 盆走到第 N 盆,边走边累计标记,就知道每盆该浇几壶了。

很多次区间操作,一次遍历搞定,这就是差分的妙处。

易错点:① 差分修改是 O(1),还原是 O(n);② 记得在 r+1-v,否则会“越界累加”;③ 差分常与前缀和配合使用。

5.6 二分法五级

① 二分查找(前提:有序)

C++
int a[10] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19};
int find(int x){
    int l = 0, r = 9;
    while (l <= r){
        int mid = (l + r) / 2;
        if (a[mid] == x) return mid;
        if (a[mid] < x) l = mid + 1;
        else            r = mid - 1;
    }
    return -1;
}
生活小例子:1~100 猜数字

二分想成老师想了一个 1~100 的数字,你来猜:每次猜中间,老师答“大了/小了”,立刻砍掉一半范围。最多 7 次必中(2⁷=128>100)。

从 1 挨个猜要 100 次,二分只要 7 次——这就是二分快的秘密!

注意前提:数字必须排好序

易错点:① 二分要求有序;② 用 mid = (l + r) / 2 防止溢出;③ 二分答案常用于“求最大最小值”类问题。

5.7 双指针五级

① 双指针的思想

两个指针在数组上移动来高效解决问题,常见“相向”和“同向(快慢)”两种。

C++
// 例:有序数组中找两数和等于 target
int a[6] = {1, 3, 4, 6, 8, 10}, target = 14;
int i = 0, j = 5;              // 相向双指针
while (i < j){
    int s = a[i] + a[j];
    if (s == target){ cout << a[i] << " " << a[j]; break; }
    if (s < target) i++;       // 和小了,左边右移
    else            j--;       // 和大了,右边左移
}
易错点:① 双指针能去掉一层循环(O(n²)→O(n));② 同向双指针常用于“滑动窗口”;③ 前提通常要数组有序

5.8 STL:string / vector / set / map五级

① STL 是 C++ 的“现成工具箱”

STL(标准模板库)提供很多现成的容器,不用自己造轮子。

C++
#include <vector>
#include <set>
#include <map>
vector<int> v = {3, 1, 2};
v.push_back(5);          // 末尾加 5
sort(v.begin(), v.end());// 排序
v.size(); v[0];

set<int> s;              // 自动去重 + 排序
s.insert(5); s.insert(5); s.insert(3);
// s 里只有 {3, 5}

map<string, int> m;      // 键值对
m["apple"] = 3;          // 像字典
m["banana"] = 5;
cout << m["apple"];      // 3
生活小例子:现成的工具箱

STL想成学校发的“现成工具箱”

  • vector = 能自动变长的“伸缩抽屉”(动态数组);
  • set = “自动去重排序的收纳盒”(不会放重复东西,还自动排好);
  • map = “按名字找东西的通讯录”(键→值)。

用现成工具,省时又不容易出错,考试常用!

易错点:vectorpush_back 追加;② set 自动去重且有序;③ map 用下标访问,不存在的键会自动插入默认值。

五级模拟题五级

选择题用二分法在 a[0..999](有序)中查找,最多比较多少次?
A. 10 次B. 100 次C. 999 次D. 500 次
答案:A
1000 ≈ 2¹⁰,二分每次砍半,最多约 log₂1000 ≈ 10 次。
选择题前缀和数组 pre 中,区间 [3, 6] 的和应该用哪个式子?
A. pre[6] - pre[2]B. pre[6] - pre[3]C. pre[3] - pre[6]D. pre[6] + pre[2]
答案:A
区间 [l, r] 的和 = pre[r] - pre[l-1] = pre[6] - pre[2]。
编程题用贪心:有若干活动(起止时间),选出最多互不冲突的活动。
答案:参考思路见解析
按结束时间从小到大排序,依次选择“结束最早且不与上一个冲突”的活动。
经典贪心:先按结束时间排序,能选就选。
编程题用快速幂求 2 的 30 次方。
答案:参考程序见解析
qpow(2, 30),用 while 循环:b 的二进制位为 1 时累乘,底数每轮平方,b 右移。
次数从 30 降到约 5 次循环,这就是快速幂。
6

六级 · 数据结构与动态规划起步

栈/队列 · 链表 · DFS/BFS · 剪枝 · 动态规划 · 背包 · 数论 · 组合
能力目标:数据结构与简单 DP核心:搜索、背包、欧几里得、排列组合

6.1 栈和队列六级

① 栈 Stack(后进先出)

只能在一端(栈顶)操作:push 入栈、pop 出栈、top 看栈顶。后进先出

C++
#include <stack>
stack<int> st;
st.push(1); st.push(2); st.push(3);
st.top();        // 3
st.pop();        // 弹掉 3
st.size();       // 2

② 队列 Queue(先进先出)

队列从队尾进、队首出:push 入队、pop 出队、front 看队首。先进先出

C++
#include <queue>
queue<int> q;
q.push(1); q.push(2); q.push(3);
q.front();       // 1(队首)
q.pop();         // 弹掉 1
q.size();        // 2
生活小例子:叠盘子 & 食堂排队
  • = 叠盘子:洗好一个放最上面,用时先拿最上面——后放先拿(撤销键也是栈!);
  • 队列 = 食堂排队打饭:先来的人排前面、先打饭——先来先走

口诀:栈=后进先出,队列=先进先出,方向别搞混!

易错点:① 栈的 top、队列的 front 都要先判空再访问;② 用前记得 #include <stack>/<queue>;③ 括号匹配、表达式求值常用栈,广度搜索常用队列。

6.3 深度优先搜索 DFS六级

① DFS 的思想

深度优先搜索:从起点出发,一条路走到黑,走不通再退回来换一条(回溯)。

C++
// 例:全排列 1~n(用 DFS 回溯)
int n = 3, a[10];
bool used[10] = {false};
void dfs(int k){
    if (k > n){                       // 选满 n 个,输出
        for (int i = 1; i <= n; i++) cout << a[i] << " ";
        cout << endl; return;
    }
    for (int i = 1; i <= n; i++){     // 尝试每个没用过的数
        if (!used[i]){
            used[i] = true;  a[k] = i;
            dfs(k + 1);               // 深入下一层
            used[i] = false;          // 回溯:撤销选择
        }
    }
}
生活小例子:走迷宫一条道走到黑

DFS想成走迷宫

  • 选一条路一直往前走,碰到死胡同就退回来,再走另一条;
  • “退回来”就是回溯——把刚才做的选择撤销;
  • 直到把所有路都试过,就找到了所有走法(如全排列、走迷宫路线)。
易错点:① 递归实现 DFS,一定要有出口;② 回溯时要恢复现场(used[i] 重新置 false);③ DFS 常用递归,也可能爆栈,注意深度。

6.4 广度优先搜索 BFS六级

① BFS 的思想

广度优先搜索:一层一层向外扩展,先访问离起点近的,用队列实现。BFS 能找到最短路径(无权图)。

C++
#include <queue>
// 例:迷宫最短步数(0 可走,1 是墙),从 (sx,sy) 到 (ex,ey)
int dx[4] = {1,-1,0,0}, dy[4] = {0,0,1,-1};
int dist[105][105];              // -1 表示未访问
queue<pair<int,int>> q;
q.push({sx, sy}); dist[sx][sy] = 0;
while (!q.empty()){
    auto [x, y] = q.front(); q.pop();
    for (int k = 0; k < 4; k++){
        int nx = x + dx[k], ny = y + dy[k];
        if (nx<0||ny<0||nx>=n||ny>=m) continue;   // 越界
        if (maze[nx][ny]) continue;               // 墙
        if (dist[nx][ny] != -1) continue;         // 已访问
        dist[nx][ny] = dist[x][y] + 1;
        q.push({nx, ny});
    }
}
cout << dist[ex][ey];
生活小例子:石子落水的水波

BFS想成石子扔进水里荡开的波纹

  • 波纹一圈一圈往外扩,先碰到岸边的是离得最近的;
  • 队列就像“波纹的排队队伍”:先来的圈先处理,处理完再扩展下一圈;
  • 所以 BFS 第一次到达终点时,步数一定最短

口诀:DFS 用栈(一条道走到底),BFS 用队列(一圈圈扩散)。

易错点:① BFS 用队列,且要记录是否访问过防止死循环;② 距离数组初始化 -1 表示未访问;③ 越界、撞墙要先判断。

6.5 搜索剪枝六级

① 剪枝的思想

剪枝就是在搜索过程中,提前判断这条路不可能出解,直接放弃,不去走,从而大幅加速。

  • 可行性剪枝:已经超过目标就停止(如累加超过 target 直接 return);
  • 最优性剪枝:当前代价已经不比已找到的最优解好,就放弃;
  • 奇偶剪枝:根据步数奇偶性提前排除不可能路径。
生活小例子:翻字典找词跳过整个字母区

剪枝想成翻字典查词:要找 “zebra”,你不会从第 1 页挨页翻,而是直接翻到后半本 Z 区——把不可能的部分整个跳过。

搜索也一样:发现这条路不可能有解,就整条放弃,别傻傻走到底。剪枝剪得好,程序快十倍都不止。

易错点:① 剪枝条件要绝对正确(剪错就漏答案);② 常用:最优性剪枝(当前解已差于历史最优);③ 先想清楚“什么情况下这条路不可能出解”。

6.6 动态规划概念六级

① DP 的思想

动态规划(DP)把大问题拆成小问题,用状态表示“某个阶段的最优值”,并通过状态转移方程从小问题推出大问题。

② 爬楼梯例子

C++
// f[i] = 到第 i 级台阶的走法数
// 转移方程:f[i] = f[i-1] + f[i-2]
int f[50];
f[1] = 1; f[2] = 2;
for (int i = 3; i <= n; i++)
    f[i] = f[i - 1] + f[i - 2];
生活小例子:填表法

动态规划想成画一张表,一格一格填

  • 先填好第一行(初始状态);
  • 后面的每一格,都根据它前面(或上面)的格子算出来(状态转移);
  • 填到最后,答案就写在最后那格。

和递归的区别:递归是“从后往前问”,DP 是“从前往后填表”,不重复计算,更快。

易错点:① DP 三要素:状态定义、转移方程、初始值;② 先想清楚“状态代表什么”;③ 从递推入手,再慢慢练背包、区间等经典模型。

6.7 背包问题六级

① 01 背包

有 n 件物品,每件有重量 w 和价值 v,背包容量 c,每件最多拿 1 件,求能装的最大价值。

C++
// dp[j] = 容量 j 时能装的最大价值
int dp[1005] = {0};
for (int i = 0; i < n; i++)              // 遍历物品
    for (int j = c; j >= w[i]; j--)      // 容量倒着遍历(保证每件只拿一次)
        dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
cout << dp[c] << endl;
生活小例子:行李箱装玩具

01 背包想成往行李箱装玩具去旅行

  • 每件玩具只能装 1 件(01 就是 0 或 1 次);
  • 行李箱容量有限(背包容量);
  • 要挑出总价值最高的组合,装不下就放弃重的、选价值高的。

dp[j] 记录“容量 j 时的最优价值”,逐个玩具决定“装 or 不装”。

易错点:① 01 背包容量要倒着遍历(防止同一件装多次);② 完全背包容量正着遍历;③ dp 数组要初始化为 0(或不取模的 -inf)。

6.8 区间动态规划六级

① 区间 DP 的思想

区间 DP 以区间 [i, j] 为状态,通常枚举区间长度从小到大,再枚举分割点合并。

C++
// 石子合并:合并相邻石子堆的最小代价
int dp[305][305];
for (int len = 2; len <= n; len++)          // 区间长度
    for (int i = 1; i + len - 1 <= n; i++){
        int j = i + len - 1;
        dp[i][j] = 1e9;
        for (int k = i; k < j; k++)          // 分割点
            dp[i][j] = min(dp[i][j],
                dp[i][k] + dp[k+1][j] + sum(i, j));
    }
易错点:① 区间 DP 先枚举长度再枚举起点;② 合并类问题常用前缀和快速算区间和;③ 状态含义常为“区间 [i,j] 的最优值”。

6.9 数论基础:欧几里得与素数筛六级

① 最大公约数(欧几里得算法)

C++
int gcd(int a, int b){
    return b == 0 ? a : gcd(b, a % b);
}
int lcm(int a, int b){
    return a / gcd(a, b) * b;    // 最小公倍数
}

② 素数筛:埃氏筛

C++
bool isprime[1000005];
void sieve(int n){
    for (int i = 2; i <= n; i++) isprime[i] = true;
    for (int i = 2; i * i <= n; i++)
        if (isprime[i])
            for (int j = i * i; j <= n; j += i)
                isprime[j] = false;   // 划掉 i 的倍数
}

③ 线性筛(欧拉筛,更快)

C++
int primes[100000], pc = 0;
bool np[1000005];
void linear_sieve(int n){
    for (int i = 2; i <= n; i++){
        if (!np[i]) primes[pc++] = i;
        for (int j = 0; j < pc && i * primes[j] <= n; j++){
            np[i * primes[j]] = true;
            if (i % primes[j] == 0) break;
        }
    }
}
易错点:① 欧几里得:gcd(a,b)=gcd(b, a%b),出口是 b==0;② 埃氏筛从 i*i 开始划;③ 线性筛每个合数只被最小质因数筛一次。

6.10 组合数学:加法原理、乘法原理、排列组合六级

① 两个基本原理

  • 加法原理:做一件事有 A、B 两类方法,分别有 m、n 种 → 共 m+n 种(“或”关系相加);
  • 乘法原理:做一件事分两步,第一步 m 种、第二步 n 种 → 共 m×n 种(“且”关系相乘)。

② 排列与组合

排列:从 n 个中选 m 个排顺序,A(n,m) = n×(n-1)×…×(n-m+1)。

组合:从 n 个中选 m 个不管顺序,C(n,m) = A(n,m) / m!。

C++
long long C(int n, int m){     // 组合数(m <= n)
    if (m > n - m) m = n - m;
    long long r = 1;
    for (int i = 0; i < m; i++)
        r = r * (n - i) / (i + 1);   // 边乘边除防溢出
    return r;
}
生活小例子:选班长和排座位
  • 排列 = 从 5 个同学里选 3 个分别当班长、副班长、学习委员(岗位不同=有顺序);
  • 组合 = 从 5 个同学里选 3 个一起去参加比赛(都是队员=没顺序);
  • 乘法原理 = 早餐 2 种主食 × 3 种饮品 = 6 种搭配。

看关键词:有职位/有顺序 → 排列;只是选人 → 组合;分步完成 → 相乘。

易错点:① 排列和组合的区别是要不要顺序;② 加法/乘法看“或”还是“且”;③ 组合数用 C(n,m) = C(n, n-m) 简化。

六级模拟题六级

选择题栈按顺序 push 1、2、3 后,依次 pop 得到的顺序是?
A. 1, 2, 3B. 3, 2, 1C. 1, 3, 2D. 2, 1, 3
答案:B
栈是后进先出,最后 push 的 3 最先出来 → 3, 2, 1。
选择题BFS(广度优先搜索)通常使用什么数据结构?
A. 栈B. 队列C. 堆D. 数组
答案:B
BFS 一层层扩展,先来的先处理,用队列;DFS 用栈。
选择题从 4 个同学中选 2 个参加比赛,有多少种选法?
A. 12B. 8C. 6D. 4
答案:C
组合 C(4,2) = 4×3÷2 = 6。选人不分先后。
编程题用 DFS 输出 1~4 的所有排列。
答案:参考思路见解析
dfs(k) 表示已选 k 个数,用 used[] 标记,for 尝试每个未用数字,递归下一层,回溯时恢复 used。
经典回溯模板:标记→递归→恢复。
编程题01 背包:n 件物品、容量 c,求最大价值。
答案:参考思路见解析
dp[j] 倒序遍历:for j=c; j>=w[i]; j-- dp[j]=max(dp[j], dp[j-w[i]]+v[i]);
容量倒着遍历保证每件物品只取一次。
7

七级 · 树图与算法深化

树 · 二叉树 · 复杂贪心/DP · 图 · 拓扑 · 最短路 · 最小生成树 · 哈希
能力目标:图论与树形算法核心:树的遍历、最短路、最小生成树

7.1 树:定义、存储与遍历七级

① 树的概念

是“一对多”的非线性结构:有唯一根节点,每个节点可以有若干孩子,没有环(像一棵倒着长的树)。

② 树的存储与遍历

C++
vector<int> children[1005];   // 邻接表存树:children[父亲] = 孩子们
int n, root;
void dfs(int u){                // 先序遍历:先访问自己,再访问孩子
    cout << u << " ";
    for (int v : children[u])
        dfs(v);
}
生活小例子:家族树

想成家族谱系图:爷爷是,下面分爸爸、叔叔,再下面分堂兄弟姐妹……一直往下分。

遍历树就像挨家挨户拜访:可以先拜访自己、再去孩子家(先序),也可以先把孩子家都走完、最后回自己(后序)。

易错点:① 树没有环、n 个节点有 n-1 条边;② 用邻接表存孩子最方便;③ 深度优先递归遍历是树的基本功。

7.2 二叉树的性质与遍历七级

① 二叉树的性质

  • 每个节点最多两个孩子(左、右子树);
  • 第 i 层最多 2^(i-1) 个节点;
  • 深度为 k 的二叉树最多 2^k - 1 个节点;
  • 叶子节点数 = 度为 2 的节点数 + 1(n0 = n2 + 1)。

② 三种遍历

C++
struct Node { int val; Node *l, *r; };
void pre(Node *p){          // 前序:根 左 右
    if (!p) return;
    cout << p->val << " ";  pre(p->l);  pre(p->r);
}
void in(Node *p){           // 中序:左 根 右
    if (!p) return;
    in(p->l);  cout << p->val << " ";  in(p->r);
}
void post(Node *p){         // 后序:左 右 根
    if (!p) return;
    post(p->l);  post(p->r);  cout << p->val << " ";
}
生活小例子:按规矩拜访亲戚

三种遍历想成拜访家族亲戚的顺序,根=爷爷:

  • 前序(根左右):先爷爷,再左边一家,再右边一家;
  • 中序(左根右):先把左边的亲戚走完,再爷爷,再右边;
  • 后序(左右根):先把孩子辈走完,最后才轮到爷爷。

口诀:看“根”在哪个位置:前=先根、中=中间、后=最后。

易错点:① 二叉树中序遍历一棵二叉搜索树会得到有序序列;② 知道中序 + 前序(或后序)可以唯一还原二叉树;③ 递归时记得判空。

7.3 较复杂的贪心与动态规划七级

① 复杂贪心

复杂贪心往往需要先排序、再依次贪心,或需要证明贪心策略。常见模型:区间选点、任务调度、背包的贪心近似等。

C++
// 例:活动安排——选最多互不冲突的活动
// 先按结束时间排序
sort(a, a + n, [](Act x, Act y){ return x.r < y.r; });
int last = -1, cnt = 0;
for (int i = 0; i < n; i++)
    if (a[i].l > last){        // 不与上一个冲突就选
        cnt++; last = a[i].r;
    }

② 复杂 DP

当状态超过一维(如“天 + 手里剩余”),就需要多维 DP。关键仍是:想清楚状态、转移、初始值。

C++
// 例:数字三角形最大路径和
int f[105][105];
for (int i = n - 1; i >= 1; i--)       // 从下往上推
    for (int j = 1; j <= i; j++)
        f[i][j] += max(f[i+1][j], f[i+1][j+1]);
cout << f[1][1];
易错点:① 贪心要证明“每步最优=全局最优”,否则可能错;② 多维 DP 用二维/三维数组,注意下标越界;③ 复杂 DP 多从“小规模枚举”找规律。

7.4 图:定义、存储与遍历七级

① 图的概念

顶点组成,边可以有方向(有向图)和权值(带权图)。

② 图的存储

C++
// 邻接矩阵(n 较小、边多时)
int g[505][505];
g[u][v] = w;                 // u 到 v 权值为 w

// 邻接表(n 大、边少时,推荐)
vector<pair<int,int>> adj[1005];
adj[u].push_back({v, w});    // u 有一条边到 v,权值 w

③ 图的遍历

C++
bool vis[1005];
void dfs(int u){             // DFS 遍历
    vis[u] = true;
    for (auto [v, w] : adj[u])
        if (!vis[v]) dfs(v);
}
易错点:① 邻接矩阵空间 O(n²),邻接表空间 O(n+m);② 有向图和无向图加边方式不同(无向图加两条);③ 遍历记得标记 vis 防重复。

7.5 拓扑排序七级

① 拓扑排序的思想

有向无环图(DAG)排一个顺序,使所有边都从“前”指向“后”。做法:每次找入度为 0 的点输出并删除其出边。

C++
queue<int> q;
int indeg[1005];
for (int i = 1; i <= n; i++) if (indeg[i] == 0) q.push(i);
while (!q.empty()){
    int u = q.front(); q.pop();
    cout << u << " ";
    for (int v : adj[u])
        if (--indeg[v] == 0) q.push(v);
}
// 若输出的点数 < n,说明图里有环
生活小例子:穿衣服的顺序

拓扑排序想成穿衣服:必须先穿衬衫才能穿外套,先穿袜子才能穿鞋。

“必须先穿什么”就是有向边,拓扑排序就是找出一个不违反任何“先穿后穿”规则的穿衣顺序

如果出现“A 要先穿 B、B 又先穿 A”的怪圈,就是有环,无法排序!

易错点:① 只能用于有向无环图;② 用队列存入度为 0 的点;③ 输出数 < 顶点数说明有环。

7.6 最短路算法七级

① Dijkstra(单源最短路,非负权)

C++
// 用优先队列(堆)优化
int dist[1005]; bool done[1005];
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> pq;
memset(dist, 0x3f, sizeof dist);
dist[s] = 0; pq.push({0, s});
while (!pq.empty()){
    auto [d, u] = pq.top(); pq.pop();
    if (done[u]) continue; done[u] = true;
    for (auto [v, w] : adj[u])
        if (dist[v] > dist[u] + w){
            dist[v] = dist[u] + w;
            pq.push({dist[v], v});
        }
}

② Floyd(多源最短路,可负权,O(n³))

C++
for (int k = 1; k <= n; k++)
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (d[i][k] + d[k][j] < d[i][j])
                d[i][j] = d[i][k] + d[k][j];
生活小例子:找最短的上学路线

Dijkstra想成找从家到学校最短的路

  • 每次都从“已知距离最近的没走过的路口”出发,看看能否让邻居更近;
  • 像水往外渗,一层层确认最短距离
  • Floyd 则是“把每个路口都当一次中转站”试试,最后所有点对的最短路都算出来。
易错点:① Dijkstra 要求边权非负;有负权用 Bellman-Ford / SPFA;② Floyd 枚举中转点 k 在最外层;③ 别忘初始化 dist 为无穷大。

7.7 最小生成树七级

① 最小生成树的概念

在带权无向图中,选 n-1 条边把 n 个点连通,且总权值最小,就是最小生成树。

② Kruskal(按边从小到大加,用并查集判环)

C++
struct Edge { int u, v, w; };
sort(e, e + m, [](Edge a, Edge b){ return a.w < b.w; });
int ans = 0, cnt = 0;
for (int i = 0; i < m && cnt < n - 1; i++){
    int ru = find(e[i].u), rv = find(e[i].v);
    if (ru != rv){           // 不成环就加
        uni(ru, rv);
        ans += e[i].w; cnt++;
    }
}
易错点:① Kruskal 核心:边按权排序 + 并查集判环;② 加的边数达到 n-1 就完成;③ Prim 是从点出发每次找最近点,也常用。

7.8 哈希算法七级

① 哈希的思想

哈希把大范围数据映射到小范围下标,实现 O(1) 查找。不同数据映射到同一位置叫冲突,要解决冲突。

C++
// 字符串哈希:把字符串变成一个整数
long long hash_str(const string &s){
    long long h = 0, P = 131, MOD = 1e9+7;
    for (char c : s)
        h = (h * P + c) % MOD;
    return h;
}
// 哈希表:用 unordered_map 直接实现
#include <unordered_map>
unordered_map<string, int> cnt;
cnt["apple"]++;              // O(1) 增删查
生活小例子:按姓氏归类的通讯录

哈希想成图书馆按“姓氏首字母”分区:要找“张伟”,先按姓跳到 Z 区,再在区内找——不用翻整本目录

“按姓分区”就是哈希函数,两个人都姓“张”挤在同一个区,就是冲突(解决了就行)。

所以哈希让查找从“翻遍所有”变成“直奔主题”。

易错点:① 哈希函数要分布均匀减少冲突;② 冲突解决:拉链法(链表)、开放寻址;③ 常用 unordered_map 直接享受 O(1) 查找。

七级模拟题七级

选择题深度为 5 的二叉树最多有多少个节点?
A. 16B. 31C. 32D. 15
答案:B
最多 2⁵ - 1 = 31 个节点。
选择题拓扑排序只能在哪种图上进行?
A. 有向有环图B. 无向图C. 有向无环图D. 任意图
答案:C
只有有向无环图(DAG)才能拓扑排序,有环就无法排。
选择题Dijkstra 算法不能处理哪种情况?
A. 边权为正B. 边权为负C. 无向图D. 有向图
答案:B
Dijkstra 要求边权非负;有负权边要用 Bellman-Ford 或 SPFA。
编程题给一棵二叉树的先序和中序遍历,输出后序遍历。
答案:参考思路见解析
先序第一个是根;在中序里找到根,左边是左子树、右边是右子树;递归左右再输出根。
先序+中序可唯一还原二叉树,后序=左+右+根。
8

八级 · 高级数据结构

倍增/ST 表 · 并查集 · 树状数组 · 线段树 · Trie · 欧拉回路 · KMP · 复杂 DP
能力目标:高级数据结构的应用核心:并查集、线段树、KMP

8.1 倍增法与 ST 表八级

① 倍增的思想

倍增用“2 的幂次跳”加速:先预处理 2^k 步的信息,再按二进制位组合跳跃,把 O(n) 变成 O(log n)。

② ST 表(静态区间最值 RMQ)

C++
// st[i][k] = 从 i 开始、长度 2^k 的区间最大值
int st[100005][20], lg[100005];
void build(int n, int a[]){
    for (int i = 1; i <= n; i++) st[i][0] = a[i];
    for (int k = 1; (1 << k) <= n; k++)
        for (int i = 1; i + (1 << k) - 1 <= n; i++)
            st[i][k] = max(st[i][k-1], st[i + (1<<(k-1))][k-1]);
    for (int i = 2; i <= n; i++) lg[i] = lg[i/2] + 1;
}
int query(int l, int r){      // O(1) 查区间最大值
    int k = lg[r - l + 1];
    return max(st[l][k], st[r - (1 << k) + 1][k]);
}
易错点:① ST 表查询 O(1) 但不能修改(静态);② 二区间覆盖法:两个长度 2^k 的区间盖住 [l,r];③ 预处理 O(n log n)。

8.2 并查集八级

① 并查集的思想

并查集快速判断两个元素是否在同一集合,并合并两个集合。核心是“找根”+“路径压缩”。

C++
int fa[100005];
void init(int n){ for (int i = 1; i <= n; i++) fa[i] = i; }
int find(int x){               // 找根(带路径压缩)
    if (fa[x] == x) return x;
    return fa[x] = find(fa[x]);
}
void uni(int a, int b){        // 合并
    fa[find(a)] = find(b);
}
bool same(int a, int b){ return find(a) == find(b); }
生活小例子:认亲戚找族长

并查集想成认亲戚

  • 每个人都有一个“族长”(根节点);
  • find 就是“一路往上问,问出族长是谁”;
  • 两个人族长相同 = 一家人(same);
  • 两家要合一家,就让一家的族长认另一家为“老大”(uni)。

路径压缩就是:问出族长后,顺手让大家都直接记住族长,下次问就快了。

易错点:① 记得初始化 fa[i]=i;② find递归+路径压缩;③ 合并前先 find 出根,防止环。

8.3 树状数组八级

① 树状数组的思想

树状数组(BIT)支持 单点修改 + 区间求和,都是 O(log n),代码比线段树短。

C++
int tree[100005], n;
int lowbit(int x){ return x & (-x); }
void add(int i, int v){          // 单点加 v
    for (; i <= n; i += lowbit(i)) tree[i] += v;
}
int sum(int i){                  // 前缀和 [1..i]
    int r = 0;
    for (; i > 0; i -= lowbit(i)) r += tree[i];
    return r;
}
int query(int l, int r){ return sum(r) - sum(l - 1); }
易错点:lowbit = x & (-x) 是核心;② 更新往上加 lowbit,查询往下减 lowbit;③ 求逆序对也常用树状数组。

8.4 线段树八级

① 线段树的思想

线段树把数组按区间分块存到二叉树节点里,支持区间查询、区间修改(O(log n)),比树状数组更通用。

C++
int sum[4 * 100005], lazy[4 * 100005];
// 建树:节点 p 表示 [l, r]
void build(int p, int l, int r, int a[]){
    if (l == r){ sum[p] = a[l]; return; }
    int mid = (l + r) / 2;
    build(p*2, l, mid, a);
    build(p*2+1, mid+1, r, a);
    sum[p] = sum[p*2] + sum[p*2+1];
}
// 区间加 v 的“懒标记”更新
void add(int p, int l, int r, int ql, int qr, int v){
    if (ql <= l && r <= qr){ sum[p] += v*(r-l+1); lazy[p] += v; return; }
    // 下传懒标记……
}
易错点:① 数组要开 4 倍空间;② 区间修改用懒标记延迟下传;③ 线段树可以扩展到最大值、最小值的区间查询。

8.5 Trie 树(字典树)八级

① Trie 的思想

Trie 树把字符串按公共前缀共享存储,适合单词统计、前缀匹配

C++
int nxt[100005][26], cnt[100005], tot = 0;
void insert(const string &s){      // 插入单词
    int u = 0;
    for (char c : s){
        int k = c - 'a';
        if (!nxt[u][k]) nxt[u][k] = ++tot;
        u = nxt[u][k];
    }
    cnt[u]++;                       // 这个单词出现次数
}
int query(const string &s){        // 查询单词出现次数
    int u = 0;
    for (char c : s){
        int k = c - 'a';
        if (!nxt[u][k]) return 0;
        u = nxt[u][k];
    }
    return cnt[u];
}
易错点:① 每个节点存 26 个孩子的下标(nxt 数组);② 插入/查询都是沿着前缀走;③ 空间较大,注意数组大小。

8.6 欧拉回路八级

① 欧拉回路的概念

  • 欧拉路径:一笔画经过每条边恰好一次的路径;
  • 欧拉回路:欧拉路径还能回到起点;
  • 判定:无向图所有顶点度数为偶有欧拉回路;有向图每个点入度=出度有欧拉回路。
C++
// Hierholzer 算法求欧拉回路
void dfs(int u){
    for (int &i = head[u]; i < adj[u].size();){
        int v = adj[u][i++];
        dfs(v);                 // 先深入再输出
    }
    path.push_back(u);          // 反序存路径
}
// 最后把 path 倒过来输出就是欧拉回路
易错点:① 先判断“能不能一笔画”(度数条件);② 用“先递归后记录”的技巧避免死循环;③ 有向图和无向图的判定条件不同。

8.7 KMP 字符串匹配八级

① KMP 的思想

在文本串中找模式串,KMP 利用 next 数组让模式串“聪明地滑动”,避免重复比较,复杂度 O(n+m)。

C++
// next[i]:模式串前 i 个字符中,最长相等前后缀长度
int nxt[100005];
void get_next(const string &p){
    int n = p.size();
    nxt[0] = -1;
    for (int i = 1, j = -1; i < n; i++){
        while (j >= 0 && p[i] != p[j+1]) j = nxt[j];
        if (p[i] == p[j+1]) j++;
        nxt[i] = j;
    }
}
// 匹配
int kmp(const string &s, const string &p){
    int n = s.size(), m = p.size(), j = -1;
    for (int i = 0; i < n; i++){
        while (j >= 0 && s[i] != p[j+1]) j = nxt[j];
        if (s[i] == p[j+1]) j++;
        if (j == m - 1) return i - m + 1;   // 匹配成功位置
    }
    return -1;
}
生活小例子:查词典时“跳过已对上的字”

KMP想成对暗号:你已经和前面几个字“对上了”,突然发现下一个对不上,不用退回开头重来,而是利用已经对上的部分,往后滑一段接着比。

next 数组就是“提前记好的滑轨刻度”——每次失配该滑到哪,早就算好了,特别快。

易错点:① next 数组是核心:记录最长相等前后缀;② 失配时 j 跳到 nxt[j],主串指针不回退;③ 注意 next 数组下标实现(-1 或 0 基准)。

8.8 树形与状态压缩 DP八级

① 树形 DP

在树上做 DP:状态放在“节点”上,先算孩子再算父亲(后序遍历)。

C++
// 例:没有上司的舞会——选父亲就不能选孩子
// dp[u][0] 不选 u,dp[u][1] 选 u
void dfs(int u){
    dp[u][1] = val[u];
    for (int v : children[u]){
        dfs(v);
        dp[u][0] += max(dp[v][0], dp[v][1]);   // 不选 u,孩子随便
        dp[u][1] += dp[v][0];                  // 选 u,孩子不能选
    }
}

② 状态压缩 DP(状压 DP)

二进制位表示“选了哪些元素”的状态,适合 n 较小(n ≤ 20)的集合类问题。

C++
// 例:旅行商问题(TSP)状态:dp[visited][last]
int dp[1<<20][20];   // visited 是二进制集合,last 是当前城市
memset(dp, 0x3f, sizeof dp);
dp[1][0] = 0;                       // 从 0 出发
for (int S = 1; S < (1<<n); S++)
    for (int u = 0; u < n; u++)
        if (S >> u & 1)
            for (int v = 0; v < n; v++)
                if (!(S >> v & 1))
                    dp[S | (1<<v)][v] =
                        min(dp[S | (1<<v)][v], dp[S][u] + g[u][v]);
易错点:① 树形 DP 递归先算孩子;② 状压 DP 用 S >> i & 1 判断元素;③ 状压只适合 n 小(1<<n 太大就爆内存)。

八级模拟题八级

选择题并查集 find 操作通常配合什么优化?
A. 懒标记B. 路径压缩C. 旋转D. 分块
答案:B
find 配合路径压缩,让节点直接指向根,查询接近 O(1)。
选择题线段树区间求和的时间复杂度是?
A. O(1)B. O(log n)C. O(n)D. O(n log n)
答案:B
线段树区间查询/修改都是 O(log n)
选择题KMP 算法中 next 数组记录的是?
A. 字符出现次数B. 最长相等前后缀长度C. 字符串长度D. 哈希值
答案:B
next[i] 表示模式串前 i 个字符中最长相等前后缀长度,用于失配后快速滑动。
编程题用树状数组实现:单点修改 + 区间求和。
答案:参考思路见解析
add(i,v) 向上累加,sum(i) 向下累加,区间和 = sum(r)-sum(l-1)。核心 lowbit(x)=x&(-x)。
树状数组代码短、速度快,是最常用的数据结构之一。
9

九级 · 图论与数论进阶

连通性 · 模运算 · 扩展欧几里得 · 逆元 · 矩阵快速幂 · 高斯消元 · 容斥
能力目标:图论与数论的深入应用核心:强连通、矩阵、容斥

9.1 图的连通性:强连通 / 割点 / 双连通九级

① 强连通分量(SCC,Tarjan)

强连通:有向图中,两点能互相到达。最大互相到达的点集叫强连通分量。Tarjan 算法用 DFS + 时间戳 + 栈 求出所有 SCC,然后缩点成 DAG。

C++
int dfn[1005], low[1005], tim = 0, scc_cnt = 0;
int scc[1005]; stack<int> st; bool instk[1005];
void tarjan(int u){
    dfn[u] = low[u] = ++tim;
    st.push(u); instk[u] = true;
    for (int v : adj[u]){
        if (!dfn[v]){ tarjan(v); low[u] = min(low[u], low[v]); }
        else if (instk[v]) low[u] = min(low[u], dfn[v]);
    }
    if (low[u] == dfn[u]){         // 找到强连通分量的根
        scc_cnt++;
        while (true){
            int x = st.top(); st.pop(); instk[x] = false;
            scc[x] = scc_cnt;
            if (x == u) break;
        }
    }
}

② 割点 / 割边 / 双连通

  • 割点:删掉它,图变得不连通(无向图);
  • 割边(桥):删掉它,图变得不连通;
  • 点双连通 / 边双连通:删掉任意一个点/边仍连通的最大子图;
  • 都可以用 Tarjan 的时间戳 dfn 与 low 判断。
易错点:① Tarjan 三个要素:dfn(时间戳)、low(最早可达)、栈;② 割点判断:low[v] >= dfn[u](非根);③ 缩点后图变为 DAG,可做拓扑/DP。

9.2 模运算与扩展欧几里得九级

① 模运算性质

  • (a + b) % m = (a%m + b%m) % m
  • (a × b) % m = (a%m × b%m) % m(乘法取模要防溢出,用 long long)
  • 除法的取模不能直接拆,需要“逆元”。

② 扩展欧几里得(解 ax + by = gcd(a,b))

C++
long long exgcd(long long a, long long b,
                 long long &x, long long &y){
    if (b == 0){ x = 1; y = 0; return a; }
    long long g = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return g;
}
// 求 ax ≡ 1 (mod m) 的 x(a 与 m 互质时存在)
易错点:① 乘法取模记得 long long 防溢出;② 扩展欧几里得能解同余方程和逆元;③ 使用前提 a、m 互质。

9.3 逆元与费马小定理九级

① 逆元的概念

模意义下,a 的逆元a⁻¹,满足 a × a⁻¹ ≡ 1 (mod m)。有了逆元,就能“做除法”:a / b ≡ a × b⁻¹ (mod m)

② 费马小定理求逆元

若 p 是质数且 p ∤ a,则 a^(p-1) ≡ 1 (mod p),所以 a⁻¹ = a^(p-2) (mod p)

C++
// 用快速幂求逆元:a^(p-2) % p
long long inv(long long a, long long p){
    return qpow(a, p - 2, p);    // 快速幂 + 取模
}
// 也可用扩展欧几里得求逆元
long long x, y;
exgcd(a, p, x, y);
long long inv2 = (x % p + p) % p;
易错点:① 费马小定理要求模数为质数;② 逆元用于模除法;③ 结果可能为负,记得 (x%p+p)%p 转正。

9.4 矩阵与矩阵快速幂九级

① 矩阵的概念

矩阵是数按行列排成的表格,有加、乘等运算法则。矩阵乘法的规则:C[i][j] = Σ A[i][k] × B[k][j]。

② 矩阵快速幂(加速递推)

斐波那契 f(n) = f(n-1) + f(n-2) 可以写成矩阵形式,用矩阵快速幂在 O(log n) 内求第 n 项。

C++
// [[f(n)], [f(n-1)]] = [[1,1],[1,0]]^(n-1) × [[f(1)],[f(0)]]
using Mat = vector<vector<long long>>;
Mat mul(Mat A, Mat B){            // 矩阵乘法
    int n = A.size();
    Mat C(n, vector<long long>(n));
    for (int i = 0; i < n; i++)
        for (int k = 0; k < n; k++)
            if (A[i][k])
                for (int j = 0; j < n; j++)
                    C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
    return C;
}
Mat qpow(Mat base, long long e){  // 矩阵快速幂
    int n = base.size();
    Mat r(n, vector<long long>(n));
    for (int i = 0; i < n; i++) r[i][i] = 1;   // 单位矩阵
    while (e){ if (e & 1) r = mul(r, base); base = mul(base, base); e >>= 1; }
    return r;
}
易错点:① 矩阵乘法不满足交换律(AB ≠ BA);② 单位矩阵相当于数字 1;③ 矩阵快速幂用于线性递推的大规模加速。

9.5 高斯消元九级

① 高斯消元的思想

加减消元把线性方程组化为“阶梯形”,再回代求出每个未知数。本质就是“对方程组做合法的变换”。

C++
// 解 n 元线性方程组(增广矩阵 a[i][n] 是常数项)
double a[105][105];
bool gauss(int n){
    for (int col = 0, row = 0; col < n && row < n; col++){
        // 找这一列绝对值最大的行,换到当前行
        int mx = row;
        for (int i = row; i < n; i++)
            if (fabs(a[i][col]) > fabs(a[mx][col])) mx = i;
        if (fabs(a[mx][col]) < 1e-9) continue;   // 无解/无穷解
        swap(a[mx], a[row]);
        // 用当前行消去下面各行的这一列
        for (int i = row + 1; i < n; i++){
            double t = a[i][col] / a[row][col];
            for (int j = col; j <= n; j++) a[i][j] -= t * a[row][j];
        }
        row++;
    }
    // 回代求每个未知数
    return true;
}
易错点:① 选“主元”时取绝对值最大可减少误差;② 判断无解/无穷解看系数是否全 0;③ 注意浮点误差(用 1e-9 比较)。

9.6 容斥原理九级

① 容斥的思想

求“至少满足一个条件”的个数时,先把单个的加起来,再减去两两重叠的,再加回三三重叠的……(奇加偶减)。

C++
// 例:1~n 中能被 a 或 b 整除的数的个数
int cnt = n / a + n / b - n / lcm(a, b);
// 三个集合:|A∪B∪C| = |A|+|B|+|C| - |AB| - |AC| - |BC| + |ABC|
生活小例子:三张重叠的圆纸片

容斥原理想成三张圆纸片叠在一起

  • 直接数三张纸片的面积,重叠部分被数了两次
  • 所以要减去两两重叠的部分
  • 但三张纸片共同重叠的地方被减多了,要再加回来

一句话:加了减,减了加,把多算少算的都补平。

易错点:① “奇加偶减”是容斥的口诀;② 多个集合用状态压缩(二进制)枚举所有组合;③ 常与“求补集”(总数 - 反面)结合。

九级模拟题九级

选择题费马小定理求逆元 a⁻¹ mod p(p 为质数)等于?
A. a^(p-1)B. a^(p-2)C. a^pD. a^(p+1)
答案:B
a^(p-1) ≡ 1 (mod p),所以 a⁻¹ ≡ a^(p-2) (mod p)。
选择题容斥原理计算三个集合并集时,最后要加上哪个部分?
A. 三个集合的交集B. 两两交集C. 单个集合D. 什么都不加
答案:A
|A∪B∪C| = 单个和 - 两两交 + 三交集(奇加偶减)。
编程题用矩阵快速幂求斐波那契第 n 项。
答案:参考思路见解析
转移矩阵 M = [[1,1],[1,0]],答案 = M^(n-1) × [f(1), f(0)]ᵀ 的第一项。
把递推写成矩阵乘法,再用快速幂把 n 次乘法降到 log n 次矩阵乘法。
编程题用扩展欧几里得求 ax + by = gcd(a,b) 的一组解。
答案:参考思路见解析
递归 exgcd(b, a%b, y, x),然后 y -= (a/b)*x,出口 b==0 时 x=1,y=0。
扩展欧几里得是解同余方程和求逆元的基石。
10

十级 · 高级专题

平衡树 · 复杂 DP 优化 · 欧拉定理 · 中国剩余定理 · 线性基
能力目标:竞赛级专题核心:平衡树、CRT、线性基

10.1 平衡树(treap / splay)十级

① 平衡树的概念

普通二叉搜索树在最坏情况下会退化成链表(O(n))。平衡树通过旋转/随机优先级,让树保持平衡,保证插入、删除、查找都在 O(log n)

② Treap(树 + 堆)

Treap 给每个节点一个随机优先级,用堆的性质(父优先级 > 子)通过旋转保持平衡。它实现简单,是竞赛中最常用的平衡树之一。

C++
struct Node { int key, pri, cnt, sz; Node *l, *r; };
// 旋转:右旋 / 左旋,保持 BST 性质并修正堆性质
void rotate_right(Node *&p){
    Node *q = p->l;
    p->l = q->r;  q->r = p;  p = q;
    upd(p->r); upd(p);
}
// 插入:先按 BST 插入,若子节点优先级更高就旋转上来

③ Splay(伸展树)

Splay 每次访问(查找/插入/删除)都把节点旋转到根(伸展),近期访问的节点下次更快,均摊 O(log n)。

易错点:① 平衡树能 O(log n) 维护“第 k 小、排名、前驱后继”;② Treap 靠随机优先级、Splay 靠旋转到根;③ 考试/竞赛可直接用 std::set(红黑树)代替部分需求。

10.2 复杂 DP 的状态设计十级

① 状态设计的原则

  • 状态要无后效性:未来的决策只由当前状态决定;
  • 状态要不重不漏地覆盖所有情况;
  • 先用小数据手推/暴力验证状态定义对不对。

② 常见复杂状态

C++
// 例:区间 DP 状态 dp[i][j](区间)
// 例:状压 DP 状态 dp[S][u](集合 S + 当前点 u)
// 例:数位 DP 状态 dp[pos][limit][pre](数位 + 边界 + 前一位)
// 例:树形背包 dp[u][k](节点 u,容量 k)
int dp[105][105];   // 设计时想清楚每个维度代表什么
易错点:① 状态维度过少会漏解,过多会爆内存;② 转移时想“从哪些状态来”;③ 复杂 DP 建议先写暴力验证小数据。

10.3 DP 优化:斜率优化 / 决策单调性十级

① 决策单调性

当“最优决策点”随着状态增大而单调不降时,可以用分治优化单调队列,把一维 O(n²) 降到 O(n log n)。

② 斜率优化(凸包)

形如 dp[i] = min(dp[j] + (前缀和的差)² + 常数) 的转移,展开后是一次函数 y = kx + b,可以用单调队列维护下凸包优化到 O(n)。

C++
// 例:经典“玩具装箱”式转移(示意)
// dp[i] = min over j< i of (dp[j] + (sum[i]-sum[j]+i-j-1-L)²)
// 令 x = sum[j]+j, y = dp[j]+(sum[j]+j)²,则每步都是
// “用斜率 k 在凸包上求最小截距”,维护单调队列即可。
易错点:① 用之前先证明决策单调性或斜率单调;② 维护的凸包用单调队列 O(1) 进出;③ 这类题套路固定,多练几道模板就会。

10.4 欧拉定理与扩展欧拉定理十级

① 欧拉函数与欧拉定理

欧拉函数 φ(n):1~n 中与 n 互质的数的个数。若 gcd(a, n) = 1,则 a^φ(n) ≡ 1 (mod n)——这就是欧拉定理。当 n 为质数时 φ(n) = n-1,退化为费马小定理。

C++
int phi(int n){                 // 欧拉函数
    int r = n;
    for (int p = 2; p * p <= n; p++)
        if (n % p == 0){
            while (n % p == 0) n /= p;
            r -= r / p;           // r * (1 - 1/p)
        }
    if (n > 1) r -= r / n;
    return r;
}

② 扩展欧拉定理

当指数很大时:若 b ≥ φ(m),则 a^b ≡ a^(b mod φ(m) + φ(m)) (mod m)。用于“超大指数取模”问题。

易错点:① 欧拉定理要求 gcd(a, n) = 1;② 扩展欧拉定理处理指数超大(字符串读入的指数);③ φ(n) 的求法:质因数分解后套公式。

10.5 中国剩余定理(CRT)十级

① 中国剩余定理

同余方程组:x ≡ a₁ (mod m₁),x ≡ a₂ (mod m₂),……(各 m 两两互质)。解法:设 M = ∏mᵢ,对每个方程求 M/mᵢ 在模 mᵢ 下的逆元,组合起来。

C++
// x ≡ a[i] (mod m[i]),m 两两互质
long long crt(int n, long long a[], long long m[]){
    long long M = 1, x = 0;
    for (int i = 0; i < n; i++) M *= m[i];
    for (int i = 0; i < n; i++){
        long long Mi = M / m[i];
        long long inv = mod_inv(Mi, m[i]);   // 逆元
        x = (x + a[i] * Mi % M * inv % M) % M;
    }
    return (x % M + M) % M;
}
生活小例子:韩信的“隔墙算兵”

传说韩信数兵:3 个一组剩 2 个、5 个一组剩 3 个、7 个一组剩 2 个,一共多少人?

这就是中国剩余定理解决的“同余方程组”:用一个巧妙的方法,把每个条件的“剩余”分别放大再拼起来,最后求出一个同时满足所有条件的最小解。

易错点:① 要求模数 两两互质;② 需要会用扩展欧几里得/费马小定理求逆元;③ 结果记得转正(mod M 后加 M 再 mod)。

10.6 线性基十级

① 线性基的概念

线性基是一组“能代表原集合所有异或结果”的最简数集,用于求最大异或和、第 k 大异或和等问题。

C++
long long base[64];               // base[i] 最高位是 i 的数
void insert(long long x){         // 插入一个数
    for (int i = 60; i >= 0; i--)
        if (x >> i & 1){
            if (!base[i]){ base[i] = x; return; }
            x ^= base[i];
        }
}
long long max_xor(){              // 求最大异或和
    long long r = 0;
    for (int i = 60; i >= 0; i--)
        r = max(r, r ^ base[i]);
    return r;
}
易错点:① 线性基里的数互相异或能表示原集合的所有异或值;② 插入时从高位到低位,能放就放、放不下就异或消掉;③ 复杂度 O(log MAX)。

十级模拟题十级

选择题为什么平衡树比普通二叉搜索树更稳定?
A. 代码更短B. 能保证树高 O(log n)C. 不用递归D. 数据必须有序
答案:B
普通 BST 最坏退化成链(O(n)),平衡树通过旋转/随机优先级保证树高 O(log n)
选择题中国剩余定理解决什么问题?
A. 求最大公约数B. 解同余方程组C. 快速幂D. 判断素数
答案:B
中国剩余定理求解 x ≡ aᵢ (mod mᵢ) 的同余方程组(模数两两互质)。
编程题用线性基求一组数的最大异或和。
答案:参考思路见解析
把每个数 insert 进线性基,再从高位到低位依次 r = max(r, r ^ base[i])。
线性基能表示所有异或结果,贪心取最高位能得最大。
编程题用中国剩余定理求“3 个一数剩 2,5 个一数剩 3”的最小正整数。
答案:参考思路见解析
m = [3,5], a = [2,3],M=15,用扩展欧几里得求 Mi 的逆元,套 CRT 公式。
答案为 8(8 % 3 = 2,8 % 5 = 3)。