把变量想成不同容量的杯子:
int是"整杯水"(只能装整数,装不下 3.14);double是大水杯(能装小数,更精确);char是小茶杯(只装一个字符);- 声明类型就是先挑好杯子,再往里倒水——所以 C++ 用变量前必须说清类型!
依据《青少年软件编程(C/C++)等级考试标准(2025 年修订版)》整理。全册共十大等级,从 C++ 编程基础入门到竞赛级专题逐级递进。每个知识点均配有详细讲解、C++ 代码示例、易错点提示,并配模拟题讲解;难懂处附适合小学生、初中生的生活化例子。
《青少年软件编程(C/C++)等级考试标准(2025 年修订版)》将 C++ 由低到高分为一级至十级,适用年龄 8 周岁(建议 10 周岁)以上。考试由考试系统集成的编程环境完成作答,以实践应用能力为主。
十级内容呈现明显的螺旋递进关系:基础语法 → 函数与算法 → 数据结构与动态规划 → 图论与数论 → 竞赛级专题。
C++ 是一门需要先编译、再运行的编程语言。常见开发环境有 Dev-C++、Code::Blocks 等。写好的代码要先“翻译”成电脑能懂的机器语言(这一步叫编译),翻译成功才能运行。
每个 C++ 程序几乎都有固定的“骨架”。下面的代码会在屏幕上输出 Hello World:
#include <iostream> // 头文件:引入输入输出的功能
using namespace std; // 使用标准命名空间
int main(){ // 主函数:程序从这里开始执行
cout << "Hello World" << endl; // 输出并换行
return 0; // 告诉系统程序正常结束
}main,不能写成 mian;③ 头文件用 #include 且不加分号。变量就是给数据起一个名字、在内存里占一块地方用来存值。C++ 是强类型语言——使用变量前必须先声明类型。
| 类型 | 占位 | 含义 | 示例 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| i | n | t | ||||||||||
| 4 | 字 | 节 | ||||||||||
| 整 | 数 | |||||||||||
| i | n | t | a | g | e | = | 1 | 0 | ; |
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 是小茶杯(只装一个字符);int x = 3.9; 会截断成 3;③ 变量名不能以数字开头,不能用 int、main 等保留字。cout << 内容 用于输出,<< 像“把数据推向屏幕”,endl 表示换行。
cin >> 变量 用于从键盘读取数据存入变量,>> 像“把输入推进变量”。
#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;
}把 cin 和 cout 想成两个人:
cin >> a 像有人问路,你告诉他把信息“塞”给变量 a(箭头指向变量,信息流进去);cout << a 像你对着喇叭喊话,把 a 里存的值“喊”到屏幕上(箭头指向屏幕,信息流出去);>> 和 << 方向写反会报错;② cin 读字符串时遇空格就停(想读整行要用 getline);③ 末尾记得 return 0;。| 运算符 | 含义 | 示例 | 结果 | |
|---|---|---|---|---|
| + | ||||
| 加 | ||||
| 7 | + | 3 | ||
| 1 | 0 |
int 相除结果是整除!5 / 2 = 2,想要 2.5 必须先转成 double:(double)5 / 2。#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,结果可能有细微误差。程序按从上到下、一句一句执行的顺序,就是顺序结构——像做菜按菜谱一步一步来。
#include <iostream>
using namespace std;
int main(){
int a, b;
cout << "请输入两个整数:";
cin >> a >> b;
int sum = a + b; // 处理:求和
cout << "和是:" << sum << endl; // 输出
return 0;
}| 类别 | 符号 | 含义 | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 关 | 系 | ||||||||||||||||||
| < | > | < | = | > | = | = | = | ! | = | ||||||||||
| 小 | 于 | / | 大 | 于 | / | 不 | 大 | 于 | / | 不 | 小 | 于 | / | 等 | 于 | / | 不 | 等 | 于 |
int score = 85;
if (score >= 60) {
cout << "及格" << endl;
} else {
cout << "不及格" << endl;
}
// 三目运算符:条件 ? 结果1 : 结果2
cout << (score >= 60 ? "pass" : "fail") << endl;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 加到 100
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
cout << sum << endl; // 5050int n = 12345, cnt = 0;
while (n > 0) { // 不断取个位
cnt++;
n /= 10;
}
cout << "位数:" << cnt << endl; // 5int x;
do {
cout << "请输入一个正数:";
cin >> x;
} while (x <= 0); // 直到输入正数才停把循环想成体育课跑圈:
for 像老师规定“跑 5 圈”——次数已知,用 for;while 像“一直跑,直到下课铃响”——看条件;do-while 像“先跑一圈再说”——不管怎样至少跑一圈。口诀:知道几圈用 for,看情况用 while,先跑再判用 do-while。
i <= 100 和 i < 100 差一次;③ for 的变量 i 只在循环内有效。以下例题贴合一级考纲(环境、变量、运算、分支、循环)设计:
#include <iostream>。int a = 7, b = 2;,a / b 的值是?for (int i = 0; i < 5; i++) cout << "你好";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 是奇数。竞赛/考试中常用 freopen 把输入输出重定向到文件:程序从 in.txt 读,结果写到 out.txt。
#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;
}#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 混用可能出错。不同类型参与运算时会自动转换(如 int 转 double),也可以强制转换:(类型)表达式。
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 码)。条件里再套条件叫嵌套;用 else if 可以写多档判断。
int score = 85;
if (score >= 90) cout << "优秀";
else if (score >= 80) cout << "良好";
else if (score >= 60) cout << "及格";
else cout << "不及格";int a, b; cin >> a >> b;
if (a == b) cout << "相等";
else {
if (a > b) cout << "a 大";
else cout << "b 大";
}else 总是和最近的 if 配对;② 多档判断用 else if 优于连续 if(一旦命中就跳过后面)。循环里面再套循环,外层循环每走一步,内层循环完整走一遍。
// 打印直角三角形
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= i; j++) {
cout << "*";
}
cout << endl;
}
/* 输出:
*
**
***
****
***** */// 九九乘法表
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
cout << j << "*" << i << "=" << j*i << "\t";
}
cout << endl;
}把嵌套循环想成排值日表:外层是“星期几”,内层是“第几节课”,星期一要把每节课都排一遍,星期二再排一遍……外层转一次,内层转一圈。
所以“打印 5 行 × 每行 5 个星号”就是外层管行、内层管列。
数组是一排同类型的变量,用下标访问,下标从 0 开始。
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;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 号柜放东西;int a[10]; 后,下面哪个下标是合法的?*?for(i=0;i<3;i++) for(j=0;j<4;j++) cout<<"*";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。
用 g[i][j] 存第 i 个同学第 j 门课成绩,外层循环 i 对每个同学,内层循环 j 累加 g[i][j]。二维数组常用“外层管行、内层管列”的双层循环。
函数是一段可以反复调用的代码块,由“返回类型 + 函数名 + 参数 + 函数体”组成。
// 返回两数中较大者
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;void 函数不需要 return;② 函数必须先声明/定义再调用;③ return 后面的语句不会执行。默认是值传递:把变量的值复制一份传给函数,函数里改参数不影响原变量。
加 & 是引用传递:把变量本身传给函数,函数里改它,原变量也变。
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) 用值传递交换不了,因为只换了副本。数组作参数时自动按“引用”效果传递(能改原数组)。递归就是函数调用自己。必须有递归出口(停止条件),否则会无限递归导致崩溃。
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 个”,再一个个传回来。先一路问下去,到出口,再一路算回来,就是递归。
注意:没有出口会一直问下去 = 死循环,程序会崩溃!
C++ 里字符串可以用 char 数组存,末尾自动带 \0 表示结束。
char s[20] = "hello"; // 实际占 6 个位置(含 \0)
strlen(s); // 长度 5
strcmp(s, "hello"); // 0(相等)
strcpy(s, "world"); // 把 s 改成 world#include <string>
string s = "abc";
s.length(); // 3
s += "d"; // "abcd"
s.substr(1, 2); // "bc"(从下标 1 取 2 个)
s.find("bc"); // 返回位置 1strcmp 相等返回 0,不是 1;③ string 用 + 拼接、用 == 比较更方便。#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,算整数幂可能不精确。模拟法就是“照着题意一步一步做”,把题目描述的过程用代码原样模拟一遍。
// 模拟:小智每天存 1 元、第 2 天存 2 元……问 n 天后共存多少
int n = 10, total = 0;
for (int day = 1; day <= n; day++)
total += day; // 每天按规则加
cout << total << endl; // 55把模拟法想成照着说明书做实验:说明书写“第一步加 1 滴,第二步加 2 滴,第三步……”,你就一步一步照做,绝不跳步。程序也一样——题目说怎么做,代码就怎么写。
关键是别自作聪明,先把过程老老实实模拟出来再说。
枚举法就是把所有可能的情况一个一个试,找出满足条件的。
// 水仙花数:三位数,各位立方和等于它本身
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
}把枚举想成钥匙不见了,挨个抽屉翻:
枚举的两个要求:范围不漏、判断条件正确。
x 的值是?void f(int a){ a = 99; } int x = 5; f(x);&。fib(6) 的值是?(fib 为斐波那契,fib(1)=fib(2)=1)for(n=100;n<=999;n++){ int a=n/100, c=n%10; if(a==c) cout<<n<<" "; }三位数回文:百位 == 个位。用 /100 取百位、%10 取个位。int sum(int n){ if(n<=1) return n; return n + sum(n-1); }递归出口:n<=1 时返回 n;否则返回 n 加上 sum(n-1)。指针就是存“地址”的变量。&变量 取地址,*指针 通过地址访问它指向的变量。
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 = 99 是“进屋把小明改成 99”,a 当然就变了。指针的威力:函数可以透过指针修改外面的变量(前面 swap 也可以用指针实现)。
& 取地址、* 解引用,别混淆;③ 空指针 nullptr 不能解引用。结构体把多个不同类型的数据打包成一种新类型,方便管理。
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;
}类在结构体基础上,还能包含函数(方法)和访问权限(public/private)。
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。| 进制 | 基数 | 数字 | 例子 | ||||||
|---|---|---|---|---|---|---|---|---|---|
| 二 | 进 | 制 | |||||||
| 2 | |||||||||
| 0 | 1 | ||||||||
| 1 | 0 | 1 | 0 | ₂ | = | 1 | 0 |
int n = 13;
string bin = "";
while (n > 0) {
bin = char('0' + n % 2) + bin; // 余数往前拼
n /= 2;
}
cout << bin << endl; // 1101cout << hex << 16 << endl; // 10(十六进制)
cout << oct << 8 << endl; // 10(八进制)
cout << dec << 10 << endl; // 10(十进制)把二进制想成4 盏灯(亮=1,灭=0),灯的“分量”从左到右是 8、4、2、1:
| 符号 | 含义 | 例子 | 结果 | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| & | ||||||||||||||
| 按 | 位 | 与 | ||||||||||||
| 5 | & | 3 | ( | 1 | 0 | 1 | & | 0 | 1 | 1 | ) | |||
| 1 |
// 判断奇偶:看最低位
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 只对整数有效。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 << " ";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]);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]);
}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 很大时慢;③ 桶排序要求数据范围小。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);
}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];
}sort(a, a+n) 更省事。数字太大(超过 long long 范围)时,用数组一位一位存,像列竖式一样逐位计算。
// 高精度加法: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;
}把高精度想成列竖式做加法:
电脑里也是一样:把大数拆成一格一格(一位一位),逐位相加、处理进位。
int a = 5, *p = &a;,执行 *p = 10; 后 a 的值是?6 & 3 的结果是?struct St{ string name; int score; }; St a[3]; 读入后,用一个变量记录最高分的下标,最后输出。结构体把“姓名+成绩”打包,用循环比较 score 找最大。计算 a^b,如果一个个乘要 b 次;快速幂利用 b 的二进制分解,把次数降到 O(log b)。
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 每次要平方。递推是从已知的初始项出发,用循环从前往后一步步推出后面的项。
// 斐波那契: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 级台阶有几种走法?
先算小的,一步步推大的,就是递推!
贪心就是每一步都选当前看起来最优的做法,希望最后得到全局最优。
// 找零钱:用最少的硬币凑出 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;把贪心想成从一筐苹果里挑出最大的:每次只看眼前,选当前最大(最大的苹果)——只要标准选对,这样最后往往就是全局最优。
但注意:贪心不是万能的!有时眼前最优会让后面更糟(比如钱不够找零的情况)。能用贪心的问题,都要能证明“每步最优 = 全局最优”。
pre[i] 表示前 i 个数的和。有了它,区间 [l, r] 的和 = pre[r] - pre[l-1],一次就算出任意区间和。
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 天累计 就行!
这就是前缀和的威力:先把累计算好,任意区间一减就出来。
pre[r] - pre[l-1];③ 算二维前缀和要注意容斥(加左上减两块)。差分是前缀和的“逆运算”。多次给区间 [l, r] 整体加同一个数,用差分能一次遍历完成。
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 盆,边走边累计标记,就知道每盆该浇几壶了。
很多次区间操作,一次遍历搞定,这就是差分的妙处。
r+1 处 -v,否则会“越界累加”;③ 差分常与前缀和配合使用。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 的数字,你来猜:每次猜中间,老师答“大了/小了”,立刻砍掉一半范围。最多 7 次必中(2⁷=128>100)。
从 1 挨个猜要 100 次,二分只要 7 次——这就是二分快的秘密!
注意前提:数字必须排好序。
mid = (l + r) / 2 防止溢出;③ 二分答案常用于“求最大最小值”类问题。用两个指针在数组上移动来高效解决问题,常见“相向”和“同向(快慢)”两种。
// 例:有序数组中找两数和等于 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--; // 和大了,右边左移
}STL(标准模板库)提供很多现成的容器,不用自己造轮子。
#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 用 push_back 追加;② set 自动去重且有序;③ map 用下标访问,不存在的键会自动插入默认值。a[0..999](有序)中查找,最多比较多少次?pre 中,区间 [3, 6] 的和应该用哪个式子?按结束时间从小到大排序,依次选择“结束最早且不与上一个冲突”的活动。经典贪心:先按结束时间排序,能选就选。
qpow(2, 30),用 while 循环:b 的二进制位为 1 时累乘,底数每轮平方,b 右移。次数从 30 降到约 5 次循环,这就是快速幂。
栈只能在一端(栈顶)操作:push 入栈、pop 出栈、top 看栈顶。后进先出。
#include <stack>
stack<int> st;
st.push(1); st.push(2); st.push(3);
st.top(); // 3
st.pop(); // 弹掉 3
st.size(); // 2队列从队尾进、队首出:push 入队、pop 出队、front 看队首。先进先出。
#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>;③ 括号匹配、表达式求值常用栈,广度搜索常用队列。链表由节点组成,每个节点存数据 + 指向下一个节点的指针。插入删除快,但查找慢。
struct Node { int val; Node *next; };
// 在头节点后面插入新节点
void insert(Node *&head, int x){
Node *p = new Node;
p->val = x;
p->next = head;
head = p;
}
// 遍历链表
void print(Node *p){
while (p){ cout << p->val << " "; p = p->next; }
}把链表想成小朋友手拉手:每个人都记住“我右手边是谁”(next 指针)。
插一个新同学,只要让他“抓住两边的手”就行——不用像数组那样把后面人都挪位!
new 分配;② 插入删除要小心指针顺序(先接新节点再改旧节点);③ 双向链表删除节点要知道前后两个指针。深度优先搜索:从起点出发,一条路走到黑,走不通再退回来换一条(回溯)。
// 例:全排列 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想成走迷宫:
广度优先搜索:一层一层向外扩展,先访问离起点近的,用队列实现。BFS 能找到最短路径(无权图)。
#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想成石子扔进水里荡开的波纹:
口诀:DFS 用栈(一条道走到底),BFS 用队列(一圈圈扩散)。
剪枝就是在搜索过程中,提前判断这条路不可能出解,直接放弃,不去走,从而大幅加速。
把剪枝想成翻字典查词:要找 “zebra”,你不会从第 1 页挨页翻,而是直接翻到后半本 Z 区——把不可能的部分整个跳过。
搜索也一样:发现这条路不可能有解,就整条放弃,别傻傻走到底。剪枝剪得好,程序快十倍都不止。
动态规划(DP)把大问题拆成小问题,用状态表示“某个阶段的最优值”,并通过状态转移方程从小问题推出大问题。
// 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 是“从前往后填表”,不重复计算,更快。
有 n 件物品,每件有重量 w 和价值 v,背包容量 c,每件最多拿 1 件,求能装的最大价值。
// 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 背包想成往行李箱装玩具去旅行:
dp[j] 记录“容量 j 时的最优价值”,逐个玩具决定“装 or 不装”。
区间 DP 以区间 [i, j] 为状态,通常枚举区间长度从小到大,再枚举分割点合并。
// 石子合并:合并相邻石子堆的最小代价
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));
}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; // 最小公倍数
}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 的倍数
}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;
}
}
}排列:从 n 个中选 m 个排顺序,A(n,m) = n×(n-1)×…×(n-m+1)。
组合:从 n 个中选 m 个不管顺序,C(n,m) = A(n,m) / m!。
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;
}看关键词:有职位/有顺序 → 排列;只是选人 → 组合;分步完成 → 相乘。
C(n,m) = C(n, n-m) 简化。dfs(k) 表示已选 k 个数,用 used[] 标记,for 尝试每个未用数字,递归下一层,回溯时恢复 used。经典回溯模板:标记→递归→恢复。
dp[j] 倒序遍历:for j=c; j>=w[i]; j-- dp[j]=max(dp[j], dp[j-w[i]]+v[i]);容量倒着遍历保证每件物品只取一次。
树是“一对多”的非线性结构:有唯一根节点,每个节点可以有若干孩子,没有环(像一棵倒着长的树)。
vector<int> children[1005]; // 邻接表存树:children[父亲] = 孩子们
int n, root;
void dfs(int u){ // 先序遍历:先访问自己,再访问孩子
cout << u << " ";
for (int v : children[u])
dfs(v);
}把树想成家族谱系图:爷爷是根,下面分爸爸、叔叔,再下面分堂兄弟姐妹……一直往下分。
遍历树就像挨家挨户拜访:可以先拜访自己、再去孩子家(先序),也可以先把孩子家都走完、最后回自己(后序)。
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 << " ";
}把三种遍历想成拜访家族亲戚的顺序,根=爷爷:
口诀:看“根”在哪个位置:前=先根、中=中间、后=最后。
复杂贪心往往需要先排序、再依次贪心,或需要证明贪心策略。常见模型:区间选点、任务调度、背包的贪心近似等。
// 例:活动安排——选最多互不冲突的活动
// 先按结束时间排序
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。关键仍是:想清楚状态、转移、初始值。
// 例:数字三角形最大路径和
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];图由顶点和边组成,边可以有方向(有向图)和权值(带权图)。
// 邻接矩阵(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,权值 wbool vis[1005];
void dfs(int u){ // DFS 遍历
vis[u] = true;
for (auto [v, w] : adj[u])
if (!vis[v]) dfs(v);
}对有向无环图(DAG)排一个顺序,使所有边都从“前”指向“后”。做法:每次找入度为 0 的点输出并删除其出边。
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”的怪圈,就是有环,无法排序!
// 用优先队列(堆)优化
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});
}
}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想成找从家到学校最短的路:
在带权无向图中,选 n-1 条边把 n 个点连通,且总权值最小,就是最小生成树。
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++;
}
}哈希把大范围数据映射到小范围下标,实现 O(1) 查找。不同数据映射到同一位置叫冲突,要解决冲突。
// 字符串哈希:把字符串变成一个整数
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) 查找。先序第一个是根;在中序里找到根,左边是左子树、右边是右子树;递归左右再输出根。先序+中序可唯一还原二叉树,后序=左+右+根。
倍增用“2 的幂次跳”加速:先预处理 2^k 步的信息,再按二进制位组合跳跃,把 O(n) 变成 O(log n)。
// 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]);
}并查集快速判断两个元素是否在同一集合,并合并两个集合。核心是“找根”+“路径压缩”。
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 就是“一路往上问,问出族长是谁”;路径压缩就是:问出族长后,顺手让大家都直接记住族长,下次问就快了。
find 用递归+路径压缩;③ 合并前先 find 出根,防止环。树状数组(BIT)支持 单点修改 + 区间求和,都是 O(log n),代码比线段树短。
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;③ 求逆序对也常用树状数组。线段树把数组按区间分块存到二叉树节点里,支持区间查询、区间修改(O(log n)),比树状数组更通用。
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; }
// 下传懒标记……
}Trie 树把字符串按公共前缀共享存储,适合单词统计、前缀匹配。
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];
}// 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 倒过来输出就是欧拉回路在文本串中找模式串,KMP 利用 next 数组让模式串“聪明地滑动”,避免重复比较,复杂度 O(n+m)。
// 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 数组就是“提前记好的滑轨刻度”——每次失配该滑到哪,早就算好了,特别快。
在树上做 DP:状态放在“节点”上,先算孩子再算父亲(后序遍历)。
// 例:没有上司的舞会——选父亲就不能选孩子
// 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,孩子不能选
}
}用二进制位表示“选了哪些元素”的状态,适合 n 较小(n ≤ 20)的集合类问题。
// 例:旅行商问题(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]);S >> i & 1 判断元素;③ 状压只适合 n 小(1<<n 太大就爆内存)。add(i,v) 向上累加,sum(i) 向下累加,区间和 = sum(r)-sum(l-1)。核心 lowbit(x)=x&(-x)。树状数组代码短、速度快,是最常用的数据结构之一。
强连通:有向图中,两点能互相到达。最大互相到达的点集叫强连通分量。Tarjan 算法用 DFS + 时间戳 + 栈 求出所有 SCC,然后缩点成 DAG。
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;
}
}
}low[v] >= dfn[u](非根);③ 缩点后图变为 DAG,可做拓扑/DP。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 互质时存在)模意义下,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)。
// 用快速幂求逆元: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 转正。矩阵是数按行列排成的表格,有加、乘等运算法则。矩阵乘法的规则:C[i][j] = Σ A[i][k] × B[k][j]。
斐波那契 f(n) = f(n-1) + f(n-2) 可以写成矩阵形式,用矩阵快速幂在 O(log n) 内求第 n 项。
// [[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;
}用加减消元把线性方程组化为“阶梯形”,再回代求出每个未知数。本质就是“对方程组做合法的变换”。
// 解 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;
}求“至少满足一个条件”的个数时,先把单个的加起来,再减去两两重叠的,再加回三三重叠的……(奇加偶减)。
// 例: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 为质数)等于?转移矩阵 M = [[1,1],[1,0]],答案 = M^(n-1) × [f(1), f(0)]ᵀ 的第一项。把递推写成矩阵乘法,再用快速幂把 n 次乘法降到 log n 次矩阵乘法。
递归 exgcd(b, a%b, y, x),然后 y -= (a/b)*x,出口 b==0 时 x=1,y=0。扩展欧几里得是解同余方程和求逆元的基石。
普通二叉搜索树在最坏情况下会退化成链表(O(n))。平衡树通过旋转/随机优先级,让树保持平衡,保证插入、删除、查找都在 O(log n)。
Treap 给每个节点一个随机优先级,用堆的性质(父优先级 > 子)通过旋转保持平衡。它实现简单,是竞赛中最常用的平衡树之一。
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 每次访问(查找/插入/删除)都把节点旋转到根(伸展),近期访问的节点下次更快,均摊 O(log n)。
std::set(红黑树)代替部分需求。// 例:区间 DP 状态 dp[i][j](区间)
// 例:状压 DP 状态 dp[S][u](集合 S + 当前点 u)
// 例:数位 DP 状态 dp[pos][limit][pre](数位 + 边界 + 前一位)
// 例:树形背包 dp[u][k](节点 u,容量 k)
int dp[105][105]; // 设计时想清楚每个维度代表什么当“最优决策点”随着状态增大而单调不降时,可以用分治优化或单调队列,把一维 O(n²) 降到 O(n log n)。
形如 dp[i] = min(dp[j] + (前缀和的差)² + 常数) 的转移,展开后是一次函数 y = kx + b,可以用单调队列维护下凸包优化到 O(n)。
// 例:经典“玩具装箱”式转移(示意)
// 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 在凸包上求最小截距”,维护单调队列即可。欧拉函数 φ(n):1~n 中与 n 互质的数的个数。若 gcd(a, n) = 1,则 a^φ(n) ≡ 1 (mod n)——这就是欧拉定理。当 n 为质数时 φ(n) = n-1,退化为费马小定理。
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)。用于“超大指数取模”问题。
解同余方程组:x ≡ a₁ (mod m₁),x ≡ a₂ (mod m₂),……(各 m 两两互质)。解法:设 M = ∏mᵢ,对每个方程求 M/mᵢ 在模 mᵢ 下的逆元,组合起来。
// 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 个,一共多少人?
这就是中国剩余定理解决的“同余方程组”:用一个巧妙的方法,把每个条件的“剩余”分别放大再拼起来,最后求出一个同时满足所有条件的最小解。
线性基是一组“能代表原集合所有异或结果”的最简数集,用于求最大异或和、第 k 大异或和等问题。
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;
}把每个数 insert 进线性基,再从高位到低位依次 r = max(r, r ^ base[i])。线性基能表示所有异或结果,贪心取最高位能得最大。
m = [3,5], a = [2,3],M=15,用扩展欧几里得求 Mi 的逆元,套 CRT 公式。答案为 8(8 % 3 = 2,8 % 5 = 3)。