信息学竞赛没入门就出门
T_TLucas_Yin
·
·
算法·理论
其实我打算把从 C++ 基本语法到我能学到的所有算法全部写在这里,所以这可以当作一个笔记或教程。
不管你什么时候打开这篇文章,请抱有以下心态:
- 这篇文章已经停更了,别指望作者再往后写新的东西。
- 一切知识和代码的介绍都是根据作者的个人理解解释的,未必专业。作者很菜的,文章中可能出现各种疏漏或错误,发现了及时私信作者指出即可。
- 有些专有名字第一次出现时,铺垫的前置知识可能不足,导致初见的人不知道是什么意思。作者会尽量在后文补全彻底理解所需的前置知识。
- 这篇文章的介绍顺序可能和别的文章不大一样,但是作者认为最适合初学者理解的顺序
- 作者的文笔很烂也很啰嗦。
当然,适当期待一下也可以。
一些前置知识:
- 关于 OI 比赛的内容、规则、考察范围等基本信息。
- 计算机的使用,软件、浏览器以及网站的使用。
- 简单了解计算机内部运行、存储的逻辑。
- 认识二进制数字。
- 初中数学和部分高中数学。
第一章 学会写代码
(说实话这一部分很多底层原理作者也没有系统的学过,解释只是为了理解起来更方便,可能十分不专业)
OI 使用的编程语言是 C++。目前常用版本为 C++98 至 C++17。所以想要学 OI,要先学会用 C++ 写代码。
首先需要下载一个 Dev-C++ 编译器,它可以编译并运行你的代码。版本从 5.11 到 6.0+ 都可。这是操作方式最简单,最适合初学者的编译器。目前正式考试使用的编译器是 Dev 5.11。
熟练后还可以使用更强大的 VS code 编译器。
关于编译器的简单使用请自行学习。
计算机使用 C++ 代码的方式可以理解为:
- 先进行编译操作,即读懂代码。
- 然后运行,即按照代码给出的指令做一些事情。
我们认为计算机是不会出错的,如果运行结果和你预想的不一样,肯定是你的代码写得有问题。
代码的通用规则
对于任何单独成行的代码,后面必须加英文分号 ;,类似于写句子时最后要加句号。
一些特殊代码
注释
先介绍一种特殊的代码格式:注释代码。
以下是单行注释。编译器中 // 后面的内容会自动变成注释。
注释的范围是同一行注释符号右面的内容//在任意一行代码的末尾加"//"就可以写出注释
还有一种多行注释。编译器中 /* 和 */ 之间的内容会自动变成注释。
/*
可以像这样
一次性给很多整行代码注释
*/
注释对代码的功能不会产生任何影响。一般用来向代码的读者解释一些信息。
头文件的使用
C++ 中的一些语句被封装在了头文件里,一个头文件相当于一个拓展包。实现很多我们需要的功能都需要使用头文件中的语句。
头文件有很多个。想要使用某一个头文件中的功能,就要调用该头文件。一般把头文件的引用语句写在程序的最开始。
引用头文件的语句格式如下:
#include<iostream>
其中 iostream 是一个头文件的名字,换成别的名字,就可以引用别的头文件。一个程序可以同时引用多个头文件。
下面这串代码调用了绝大多数平时常用的头文件:
#include<iostream>//标准输入输出流,提供了输入和输出语句
#include<cstdio>//提供了另一种输入输出语句和指向文件的输入输出流
#include<cmath>//提供了一些数学函数
#include<algorithm>//主要提供了可以排序数组的sort函数
#include<cstring>//提供了针对char类型的一些函数
#include<string>//提供了string数据类型和一些string的函数
#include<stack>
#include<vector>
#include<queue>
#include<map>
#include<set>//上面这些头文件分别提供了STL库中的同名数据结构和与其相关函数的功能,这样的头文件还有很多
但如今我们通常不会一次性引用这么多头文件,而是这样写,
#include<bits/stdc++.h>
这个被称为万能头,它包含了上述所有头文件,以及除此以外的很多很多其他的头文件(事实上是标准 C++ 语句库中的所有头文件)。这样任何功能都可以随心所欲地用了,更加方便。
万能头也有一定的缺点,例如增加编译时长、引用后使用一些特殊字符串作为变量名时会报错等。不过使用通常不会给代码的执行带来很大影响。
命名空间的使用
可以认为命名空间将 C++ 的所有代码划分为不同区域,不同区域间的代码可以重名。为了同时使用不同空间的代码,要在代码前写它所属的命名空间。例如命名空间 std 中的输出语句:
std::cout<<"123";
如果在整个程序前引用命名空间,后面该命名空间内的语句就可以不用加前缀。引用语句这样写:
using namespace std;
同理,把 std 换成别的,可以使用别的命名空间。一份代码只能引用一个命名空间。
引用命名空间 std 后,上面的输出语句就可以直接写成:
cout<<"123";
当相似的代码堆叠得很多时,引用命名空间就可以省下很多力气(但引用命名空间并不是必须的)。
目前我见过的所有 OI 代码都只需要使用 std 命名空间。
用户还可以直接在代码中创建自己的命名空间,但这是更高级的语法,而且通常没必要。
主函数的定义
一个解答 OI 问题的程序,通常只由一个个函数构成。
函数可以看成一段被封装成一个整体的语句。一个程序中的定义或声明语句可以写在函数外面,而所有可执行的代码必须写在某一个函数里面。
函数可以由一行代码调用。每调用一次函数即按顺序执行该函数内的每一行代码一次。当然,调用函数的语句也应写在函数内。
这里先介绍一个特殊的函数:主函数。
任何程序都要有主函数。在运行程序时,电脑会自动调用一次主函数,从而启动你的程序。你的代码运行时永远从主函数的第一行开始,并依次执行以下的每一行代码,直到主函数的最末尾。
主函数的基本结构是这样写的:
int main(){
//一些代码
return 0;
}
return 0; 是主函数的返回代码。主函数中必须有这条代码且必须像这样写(不能把 0 换成别的数字),否则程序运行会出错。
由大括号 {} 包裹起来的部分即是主函数包括的代码。程序运行的过程就是依次执行这些代码的过程。
综上所述,我们可以总结出 C++ 程序的基本结构框架:
#include<bits/stdc++.h>
using namespace std;
int main(){
return 0;
}
通常任何程序都包含这样一个框架。
数据存储
数据的存储单位
首先要知道计算机用二进制存储数据。
一个二进制位(bit),简称位,即一个可以表示 0 或 1 两种状态的位置。一个二进制位可以用 1b 来表示。二进制位是计算机存储的最小单位。
$1024$ 个字节排列起来就成了一个更大的单位,用 $1KB$ 表示。即,$1KB=1024B$。
同理,还有:
$$1024KB=1MB$$
$$1024MB=1GB$$
$$1024GB=1TB$$
$$1024TB=1PB$$
$$1024PB=1EB$$
$$1024EB=1ZB$$
$$1024ZB=1YB$$
$$1024YB=1BB$$
$$1024BB=1NB$$
$$1024NB=1DB$$
$$...$$
于是用二进制位就能表示很多种状态,能存储很多内容了。
在程序中,不同的内容要用不同的数据类型来存储。C++ 中提供了一些基本数据类型。常见的基本数据类型如下:
- `char` 存储一个字符,占有 $1B$ 空间。
- `bool` 存储一个状态 $0$ 或 $1$,占有 $1B$ 空间。
- `int` 存储一个整数,占有 $4B$ 空间。
- `float` 存储一个实数,占有 $4B$ 空间。
- `long long` 存储一个整数,占有 $8B$ 空间。
- `double` 存储一个实数,占有 $8B$ 空间。
- `__int128` 存储一个整数,占有 $16B$ 空间。
- `long double` 存储一个实数,占有 $16B$ 空间。
一个二进制位可以表示两种状态。一个数据类型占多少二进制位的空间,就能表示多少二进制位所能表示的状态种类。例如,一个 `char` 类型就可以表示 $2^8=256$ 种状态。其中的状态形如 `00101011` 或 `10010101` 等等。特殊的是 `bool` 类型虽然占 $8b$,但只能存储 $0$ 或 $1$ 两种。
那么如何用只由 $0/1$ 组成的一个个字节表示整数、字符、实数等各种不同的含义呢?
#### 整数的存储
对于整数的存储,我们把该数据中由许多二进制位组成的一个“状态”直接看成一个二进制数,这个二进制数是几,就表示我们存储的整数是几。例如二进制数 $00010011$ 就表示 $19$ 这个数。
但是整数还有正负之分。所以在一个整数数据类型中,我们把最左边的二进制位独立出来,称作“符号位”。符号位上的数若是 $1$,就表示存储的这个数是负数;若是 $0$,则表示存储的这个数是正数。这样我们就只用二进制位的状态表示出了一个有符号整数。
但计算机中并不是直接存储像这样的数据来表示整数。我们把如上得到的有符号位的二进制数成为整型数据的“原码”,此外还有“反码”和“补码”。对于一个正数,该数的反码和补码都与原码相同;对于一个负数,该数的反码由原码除符号位之外的每一个二进制位取反得到,补码由反码 $+1$ 得到。
计算机整数类型中存储的是整数的补码。
例如……(这里作者懒得写例子了 QWQ)
一个 `int` 类型的数据有 $31$ 个可以表示数据的二进制位和一个符号位,由此我们可以推得,它可以存储一个值域在 $[-(2^{31}-1),2^{31}-1]$ 之间的整数。同理,一个 `long long` 类型的数据可以存储一个值域在 $[-(2^{63}-1),2^{63}-1]$ 的整数。
如果我们在 `int` 或 `long long` 类型前加上 `unsigned` 标签,即形成新的数据类型 `unsigned int` 或 `unsigned long long`。这两种数据类型中没有符号位,所以只能存储正整数,但存储数据的值域会变大,例如 `unsigned int` 可以存储值域在 $[0,2^{32}-1]$ 的整数。
#### 字符的存储
通常我们会把字符也映射为整数。即,规定每一个字符都用一个对应的整数来表示,使用时再根据整数和映射关系还原出最初的字符。这样就可以把任意字符也存储到电脑里了。
像这样的映射关系有很多种,在 C++ 中一般使用 ASCLL 表(读作阿斯科码表),它将常用的运算符号、数字、字母和其他特殊字符对应转换成了数字。

更强大的对应关系还有 Unicode 码,它的容量更大,实现了人类的绝大多数语言符号、数学符号、特殊符号转换为十六进制数字,只不过有的地方无法渲染全部这些字符。
`char` 类型前面也可以加 `unsigned` 标签,但由于 ASCLL 码没有负数,所以似乎没什么用。
#### 布尔类型
布尔值指只有真(`true`)、假(`false`)两种可能状态的数据。在计算机中分别用二进制位的状态 $0$、$1$ 表示。
`bool` 数据类型即存储布尔值的数据类型。在 `bool` 类型的数据中存储任何不为 $0$ 的值都会被转化为 $1$。
#### 实数的存储
实数的存储与整数类似,依旧是用二进制数表示原数,并把最靠前的二进制位分离出来做符号位。
`float` 和 `double` 数据类型虽然名义上是存储实数,但限于计算机运行逻辑,实际上只能存一个有限小数。存储时采用了“浮点数”的存储方法,即保留能存下的最多位有效数字,并根据需要移动小数点的位置。
例如,若实数类型内存储一个整数部分有 $1$ 位数,小数部分有 $114$ 位的小数,则它会保留整数和 $30$ 位小数。
若实数类型内存储一个整数部分有 $50$ 位数,小数部分有 $0$ 位的数,则它会保留整数部分的前 $31$ 位,后 $29$ 位全部省略为 $0$。
实际的存储方式似乎比这个更复杂,感兴趣可以自行学习。
根据实数存储类型的特殊性质,在一些情境下会使用其实现舍弃精度扩大值域的操作。
实数类型前也可以加 `unsigned` 标签构成无符号浮点数类型,与无符号整数类型相同,即去掉符号位。
### 数据容器与自定义函数的使用
我们已经了解了 C++ 中的基本存储类型。但我们还不会写代码来用这些数据类型建立真正可以存储数据的容器。下面来看看如何实现此功能。
#### 变量
变量是一种数据的容器。形象的说,一个变量就像是一个房子。房子里可以住一个数据。
每个变量都有一个对应的数据类型。变量内存储数据的类型必须与变量的类型相同(例如 `int` 类型的变量内只能存储整数)。
每个变量还有一个名字,方便我们区分不同的变量、调用变量以及使用变量运算。变量的名字有一定的命名规则:
- 只能由大小写字母、数字、下划线组成。
- 第一个字符不能是数字
##### 变量的定义
初始时程序里是没有任何变量的。要创建一个变量,需要写以下语句:
```cpp
int a;
```
这样即创建了一个名为 `a` 的整数类型变量。
也可以用同一条语句创建多个同类型变量。
```cpp
int a,b,__c,BIANliang111,e114_f514;
char zifu1,zifu2;
```
变量定义语句的位置影响了变量的适用范围。
- 若定义语句写在函数外,则该变量称为**全局变量**。在定义语句下面的任何一个函数内都可以使用。
- 若定义语句写在某对大括号 `{}` 内,则该变量称为 **局部变量**,只能在该大括号内使用。
若两个重名变量的适用范围有重合,则程序会发生编译错误,无法正常运行。为了避免程序混乱,我们尽量保证永不出现同名变量。
##### 数据的表示
一些数据最初不是存储在变量里的,我们有特定的方式直接表示它。
对于一个 `int` 类型的数字,直接写出来即可表示。如 `114514` 表示 $114514$ 这个数本身,也可在前面加负号。注意数字的大小要在 `int` 范围内,超出范围的数字可能会被识别成别的数字。
`long long` 类型的数字在数字后面加 `ll` 表示。如 `123456789101112ll` 表示这么一个很大的数。同理也不能超出 `long long` 的范围,超出范围的数字无法被识别,造成编译错误。
小数直接以浮点数的形式表示,如 `-12.8`,`25.551141` 等。同样注意数字不要太长。
一个字符用英文单引号括起来表示,如 `'a'` 表示字母 $a$。没有单引号的字符会被识别成其他含义(如关键字、变量名等)而不是它本身。不要括一些奇怪的字符如汉字等,否则会编译错误。一些特殊的字符有特殊的表达方式,如 `'\n'` 表示换行,`'\r'` 表示回车,`'\\'` 表示反斜杠。
一串字符称为一个**字符串**,用英文双引号括起来表示。如 `"Hello World!"`。
可以使用变量的名字表示存储在该变量中的数据。
##### 赋值
变量是用来存储数据的。对变量赋值即把一个数据存到变量里。
```cpp
int a;
a=5;
```
上面的代码定义了变量 `a` 并往里面存了一个整数 `5`。
我们也可以在定义变量时直接给它赋一个初始值。
```cpp
int a=5,b=3;
char c='A';
```
变量可以直接赋值为任何对应类型的数据,包括另一个变量。
```cpp
int a=3,b=a;//此时a和b里都是3
```
若对一个已经有值的变量再次赋值,新值会直接覆盖旧值。
变量赋值语句本身也可以作为一个值,等于被赋值的变量里的新值。所以赋值时可以连等(**但定义语句中不行**),从右往左执行。
```cpp
int a,b,c;
a=b=c=6;//c赋值为6,b赋值为c,a赋值为b
```
##### 数据的运算与类型转换
C++ 内置了四则运算,我们可以直接用`+`、`-`、`*`、`/` 四种符号写式子。
我们目前只考虑运算符号的两侧为同类型数据,那么运算的结果也是同类型数据。
```cpp
int a=5,b=3,c;
c=a+b;//此时c=8
a=b*c//此时a=24
```
默认的运算优先级为 `*`=`/`>`+`>`-`,式子里要先算加减需在外面套括号,**多层嵌套都用小括号**
```cpp
a=((5+3)*(20-4)/3)+6/4
```
(介绍完所有语法后讲时空复杂度)