算法竞赛介绍
了解 ICPC、CCPC、天梯赛、蓝桥杯与百度之星的特点,找到适合自己的参赛方向。
认识常见竞赛 →知识不是堆得越多越好,重要的是按照正确的顺序真正掌握。
了解 ICPC、CCPC、天梯赛、蓝桥杯与百度之星的特点,找到适合自己的参赛方向。
认识常见竞赛 →从代码框架、输入输出到数组、结构体和函数,每个知识点都有逐行解释、运行结果与练习。
进入 14 章教程 →基础算法、搜索、数据结构、动态规划与图论,按难度逐步建立完整知识网络。
查看算法目录 →按照 C++ 教程的学习顺序完成洛谷入门题,把刚学会的语法真正变成解题能力。
打开语法题单 →教程帮助你理解知识,题目帮助你真正掌握知识。可以根据当前阶段选择一个主平台持续练习。
Online Judge,在线评测系统。提交代码后,系统会自动编译、运行并判断答案是否正确。
中文题库与社区内容丰富,题目难度标记直观,适合从基础语法、算法模板逐步练习。
包含在线编程、竞赛题目与企业笔试训练,国内高校算法赛事和练习活动较为丰富。
国际竞技编程平台,拥有频繁的线上比赛、分级竞赛和 Rating 系统,题目强调思维与实现速度。
日本竞技编程平台,题目风格严谨、难度梯度稳定,Beginner Contest 很适合进行定期训练。
初学阶段可以先在洛谷或牛客完成基础题;熟悉常用算法后,再定期参加 Codeforces Div. 3/4 或 AtCoder Beginner Contest。不要同时追逐太多平台,持续练习比平台数量更重要。
天才就是百分之一的灵感,加上百分之九十九的汗水。— Thomas Edison
写给第一次接触编程的你。我们会从一份完整的代码框架开始,把每个符号、每行代码和运行结果讲清楚。
从输入、输出和完整代码框架开始,不要求提前掌握任何编程知识。
每学完一个例子,都自己输入、运行并修改数据,观察程序结果。
学完对应语法后进入刷题训练,用协会 OJ 和练习题巩固知识。
第一次学习建议按左侧编号依次完成,已经有基础也可以直接选择章节。
编程环境就是我们写代码、检查错误、把代码翻译成程序并运行它的一套工具。Windows 初学者可以在下面两种方案中任选一种。
编辑器、编译器和调试工具可以一起安装,配置步骤少,适合想尽快写出第一段程序的同学。
MinGW64 的 Windows 安装版。hello.cpp。VS Code 本身是代码编辑器,不自带 C++ 编译器,需要额外安装编译工具。
C/C++ 扩展。g++ --version。hello.cpp。g++.exe。如果你现在只想学习语法,选小熊猫 C++ 最省心;如果你已经熟悉文件路径、终端和扩展,选 VS Code。两者写出的 C++ 代码没有区别。
新建 hello.cpp,复制下面的代码并运行。看到黑色运行窗口中出现 Hello, World! 就说明环境可用。
#include <iostream>
using namespace std;
int main() {
cout << "Hello, World!";
return 0;
}Hello, World!
先检查文件名是否以 .cpp 结尾、代码中的标点是否为英文符号、每条语句末尾是否有分号。VS Code 用户还要确认安装的是 C++ 编译器,而不只是 C/C++ 扩展。
一份竞赛程序就像一张固定格式的答题纸。刚开始不必背下来,先理解每一部分负责什么。
1#include <bits/stdc++.h>
2using namespace std;
3
4int main() {
5 // 解题代码写在这里
6 return 0;
7}#include 引入工具bits/stdc++.h 会一次性引入算法竞赛常用的标准库,让我们能够使用输入输出、数组容器和排序等工具。
using namespace std;让我们可以直接写 cout,不用每次都写完整的 std::cout。
int main() 是程序入口程序运行时会先找到 main 函数,再从左花括号开始逐行执行。
return 0; 正常结束告诉操作系统程序顺利执行完毕。在竞赛代码中通常保留这一行。
() 圆括号通常放条件或参数;{} 花括号包住一段代码;<> 尖括号在这里包住头文件名。代码必须使用英文输入法下的 ;、()、{} 和双引号。中文的 ;、( ) 无法通过编译。
cout 用来把文字或计算结果输出到屏幕。符号 << 可以理解为“把右边的内容送到屏幕”。
cout << "Hello World" << '\n';Hello World
双引号中的内容叫做字符串,会原样输出。'\n' 表示换到下一行,它虽然由两个可见字符组成,但在 C++ 中代表一个换行符。
cout << "答案是:" << 3 + 5 << '\n';
cout << "A" << " " << "B";答案是:8 A B
"A"字符串,用双引号,可以包含多个字符。'A'单个字符,用单引号,只能表示一个字符。endl也能换行,但竞赛大量输出时通常使用更快的 '\n'。请输出两行文字:第一行是你的名字,第二行是 I love C++!。注意两行之间需要换行。
算法题的数据会通过标准输入交给程序。cin 负责读取数据,>> 可以理解为“把输入送进右边的变量”。
int a, b; // 准备两个整数变量
cin >> a >> b; // 依次读入 a 和 b
cout << a + b << '\n';12 8
20
空格和换行都可以分隔输入数据。因此输入写成第一行 12、第二行 8,程序仍能正确读取。
不能直接写 cin >> a; 而没有提前告诉 C++ 变量 a 的类型。这里的 int a; 就是在声明一个整数变量。
输入长和宽两个整数,输出它们的乘积。例如输入 4 6,应该输出 24。
变量可以想象成带名字的盒子:盒子中保存数据,类型决定这个盒子能装什么、能装多大。
int age = 15; // 整数
long long score = 10000000000LL; // 大整数
double pi = 3.14159; // 小数
char grade = 'A'; // 单个字符
bool passed = true; // 真或假
string name = "Alice"; // 字符串int普通整数绝对值不超过约 21 亿long long很大的整数乘法、总和或答案可能很大时优先考虑double带小数的数据平均数、几何计算等char / string单字符 / 一段文字字符题、字符串题int x = 5;
x = 8; // 把盒子里的 5 换成 8
x = x + 2; // 读取原来的 8,加 2,再存回 x
cout << x;10
sizeof 可以查看一种类型或一个变量占用多少字节(Byte)。它的结果是整数,写类型时通常要加括号,写变量时括号可以省略。
int x = 10;
double price = 3.5;
cout << sizeof(int) << ' ';
cout << sizeof x << ' ';
cout << sizeof(price);4 4 8
多数竞赛环境中 int 占 4 字节、long long 和 double 占 8 字节、char 占 1 字节。真正需要确认时,以当前程序的 sizeof 结果为准。
100000 * 100000 已超出 int 范围。写成 1LL * 100000 * 100000,或把变量声明为 long long。
运算符让程序进行数学计算、比较大小并组合条件。它们是后面判断和循环的基础。
+ - * /四则运算整数除法会舍去小数部分:7 / 2 得到 3。%取余数7 % 2 得到 1,常用来判断奇偶。== !=相等 / 不相等比较结果是 true 或 false。> < >= <=大小比较注意“大于等于”写作 >=。&& || !并且 / 或者 / 取反用来组合多个判断条件。++ --增加 1 / 减少 1i++ 等价于 i = i + 1。int a = 7, b = 2;
cout << a + b << ' '; // 加法:9
cout << a - b << ' '; // 减法:5
cout << a * b << ' '; // 乘法:14
cout << a / b << ' '; // 整数除法:3
cout << a * 1.0 / b; // 小数除法:3.59 5 14 3 3.5
int x = 10;
x += 3; // x = x + 3,现在是 13
x *= 2; // x = x * 2,现在是 26
x--; // x = x - 1,现在是 25
cout << x;25
+=、-=、*=、/= 和 %= 都是在原值上计算后再存回变量。单独使用时,++x 与 x++ 都会让 x 加 1;初学阶段不要把它们塞进复杂表达式。
int n;
cin >> n;
cout << (n % 2 == 0);如果输入 8,表达式 8 % 2 == 0 成立,输出 1;输入 7 时条件不成立,输出 0。
int age;
cin >> age;
bool isTeen = age >= 13 && age <= 18;
bool isFree = age < 6 || age >= 65;
cout << isTeen << ' ' << isFree << ' ';
cout << !isFree;15
1 0 1
&& 要求两边都成立,|| 只要一边成立,! 会把真假反过来。C++ 默认用 1 表示 true、0 表示 false。
* / % 通常先于 + - 计算,例如 2 + 3 * 4 得到 14。培训和比赛中都建议用括号主动表达意图:(2 + 3) * 4 得到 20。
= 和 == 完全不同x = 5 是把 5 存入 x;x == 5 才是在询问“x 是否等于 5”。这是条件判断中最常见的笔误。
当条件成立时执行一段代码,不成立时执行另一段代码,这就是分支结构。
int score;
cin >> score;
if (score >= 90) {
cout << "优秀";
} else if (score >= 60) {
cout << "及格";
} else {
cout << "继续努力";
}else if 会按从上到下的顺序检查。一旦某个条件成立并执行,对应的整组判断就结束了,因此更严格的条件通常写在前面。
if (age >= 13 && age <= 18) {
cout << "青少年";
}当变量只需要和几个固定值比较时,可以使用 switch。每个 case 表示一种情况,default 处理其他情况。
int day;
cin >> day;
switch (day) {
case 1:
cout << "Monday";
break;
case 2:
cout << "Tuesday";
break;
default:
cout << "Other day";
}Tuesday
执行某个 case 后,break 会离开整个 switch。如果漏写,程序会继续执行后面的 case,这种现象称为“贯穿”。
输入一个整数。大于 0 输出 positive,等于 0 输出 zero,小于 0 输出 negative。
当你知道一段代码需要执行多少次,for 循环通常最合适。它把“从哪里开始、何时继续、每次怎样变化”写在同一行。
for (int i = 1; i <= 5; i++)for (int i = 1; i <= 5; i++) {
cout << i << ' ';
}1 2 3 4 5
int n, sum = 0;
cin >> n;
for (int i = 1; i <= n; i++) {
sum += i; // 等价于 sum = sum + i
}
cout << sum;5
15
break 会立刻结束整个循环;continue 只跳过当前这一轮,随后进入下一轮。它们既可以用在 for 中,也可以用在 while 中。
for (int i = 1; i <= 10; i++) {
if (i == 8) break; // 到 8 时结束循环
if (i % 2 == 0) continue; // 偶数跳过输出
cout << i << ' ';
}1 3 5 7
i 为 2、4、6执行 continue,本轮后面的输出语句不再执行。
i 为 8执行 break,整个循环结束,9 和 10 也不会再处理。
外层循环每执行一轮,内层循环都会从头完整执行。常用于打印图形、枚举行列、处理二维数组。
for (int row = 1; row <= 3; row++) {
for (int col = 1; col <= 4; col++) {
cout << '*';
}
cout << '\n';
}**** **** ****
row 控制行外层共执行 3 轮,因此输出 3 行。
col 控制列每一行中内层执行 4 轮,因此每行输出 4 个星号。
循环体共执行 3 × 4 次若两层分别循环 n 次和 m 次,总次数就是 n × m。
在内层循环执行 break,外层循环仍会继续下一轮。如果需要同时结束两层,通常使用布尔标记,或把这段逻辑封装进函数后用 return。
i <= n 会包含 n,循环 n 次;i < n 不包含 n,只到 n - 1。写循环前先在纸上明确第一个值和最后一个值。
输入 n,输出 1 到 n 之间的所有偶数。你可以让 i 每次加 1 后判断,也可以思考怎样让 i 每次直接加 2。
当循环次数不确定,但“继续执行的条件”很清楚时,使用 while。每一轮开始前,程序都会先检查括号中的条件。
int n;
cin >> n;
while (n > 0) {
cout << n % 10 << ' ';
n /= 10;
}4 3 2 1
n % 10取得十进制个位数。
n /= 10删掉个位数,让 n 逐步变为 123、12、1、0。
n > 0当 n 变成 0 时条件不成立,循环结束。
下面的程序不断读入整数:遇到负数就跳过,遇到 0 就结束,其他数累加。循环次数由输入内容决定,所以很适合使用 while。
int sum = 0;
while (true) {
int x;
cin >> x;
if (x == 0) break; // 结束整个 while
if (x < 0) continue; // 跳过负数,继续读下一个
sum += x;
}
cout << sum;5 -2 7 0
12
while (true) 为什么能结束?它本身是无限循环,但读到 0 时会执行 break。这种“先循环、满足条件再退出”的写法在处理未知数量的输入时很常见。
int x;
do {
cin >> x;
} while (x < 0); // 别漏掉最后的分号while 是先判断再执行,可能一次也不执行;do while 是先执行再判断,因此循环体至少执行一次。
循环体必须让条件逐渐接近“不成立”。如果漏掉 n /= 10,n 永远不变,程序就会一直运行。遇到这种情况可以手动停止程序。
在手动维护循环变量的 while 中,如果先执行 continue,后面的 i++ 就会被跳过,程序可能永远停在同一个值。可以把更新语句放到判断之前,或改用 for。
for遍历 1 到 n、重复固定次数、遍历数组。while不断读入直到遇到 0、数字拆位、次数事先未知。如果要保存 100 个整数,没必要创建 100 个不同名字的变量。数组用一个名字管理一组类型相同的数据。
长度为 5 的数组,下标是 0、1、2、3、4。最后一个元素是 a[4],访问 a[5] 已经越界。
int n;
cin >> n;
int a[1005];
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int answer = a[0];
for (int i = 1; i < n; i++) {
answer = max(answer, a[i]);
}
cout << answer;5 8 3 6 1 9
9
int a[1005] 提前准备 1005 个位置。实际使用前 n 个位置,也就是 a[0] 到 a[n-1]。
int a[5] = {8, 3, 6, 1, 9};
int zero[100] = {}; // 所有元素初始化为 0
for (int i = 0; i < 5; i++) {
cout << a[i] << ' ';
}局部数组如果没有初始化,里面的值是不确定的,不能直接拿来累加或比较。计数数组常用 {} 将所有位置清零。
int a[3][4] 可以看成 3 行 4 列的表格。访问时先写行下标,再写列下标,例如 a[1][2] 表示第 2 行第 3 列。
int n, m;
cin >> n >> m;
int a[105][105];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> a[i][j];
}
}
int sum = 0;
for (int j = 0; j < m; j++) {
sum += a[0][j]; // 累加第 1 行
}
cout << sum;2 3 1 2 3 4 5 6
6
i = 0 ... n-1外层循环枚举每一行。
j = 0 ... m-1内层循环枚举这一行的每一列。
a[i][j]表示第 i + 1 行、第 j + 1 列的元素。
输入 n 行 m 列的整数矩阵,使用两层循环找出最大值,并输出它所在的行号和列号。
数组越界不一定立即报错,但会读写不属于它的内存,导致答案错误甚至程序崩溃。竞赛中应始终检查循环边界。
string 用来保存一串字符。它很像一个字符数组,同样可以通过从 0 开始的下标访问每个字符。
string s;
cin >> s;
cout << s.size() << '\n';
cout << s[0] << '\n';
for (char c : s) {
cout << c << ' ';
}4 c c o d e
cin >> s读取到空格就停止,适合单个单词。getline(cin, s)读取完整一行,能够包含空格。s.size()取得字符串长度,结果是字符数量。bool ok = true;
for (int i = 0; i < s.size(); i++) {
if (s[i] != s[s.size() - 1 - i]) {
ok = false;
}
}第 i 个字符与倒数第 i 个字符比较。注意最后一个字符下标是 s.size() - 1。
结构体可以把多个不同类型、但彼此相关的数据组合成一个整体。竞赛中常用它表示学生、坐标、边、区间或题目记录。
struct Student {
string name;
int score;
int age;
}; // 结构体定义结束后需要分号Student我们创建的新类型名称,以后可以像 int 一样用它声明变量。
成员变量name、score 和 age 描述一名学生的不同信息。
末尾分号结构体右花括号后必须写分号,这是非常常见的编译错误。
Student a;
a.name = "Alice";
a.score = 95;
a.age = 16;
cout << a.name << ' ' << a.score;Alice 95
点号 . 表示访问结构体中的某个成员。例如 a.score 就是“学生 a 的分数”。
int n;
cin >> n;
Student students[105];
for (int i = 0; i < n; i++) {
cin >> students[i].name >> students[i].score;
}
int best = 0;
for (int i = 1; i < n; i++) {
if (students[i].score > students[best].score) {
best = i;
}
}
cout << students[best].name;3 Alice 95 Bob 88 Carol 97
Carol
定义结构体 Point,包含整数成员 x 和 y。输入两个点,输出它们横坐标之和与纵坐标之和。
函数把一段可以重复使用的逻辑封装起来。它可以接收参数,完成计算,再把结果返回给调用者。
int返回值类型maximum函数名称(int a, int b)参数列表int maximum(int a, int b) {
if (a > b) {
return a;
}
return b;
}
int main() {
int answer = maximum(7, 12);
cout << answer; // 输出 12
return 0;
}int f()函数会返回一个整数,需要写 return。bool f()函数返回 true 或 false,常用于判断。void f()函数不返回结果,只执行某些操作。调用函数时,括号中传入的是实参;函数定义中接收数据的 a、b 是形参。普通参数会复制一份数据,修改形参不会影响外面的变量。
void addOne(int x) {
x++;
}
int main() {
int n = 5;
addOne(n);
cout << n;
}5
在参数类型后加 & 表示引用。此时参数是外部变量的别名,对它的修改会保留下来。
void swapValue(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
int x = 3, y = 8;
swapValue(x, y);
cout << x << ' ' << y;8 3
int x值传递:复制一份,函数内修改不影响原变量。int &x引用传递:直接操作原变量,可以把修改带出函数。const string &s只读引用:避免复制较大的数据,同时禁止函数修改它。void printPositive(int x) {
if (x <= 0) {
return; // 立即结束函数,不返回具体数值
}
cout << x;
}void 函数不返回计算结果,但仍可以用单独的 return; 提前结束。一个有返回值的函数则要保证每条可能执行的路径都能返回正确类型的值。
C++ 需要先知道函数长什么样,才能调用它。可以把完整函数写在 main 前,也可以先写函数声明,再把实现放到 main 后。
int square(int x); // 函数声明:末尾有分号
int main() {
cout << square(6);
}
int square(int x) { // 函数实现
return x * x;
}在函数的花括号中声明的变量,离开函数后就不能再访问。不同函数可以拥有同名的局部变量,它们互不影响。
编写 bool isPrime(int n),判断 n 是否为质数。在 main 中读入一个整数,根据函数返回值输出 Yes 或 No。
STL 是 C++ 标准库提供的一套现成工具。入门阶段先掌握 vector、sort,以及 max、min、swap 等常用函数。
vector<int> a;
a.push_back(8); // 在末尾加入 8
a.push_back(3); // 在末尾加入 3
a.push_back(6); // 在末尾加入 6
cout << a.size(); // 输出 3int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
sort(a.begin(), a.end());
for (int x : a) cout << x << ' ';5 8 3 6 1 9
1 3 6 8 9
vector<int> a(n)创建长度为 n 的整数 vector。
a.begin()指向第一个元素的位置。
a.end()指向最后一个元素后面的位置,sort 排序范围左闭右开。
max 取较大值,min 取较小值,swap 交换两个变量的值。它们能让常见操作写得更直接。
int a = 8, b = 3;
cout << max(a, b) << ' '; // 较大值
cout << min(a, b) << '\n'; // 较小值
swap(a, b);
cout << a << ' ' << b;8 3 3 8
使用标准头文件时,max、min 位于 <algorithm>,swap 可由 <utility> 提供。竞赛中常用的 #include <bits/stdc++.h> 已经包含这些头文件。
例如 max(3, 4LL) 的一个参数是 int、另一个是 long long,可能无法编译。可以写成 max(3LL, 4LL),让两边类型保持一致。
reverse翻转顺序reverse(a.begin(), a.end()) 将整个 vector 前后颠倒。count统计出现次数count(a.begin(), a.end(), 3) 统计 3 出现了几次。find寻找元素找不到时返回 a.end(),使用前要先判断。max_element寻找最大元素返回元素所在位置,前面加 * 取得数值。min_element寻找最小元素用法与 max_element 相同。fill批量赋值fill(a.begin(), a.end(), 0) 把所有元素改成 0。vector<int> a = {8, 3, 6, 3, 9};
cout << count(a.begin(), a.end(), 3) << '\n';
cout << *max_element(a.begin(), a.end()) << '\n';
reverse(a.begin(), a.end());
for (int x : a) cout << x << ' ';8 3 6 3 9
2 9 9 3 6 3 8
如果 vector 为空,max_element 会返回 a.end(),此时不能在前面加 *。应先用 a.empty() 判断容器中是否有元素。
abs(x)取得绝对值,例如 abs(-7) 得到 7。sqrt(x)计算平方根,返回小数,例如 sqrt(16) 得到 4。pow(a, b)计算 a 的 b 次方;整数幂在竞赛中常用循环计算,避免浮点误差。读入 n 个整数,输出其中的最大值、最小值和某个指定数字出现的次数,然后将整个序列逆序输出。
下一步不要急着学习更多语法。先用这些知识完成求和、最大值、统计、模拟等基础题目,让输入—计算—输出的过程变得熟练。
教程负责把知识讲明白,题目负责让知识真正属于你。这里按照 C++ 语法课程的顺序整理入门题目,学完一章就能立刻练习。
洛谷题可手动标记完成,协会 OJ 题需判题通过。当前积分:0
登录后填写你在协会 OJ 使用的用户名,再用一次性绑定码完成身份验证。
熟悉完整代码框架、cout、cin 和最基本的计算。
练习整数、小数、字符以及常用算术运算。
根据不同输入选择不同执行路径,练习逻辑表达式。
让程序重复工作,完成统计、累加和过程模拟。
保存一批数据,并按下标访问、统计或修改它们。
拆分重复逻辑、组织复合数据,并使用标准库完成排序。
这些题目由协会 Hydro 自动判题,AC 后本站会自动点亮并计入积分。
按照题目给出的顺序维护状态,逐步还原整个过程。
每一步选择当前最优方案,并思考它为什么能得到全局最优。
利用单调性不断缩小答案范围。
预处理累计信息,快速回答区间查询。
这一专题将在算法学习补充对应课程后开放。
这一专题将在算法学习补充对应课程后开放。
这一专题将在算法学习补充对应课程后开放。
按一维顺序定义状态,并从已知状态递推答案。
在容量限制下选择物品,理解状态与枚举顺序。
按区间长度递推,合并更小区间的结果。
使用邻接表表示点和边,为遍历与图算法做准备。
按距离分层遍历图,求无权图最短路。
根据边权特点选择合适算法,求点之间的最短距离。
以最小总代价连接全部顶点。
语法告诉我们代码怎样写,算法告诉我们问题应该怎样解决。这里会从读懂题目开始,逐步建立复杂度意识,再进入动态规划、图论和数论。
读题、复杂度、模拟、贪心、二分、前缀和与差分。
当前学习重点学习状态设计、转移方程、递推顺序与空间优化。
路线已规划从图的存储与遍历开始,进入最短路和生成树。
路线已规划掌握整除、质数、快速幂和同余等竞赛工具。
路线已规划先建立读题和复杂度意识,再学习具体算法会更稳。
这一阶段先建立正确的解题流程和复杂度意识,再学习模拟、贪心、二分、前缀和与差分。
先把题目描述翻译成清晰的输入、计算和输出任务。
模拟题不要求先套一个复杂算法,而是把题目描述中的状态、规则和操作顺序准确翻译成程序。关键是先写清“现在保存什么”“每一步怎样变化”“什么时候结束”。
找出过程中会发生变化的数据,例如当前位置、当前时间、剩余数量或字符串下标。
按照题意把一次操作拆成固定顺序,避免更新先后颠倒。
重点检查第一步、最后一步、空输入、越界以及日期进位等特殊情况。
在纸上逐步记录状态,与程序每一步的结果进行对照。
从位置 0 出发,依次读取字符。遇到 L 向左移动一格,遇到 R 向右移动一格,最后输出位置。
string commands;
cin >> commands;
int position = 0;
for (char command : commands) {
if (command == 'L') position--;
else if (command == 'R') position++;
}
cout << position << '\n';规则越多,越应该先列出状态和操作顺序。更新多个变量时,要确认后一步使用的是更新前还是更新后的值。
输入一个合法日期,输出它的下一天。分别考虑月末、年末以及闰年二月。
贪心算法在每一步选择当前看来最优的方案,并且不回头修改。真正的难点不是写出选择,而是说明这个局部选择为什么不会让最终答案变差。
先确认要最小化、最大化或尽可能多地完成什么。
常见策略包括按结束时间、代价、收益或某个比值排序。
用很小的数据尝试推翻策略,能被反例推翻的就不是正确贪心。
证明任意最优解都能调整为包含当前选择,且答案不会变差。
把区间按照结束位置从小到大排序,每次选择第一个与已选区间不冲突的区间。越早结束,就为后面的区间留下越多空间。
sort(intervals.begin(), intervals.end(), [](auto a, auto b) {
return a.second < b.second;
});
int answer = 0, lastEnd = -INF;
for (auto [left, right] : intervals) {
if (left >= lastEnd) {
answer++;
lastEnd = right;
}
}排序把候选方案放到一个有利顺序中,但“排序后取第一个”仍然需要正确性说明。
给出若干活动的开始与结束时间,选择数量最多且互不重叠的一组活动。
一道算法题通常由题目描述、输入格式、输出格式、数据范围和样例组成。很多错误不是算法不会,而是没有把输入、输出或数据范围读准确。
说明需要解决什么问题。先把故事背景翻译成一句明确任务,例如“求区间和”或“寻找第一个满足条件的位置”。
说明程序会读到哪些数据、每个数据的含义以及排列顺序。变量声明和循环次数都来自这里。
说明最终要输出什么。空格、换行、保留小数位数以及输出顺序都可能影响评测结果。
决定数据类型和算法复杂度。看到 n ≤ 10^5,通常就不能使用 O(n²)。
给定两个整数 a 和 b,输出它们的和。
一行两个整数 a, b,以空格分隔。
输出一个整数,表示 a + b。
|a|, |b| ≤ 10⁹
#include <bits/stdc++.h>
using namespace std;
int main() {
long long a, b;
cin >> a >> b;
cout << a + b << '\n';
return 0;
}12 8
20
时间复杂度描述输入规模 n 增大时,程序操作次数增长得有多快。它不直接等于运行秒数,而是帮助我们在写代码之前判断算法是否可能超时。
int answer = a[0] + a[n - 1];for (int i = 0; i < n; i++)
sum += a[i];for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
check(i, j);n ≤ 20指数级、状态枚举可以尝试枚举许多组合n ≤ 500O(n³)三层循环需要谨慎n ≤ 5,000O(n²)大约数千万次操作n ≤ 10⁵O(n log n) 或 O(n)排序、二分、线性遍历n ≤ 10⁷O(n)通常只能做少量遍历实际速度还受到常数、语言、内存访问和评测机影响。竞赛中常用“约一秒执行一亿次简单操作”进行粗略判断,但不能把它当成精确保证。
空间复杂度描述算法额外使用的存储空间怎样随输入规模增长。数组、容器、递归调用栈都会占用内存;超过题目的内存限制会得到 MLE。
无论 n 多大,只使用固定数量的变量。
int sum, maximum;保存 n 个同类型元素,空间随 n 线性增长。
vector<int> a(n);保存 n × n 个元素,n 增大时内存增长很快。
int grid[n][n];常见的 int 通常占 4 字节,long long 通常占 8 字节。估算数组内存时,可以使用:
int a[1'000'000] 大约占用 1,000,000 × 4 B ≈ 3.8 MB
如果算法直接在输入数组上修改数据,只使用少量额外变量,它的额外空间可能是 O(1)。分析时要区分“输入本身占用的空间”和“算法额外申请的空间”。
二分查找利用数据的有序性或答案的单调性,每次检查中间位置并排除一半范围,把线性查找的 O(n) 降低为 O(log n)。
例如数组已经从小到大排序;或者某个条件在一段范围内为 false,之后全部为 true。
第 1 次:left = 0, right = 6, mid = 3
发现 a[3] = 13,目标找到。
bool exists(const vector<int>& a, int target) {
int left = 0;
int right = (int)a.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (a[mid] == target) return true;
if (a[mid] < target) left = mid + 1;
else right = mid - 1;
}
return false;
}left <= right闭区间 [left, right] 还有元素时继续查找。
left + (right-left)/2与 (left+right)/2 含义相同,但能够避免两数相加溢出。
更新时越过 mid已经检查过 mid,所以使用 mid+1 或 mid-1,否则可能死循环。
区间定义混乱。写代码前先决定使用闭区间 [left, right] 还是左闭右开区间 [left, right),循环条件和更新方式必须始终与它保持一致。
如果需要反复询问数组某个区间的元素总和,每次从左到右重新累加会很慢。前缀和先进行一次 O(n) 预处理,之后每次区间查询只需 O(1)。
定义 prefix[i] 表示原数组前 i 个元素之和。为了让公式更整齐,令 prefix[0] = 0。
把第 i 个元素加入前 i-1 个元素的总和。
前 r 个元素之和,减去 l 之前的所有元素。
求数组第 2 到第 4 个元素之和:
prefix[4] - prefix[1] = 9 - 3 = 6int n, q;
cin >> n >> q;
vector<long long> prefix(n + 1, 0);
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
prefix[i] = prefix[i - 1] + x;
}
while (q--) {
int left, right;
cin >> left >> right;
cout << prefix[right] - prefix[left - 1] << '\n';
}即使数组中的每个元素都能放进 int,许多元素相加后的前缀和也可能超过 int 范围。只要总和可能很大,就使用 long long。
二维前缀和用于快速计算矩阵中的矩形区域和。它是一维前缀和的自然推广:prefix[i][j] 表示从左上角 (1, 1) 到右下角 (i, j) 的整个矩形元素之和。
例如地图区域统计、二维棋盘计数、图片像素区域求和等。
上方矩形与左方矩形都包含左上角重叠区域,因此相加后必须把 prefix[i-1][j-1] 减去一次。
a[i][j]prefix[i-1][j]prefix[i][j-1]prefix[i-1][j-1]设矩形左上角为 (x1, y1),右下角为 (x2, y2),包含边界。先取右下角的大前缀矩形,再减去上方和左方多余区域,最后把被重复减去的左上区域加回来。
int n, m, q;
cin >> n >> m >> q;
vector<vector<long long>> prefix(
n + 1, vector<long long>(m + 1, 0)
);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
long long value;
cin >> value;
prefix[i][j] = value
+ prefix[i - 1][j]
+ prefix[i][j - 1]
- prefix[i - 1][j - 1];
}
}
while (q--) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
long long answer = prefix[x2][y2]
- prefix[x1 - 1][y2]
- prefix[x2][y1 - 1]
+ prefix[x1 - 1][y1 - 1];
cout << answer << '\n';
}建议让矩阵下标从 1 开始,并额外保留第 0 行和第 0 列为 0。这样查询贴着上边界或左边界的矩形时,不需要额外分类讨论。
如果要对数组的许多区间整体加上一个数,逐个修改区间中的每个元素会很慢。差分只修改区间的两个边界,每次操作是 O(1),最后再用一次前缀和还原整个数组。
令 diff[i] = a[i] - a[i - 1],并规定 a[0] = 0。差分记录的是“当前位置相对前一个位置变化了多少”。
最后一个 0 是额外预留的 diff[n + 1],用于处理右端点恰好为 n 的区间修改。它不属于原数组。
记录相邻元素之间的变化量。
对 diff 求前缀和,就能重新得到 a。
表示从位置 l 起,后面的元素都多出 k。
让这次增加只影响到 r,不继续传到后面。
原数组为 3 1 4 1 5,将区间 [2, 4] 全部加 2:
int n, m;
cin >> n >> m;
vector<long long> diff(n + 2, 0);
long long previous = 0;
for (int i = 1; i <= n; i++) {
long long value;
cin >> value;
diff[i] = value - previous;
previous = value;
}
while (m--) {
int left, right;
long long value;
cin >> left >> right >> value;
diff[left] += value;
diff[right + 1] -= value;
}
for (int i = 1; i <= n; i++) {
diff[i] += diff[i - 1];
cout << diff[i] << ' ';
}5 2 3 1 4 1 5 2 4 2 1 3 -1
2 2 5 3 5
前缀和擅长“数组不变,多次查询区间和”。差分擅长“多次修改区间,最后得到整个数组”。两者关系差分的前缀和是原数组,原数组的相邻差是差分。如果数组使用 1 到 n 的下标,差分数组至少开到 n + 1。代码中常写 vector<long long> diff(n + 2),避免访问 diff[right + 1] 时越界。
如果每次修改后都要立刻查询当前区间和,不能每次重新还原数组;这类在线问题通常需要树状数组或线段树。
二维差分可以把矩形 (x1, y1) 到 (x2, y2) 内的所有元素同时加上 value。一次修改只需要改变四个角,最后对差分矩阵求二维前缀和即可还原。
左上角开始产生影响,下方和右方分别取消影响,右下角因为被减了两次,需要再加回来一次。
void addRectangle(int x1, int y1,
int x2, int y2, long long value) {
diff[x1][y1] += value;
diff[x2 + 1][y1] -= value;
diff[x1][y2 + 1] -= value;
diff[x2 + 1][y2 + 1] += value;
}所有矩形修改结束后,按照从上到下、从左到右的顺序计算:diff[i][j] += diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1]。此时 diff[i][j] 就是最终矩阵中的值。
输入长度为 n 的数组和 m 次操作,每次把区间 [l, r] 加上 k。使用差分输出所有操作完成后的数组,并尝试与逐个修改元素的做法比较运行次数。
现在你已经能够根据数据范围判断复杂度,使用二分缩小查找范围,用前缀和优化区间查询,并用差分优化批量区间修改。
会从状态定义和转移方程开始,用具体例子解释记忆化、递推顺序与空间优化。
将从图的存储和遍历开始,再逐步进入最短路、最小生成树、拓扑排序与连通性问题。
将介绍整除、最大公约数、质数筛法、快速幂、同余与组合计数等竞赛常用知识。
不同竞赛在参赛对象、比赛方式和题目难度上各有特点。了解它们不是为了盲目追逐奖项,而是为了给自己的学习找到一个清晰目标。
在有限时间里,将问题转化为算法,再写成正确、高效的程序。
赛事规则每年可能调整,正式报名和参赛资格请始终以当届官方通知为准。
全球高校程序设计竞赛。经典赛制是 3 名学生组成一队,共用一台计算机,在有限时间内解决多道算法题。
面向中国高校学生的高水平程序设计竞赛,强调算法设计、逻辑推理、编程实现和团队合作。
重点考查基础程序设计能力以及数据结构与算法应用能力。选手独立作答,同时通过团体成绩体现学校整体水平。
覆盖软件与电子等多个类别。软件赛中常见 C/C++ 程序设计方向,通常按照不同组别和阶段进行选拔。
由百度发起的程序设计赛事,重视基础算法、数据结构、编程实现以及分析和解决问题的能力。历史赛事多采用在线评测与逐轮晋级形式。
这两项赛事是大学算法竞赛中最具代表性的团队赛。下面用一场比赛从开始到结束的过程,把赛制讲清楚。
面向全球高校的多层级团队程序设计竞赛,强调算法能力、临场决策和三人协作。
三名队员会同时阅读题目,但只有一人能够操作电脑。有人负责推导算法,有人检查边界和样例,有人把已经确认的思路写成代码。角色不是固定职业,而会根据题目和队员特长不断切换。
队伍首先按照通过题目数量排名;解题数相同时,总用时更少的队伍靠前。每道通过题的用时从开赛时刻计算到首次通过,之前未通过的提交通常会增加罚时。
A 题在第 40 分钟通过,之前有 2 次错误提交:
40 + 2 × 20 = 80 分钟未解决的题目通常不计入总用时常见路径是先加入学校集训队,经过校内选拔后代表学校参加相应区域赛事,优秀队伍继续向更高阶段晋级。具体赛区划分、资格和晋级办法应查看当赛季官方规则。
面向中国高校学生的年度性高水平赛事,赛题风格和现场形式与 ICPC 团队赛高度相近。
CCPC 现场赛的典型规则是三名正式队员组成一队,由一名高校教师担任教练。比赛采用上机编程、机器实时评测和实时排名,队伍共同使用一台比赛机器。
总决赛规则示例中,比赛时长为 5 小时,题目通常为英文描述;通过一道题后,赛场会升起对应颜色的气球,这也是现场赛非常有辨识度的传统。
排名首先比较解题数量;数量相同时再比较总用时。每道已通过题目的用时,从比赛开始计算到首次正确提交,之前的错误提交会带来额外罚时。
两者都非常重视算法、数据结构、代码正确性和团队配合,也都常采用三人一机的现场赛形式。主要区别在于赛事组织体系和覆盖范围:ICPC 是国际赛事体系,CCPC 则重点服务中国高校程序设计竞赛。
备赛知识高度互通,因此高校集训队通常会用同一套训练体系准备两项赛事。
与三人共用一台电脑不同,选手独立操作和提交。题目通常具有明显梯度,既考查基础语法和读题速度,也逐步覆盖数据结构与算法。它适合用来检验一所学校不同水平选手的整体程序设计能力。
备赛重点:基础题正确率、分段得分、时间分配软件类竞赛包含 C/C++ 程序设计等方向,并根据参赛对象设置相应组别。相比团队现场赛,个人赛更直接地检验独立读题、实现和调试能力,常被初学者用作阶段性目标。
备赛重点:语法熟练度、模拟枚举、常用算法历史上的程序设计大赛采用在线评测,重点考查基础算法、数据结构和程序实现能力。它为学习者提供了不同于高校系列赛的题目风格;是否举办、参赛资格和晋级方式需要关注当年官方公告。
备赛重点:综合算法能力、代码速度、线上赛经验榜单按照学习积分排序,同分时完成题目更多的学习者优先。
排行榜展示注册时填写的昵称与姓名,同时统计洛谷手动完成积分和协会 OJ 判题积分。
校验并发布 Hydro 题目、配置积分与训练分类,查看学生最近的真实评测记录。