各位未来的算法大佬们,大家好!👋 是不是刚听说 OI(信息学奥林匹克竞赛)时,以为是什么歪门邪道?其实非也非也,这玩意儿全称是信息学奥林匹克竞赛,说白了就是用代码解决数学和逻辑问题的 "脑力奥运会"🏆。今天咱就从最基础的语法开始,一步步爬向算法入门的门槛,保证全程无废话、多笑点、代码接地气,变量名绝不搞 "bad_apples [apple_number]" 这种花里胡哨的操作,主打一个 "b [n] 就能解决" 的朴实无华~
第一章:OI 入门先搭台 ------ 环境配置像开黑前装游戏
在敲代码前,得先有个 "战场" 吧?就像打游戏要先装客户端,OIer 的第一站就是配置编程环境。目前主流的有 Dev-C++、Code::Blocks、VS Code,新手建议从 Dev-C++ 入手,轻便如 "手机小游戏"📱,不像 VS Code 那样需要装一堆插件,省事儿!
1.1 安装 Dev-C++:三步搞定,比泡方便面还快
-
百度搜 "Dev-C++ 官方下载",认准带 "Bloodshed" 标识的官网(别点到广告!❌);
-
下载后双击安装,一路点 "Next",路径选个好记的(比如 D 盘根目录,别藏太深找不到);
-
安装完成后打开,看到 "File -> New -> Source File",恭喜!你的代码 "草稿纸" 准备好了📝。
1.2 第一个程序:"Hello World"------OI 界的 "见面礼"
不管学啥编程语言,第一个程序必然是跟世界打个招呼,这是行业 "潜规则"😎。来,跟着敲:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello OI! I'm coming!" << endl;
return 0;
}
代码解读(敲黑板!📌):
-
#include <iostream>
:相当于 "请个秘书",iostream
是输入输出的 "工具箱",没有它就没法打印文字、读入数据; -
using namespace std;
:"秘书办公室地址",std
是标准命名空间,不写这个的话,每次用cout
都得写成std::cout
,麻烦得很; -
int main()
:程序的 "正门",所有代码都得从这儿进,int
表示这个门 "出来时要带个整数"; -
cout << ... << endl;
:"喇叭" 功能,把引号里的内容喊出来,endl
是 "换行键",喊完换一行; -
return 0;
:从正门出来时带的 "凭证",0 表示 "程序跑得很顺利,没出岔子"。
运行程序:
点 Dev-C++ 上的 "运行" 按钮(绿色三角,像播放键▶️),会弹出个黑框框,里面显示 "Hello OI! I'm coming!",恭喜!你成功写出了第一行 OI 代码,比 90% 的路人强了~
第二章:C++ 语法基础 ------ 代码的 "拼音和汉字"
如果把程序比作文章,语法就是 "拼音和汉字",得先学会怎么组词造句,才能写长篇大论。咱从 "变量""数据类型" 这些最基础的说起,保证比学英语语法简单!
2.1 变量:给数据 "起名字",别搞复杂的!
变量就是 "装东西的盒子"📦,比如装年龄、装分数、装数量。起名字有讲究:
-
只能用字母、数字、下划线,且不能以数字开头(比如
a1
行,1a
不行); -
不能用 C++ 的 "关键字"(比如
int
cout
这些已经有特殊含义的词); -
别搞 "apple_number" 这种长名字,
a
b
x
y
arr
b[n]
就行,OIer 讲究效率!
示例:定义变量
int a; // 装整数(比如1、-5、100),像"小盒子"
double b; // 装小数(比如3.14、0.618),像"大盒子"
char c; // 装单个字符(比如'a'、'A'、'1'),像"迷你盒子"
bool d; // 装布尔值,只有true(真,相当于1)和false(假,相当于0),像"开关盒子"
变量初始化:"盒子刚买回来先装东西"
定义变量后最好马上赋值,不然里面可能是 "垃圾数据"(就像新买的盒子里有灰尘):
int a = 10;
double b = 3.14159;
char c = 'O';
bool d = true;
2.2 数据类型:不同 "盒子" 装不同 "东西"
刚才提到的int
double
就是数据类型,相当于 "盒子的尺寸",装错了会出问题(比如把西瓜塞进火柴盒🚫)。常见的有这些:
数据类型 | 作用 | 范围(大概) | 示例 |
---|---|---|---|
int | 存储整数 | -20 亿~20 亿 | 10、-500 |
long long | 存储大整数 | -9e18 ~ 9e18 | 12345678901234 |
double | 存储小数(双精度) | 约 15 位小数 | 3.14、0.0001 |
float | 存储小数(单精度) | 约 6 位小数 | 1.23f(要加 f) |
char | 存储单个字符 | ASCII 码范围 | 'A'、'5'、'+' |
bool | 存储真假 | true/false | true |
避坑提醒!⚠️
-
整数除法会 "丢小数":比如
5/2
结果是 2,不是 2.5!要想得到小数,得把其中一个数改成小数,比如5.0/2
或5/2.0
; -
long long
变量赋值要加LL
:比如long long x = 12345678901234LL;
,不然可能 "装不下" 导致出错; -
char
类型用单引号:'a'
是字符,"a"
是字符串,别搞混!
2.3 运算符:给数据 "做运算",像数学题一样
运算符就是 "计算器功能"🧮,加加减减乘乘除除都靠它。
算术运算符:最常用的 "加减乘除余"
int x = 10, y = 3;
cout << x + y << endl; // 加,输出13
cout << x - y << endl; // 减,输出7
cout << x * y << endl; // 乘,输出30
cout << x / y << endl; // 除,输出3(整数除法)
cout << x % y << endl; // 取余,输出1(10除以3余1)
赋值运算符:"把右边的东西装进左边的盒子"
int a = 5;
a += 3; // 相当于a = a + 3,结果a=8
a -= 2; // 相当于a = a - 2,结果a=6
a *= 4; // 相当于a = a * 4,结果a=24
a /= 6; // 相当于a = a / 6,结果a=4
a %= 3; // 相当于a = a % 3,结果a=1
比较运算符:"比大小",结果是 bool 值
int x = 5, y = 8;
cout << (x > y) << endl; // 大于,false(输出0)
cout << (x < y) << endl; // 小于,true(输出1)
cout << (x == y) << endl; // 等于,false(输出0)------ 注意是两个等号!一个等号是赋值
cout << (x != y) << endl; // 不等于,true(输出1)
cout << (x >= y) << endl; // 大于等于,false(输出0)
cout << (x <= y) << endl; // 小于等于,true(输出1)
逻辑运算符:"与或非",处理 "条件判断"
bool a = true, b = false;
cout << (a && b) << endl; // 与:都真才真,输出0
cout << (a || b) << endl; // 或:有真就真,输出1
cout << (!a) << endl; // 非:取反,输出0
2.4 输入输出:和程序 "对话",传递信息
程序不光要 "说话"(输出),还得 "听话"(输入),不然就是 "自说自话的哑巴"🗣️。输入用cin
,输出用cout
,记住 "箭头方向":cin
是 "数据流进变量"(>>),cout
是 "数据流出屏幕"(<<)。
基本输入输出示例
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
cout << "请输入一个整数、一个小数、一个字符:" << endl;
cin >> a >> b >> c; // 可以连续输入,用空格或回车分隔
cout << "你输入的整数是:" << a << endl;
cout << "你输入的小数是:" << b << endl;
cout << "你输入的字符是:" << c << endl;
return 0;
}
输入输出格式控制(进阶小技巧)
有时候输出小数要控制位数,比如保留 2 位小数,这时候需要 "请个专门的秘书"------iomanip
库:
#include <iostream>
#include <iomanip> // 格式控制库
using namespace std;
int main()
{
double pi = 3.1415926535;
cout << fixed << setprecision(2) << pi << endl; // 保留2位小数,输出3.14
cout << setprecision(6) << pi << endl; // 保留6位小数,输出3.141593
return 0;
}
2.5 分支结构:程序 "选路走",像岔路口一样
生活中总要做选择:"如果下雨就带伞,否则不带";程序里也一样,靠分支结构实现 "选路走"。主要有if
和switch
两种。
if 语句:"如果... 就... 否则..."
#include <iostream>
using namespace std;
int main()
{
int score;
cout << "请输入成绩:" << endl;
cin >> score;
if (score >= 90)
{
cout << "优秀!🎉" << endl;
}
else if (score >= 80)
{
cout << "良好!👍" << endl;
}
else if (score >= 60)
{
cout << "及格!🙂" << endl;
}
else
{
cout << "不及格,要加油!💪" << endl;
}
return 0;
}
避坑提醒!⚠️
-
if
后面的括号里是 "条件",必须是 bool 值或能转成 bool 值的表达式(比如score >= 90
); -
不要漏写大括号!如果
if
后面只有一句话,可以不写,但多句话必须写,不然程序会 "认错亲"(只执行第一句); -
else
总是跟着最近的未配对的if
,别搞混层级。
switch 语句:"多选一",像选择题一样
当条件是 "等于某个固定值" 时,用switch
比if-else
更清晰:
#include <iostream>
using namespace std;
int main()
{
int day;
cout << "请输入星期几(1-7):" << endl;
cin >> day;
switch (day)
{
case 1:
cout << "星期一,打工人的开始😩" << endl;
break; // 必须加break,不然会"串台"执行下一个case
case 2:
cout << "星期二,还没缓过来😮💨" << endl;
break;
case 3:
cout << "星期三,周中了,加油💪" << endl;
break;
case 4:
cout << "星期四,快周末了✨" << endl;
break;
case 5:
cout << "星期五,狂喜!🎉" << endl;
break;
case 6:
cout << "星期六,摆烂日😴" << endl;
break;
case 7:
cout << "星期日,emo了😔" << endl;
break;
default:
cout << "输入错误!😵" << endl; // 没有匹配的case时执行
}
return 0;
}
switch 避坑!⚠️
-
case
后面必须是 "常量表达式"(比如 1、'A',不能是变量); -
每个
case
后面一定要加break
,不然会从匹配的case
开始,一直执行到break
或结束,比如输入 1 会输出所有 case 的内容,惨不忍睹; -
default
可选,但加上能处理 "输入错误" 的情况,更严谨。
2.6 循环结构:程序 "重复做",像复读机一样
如果要让程序 "打印 100 遍'我爱 OI'",总不能写 100 行cout
吧?这时候就需要循环结构,相当于 "复读机开关"🔁。主要有for
、while
、do-while
三种。
for 循环:"固定次数" 的循环,最常用!
格式:for (初始化; 条件; 更新)
,像 "设定复读次数:从第 1 次开始,复读到 100 次,每次加 1"。
示例 1:打印 1 到 10
#include <iostream>
using namespace std;
int main()
{
for (int i = 1; i <= 10; i++)
{
cout << i << " ";
}
// 输出:1 2 3 4 5 6 7 8 9 10
return 0;
}
示例 2:计算 1 到 100 的和
#include <iostream>
using namespace std;
int main()
{
int sum = 0;
for (int i = 1; i <= 100; i++)
{
sum += i; // 相当于sum = sum + i
}
cout << "1+2+...+100=" << sum << endl; // 输出5050
return 0;
}
while 循环:"满足条件就循环",不知道次数时用
格式:while (条件)
,像 "只要没吃饱,就一直吃"。
示例:计算 1 到 n 的和,直到和超过 1000
#include <iostream>
using namespace std;
int main()
{
int sum = 0, n = 0;
while (sum <= 1000)
{
n++;
sum += n;
}
cout << "当n=" << n << "时,和超过1000,此时和为" << sum << endl;
return 0;
}
do-while 循环:"先做一次,再判断条件"
格式:do { ... } while (条件);
,和while
的区别是 "先执行一次循环体,再判断",像 "先吃一口,再看饱没饱"。
示例:至少打印一次 "Hello"
#include <iostream>
using namespace std;
int main()
{
int x = 0;
do
{
cout << "Hello" << endl;
x++;
} while (x > 1); // 条件不满足,但还是执行了一次
// 输出:Hello
return 0;
}
循环避坑!⚠️
-
别写 "死循环"!比如
for (;;)
或while (true)
,除非你想让程序 "无限复读" 直到电脑死机💻💥。一旦出现死循环,赶紧按 "Ctrl+C" 强制停止,不然你的电脑会像 "卡壳的录音机" 一样停不下来; -
循环条件要 "能变化":比如
while (sum <= 1000)
里的sum
会不断增加,最终会不满足条件跳出循环;如果写成while (1 <= 1000)
,条件永远为真,就成了死循环; -
for
循环的 "更新语句" 别漏写:比如for (int i = 1; i <= 10; )
,少了i++
,i
永远是 1,会一直循环下去。
2.7 数组:"一排盒子",装多个同类型数据
如果要装 100 个学生的成绩,总不能定义 100 个变量a1
a2
... a100
吧?这时候就需要数组,相当于 "一排一模一样的盒子"📚,每个盒子有编号(下标),方便查找。
数组的定义:"指定盒子数量和类型"
格式:数据类型 数组名[长度];
,长度必须是 "常量"(比如 100、5,不能是变量)。
int a[10]; // 10个装整数的盒子,编号0-9(注意!下标从0开始,不是1!)
double b[5]; // 5个装小数的盒子,编号0-4
char c[20]; // 20个装字符的盒子,编号0-19
数组的初始化:"给一排盒子装东西"
// 方式1:全部初始化
int a[5] = {1, 2, 3, 4, 5}; // a[0]=1, a[1]=2, ..., a[4]=5
// 方式2:部分初始化,未初始化的默认为0
int b[5] = {1, 2}; // b[0]=1, b[1]=2, b[2]=0, b[3]=0, b[4]=0
// 方式3:不写长度,让编译器自己数
int c[] = {10, 20, 30}; // 编译器会自动判断长度为3,下标0-2
数组的使用:"通过编号找盒子"
示例:输入 10 个整数,求它们的和
#include <iostream>
using namespace std;
int main()
{
int a[10], sum = 0;
cout << "请输入10个整数:" << endl;
for (int i = 0; i < 10; i++)
{
cin >> a[i]; // 给第i个盒子装数据
sum += a[i]; // 累加第i个盒子里的数据
}
cout << "这10个整数的和是:" << sum << endl;
return 0;
}
数组避坑!⚠️
-
下标从 0 开始!这是 OI 新手最容易踩的坑!比如
int a[5]
的下标是 0-4,不是 1-5,访问a[5]
会 "越界",导致程序崩溃或输出乱码(相当于去翻别人的盒子,会被 "保安" 抓👮); -
数组长度不能是变量!比如
int n = 10; int a[n];
在 C++ 里是不允许的(某些编译器支持,但 OI 比赛里绝对不行),必须用常量,比如int a[10];
; -
数组不能直接赋值!比如
int a[5] = {1,2,3,4,5}; int b[5]; b = a;
是错误的,要赋值得用循环逐个元素复制。
2.8 字符串:"一串字符盒子",装文字用
字符串就是 "字符数组的升级版",用来装文字(比如 "OI 加油"),用string
类型最方便,比字符数组好用 100 倍!👍
string 的定义和初始化
#include <string> // 必须包含这个库!
using namespace std;
int main()
{
string s1; // 空字符串
string s2 = "OI"; // 初始化字符串为"OI"
string s3 = s2; // 用另一个字符串初始化
string s4(5, 'a'); // 5个'a'组成的字符串,即"aaaaa"
return 0;
}
string 的常用操作
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "Hello";
string t = " OI!";
// 1. 拼接字符串(直接用+)
string res = s + t;
cout << res << endl; // 输出"Hello OI!"
// 2. 获取长度(用size()或length(),都一样)
cout << "长度:" << res.size() << endl; // 输出9
// 3. 访问单个字符(和数组一样,下标从0开始)
cout << "第一个字符:" << res[0] << endl; // 输出'H'
// 4. 输入输出字符串(直接用cin和cout,不用考虑空格)
string s5;
cout << "请输入一个字符串:" << endl;
cin >> s5; // 输入"Algorithm"
cout << "你输入的是:" << s5 << endl; // 输出"Algorithm"
return 0;
}
字符串避坑!⚠️
-
用
string
必须包含<string>
库!不然编译器会 "不认识"string
类型; -
cin
读字符串时会 "遇到空格就停":比如输入 "Hello World",cin >> s
只会读入 "Hello",如果要读入带空格的字符串,要用getline(cin, s)
; -
字符串下标也会越界!比如
s = "OI"
,访问s[2]
会出错。
2.9 函数:"代码的'积木块'",重复使用更高效
如果多个地方都需要 "计算两个数的和",总不能每次都写一遍加法代码吧?这时候就需要函数,相当于 "预制的积木块"🧱,写一次就能反复用。
函数的定义:"造一个积木块"
格式:返回值类型 函数名(参数列表)
{
函数体(要执行的代码)
return 返回值;// 和返回值类型对应
}
示例:定义一个求两个整数和的函数
#include <iostream>
using namespace std;
// 函数定义:返回值类型int,函数名add,参数列表int x, int y
int add(int x, int y)
{
int z = x + y;
return z; // 返回和z
}
int main()
{
int a = 5, b = 3;
int sum = add(a, b); // 调用函数,把a和b传给x和y,接收返回值
cout << "5+3=" << sum << endl; // 输出8
return 0;
}
函数的参数:"给积木块传材料"
参数分 "形参" 和 "实参":
-
形参:函数定义时的参数(比如
add
函数的x
y
),相当于 "积木块的接口"; -
实参:函数调用时的参数(比如
add(a, b)
的a
b
),相当于 "传给接口的材料"。
函数的返回值:"积木块的产出"
-
返回值类型要和
return
后面的值类型一致:比如int add(...)
,return
后面必须是整数; -
如果没有返回值,返回值类型写
void
,可以不写return
:void print_hello()
{
cout << "Hello Function!" << endl;
}
int main()
{
print_hello(); // 调用函数,输出"Hello Function!" return 0;
}
函数避坑!⚠️
-
函数要 "先声明后使用":如果函数定义在
main
函数后面,必须在main
前面声明,不然编译器会 "不认识" 函数:#include <iostream>
using namespace std;
// 函数声明(告诉编译器有这个函数)
int add(int x, int y);
int main()
{
int sum = add(5, 3); // 可以正常调用 return 0;
}
// 函数定义(在main后面)
int add(int x, int y)
{
return x + y;
}
-
形参和实参类型要匹配:比如
add(5.0, 3)
,形参是int
,实参是double
,可能会出错; -
不要在函数里定义 "全局变量":函数里定义的变量是 "局部变量",出了函数就 "消失" 了,全局变量在函数外定义,所有函数都能访问,但尽量少用(容易搞混)。
2.10 指针:"变量的'地址牌'",进阶知识点(入门了解即可)
指针是 C++ 的 "灵魂",但对 OI 新手来说有点难,先简单了解:指针就是 "存储变量地址的变量",相当于 "地址牌"🗺️,通过地址能找到变量本身。
指针的定义和使用
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int *p = &a; // 定义指针p,存储a的地址(&是取地址符)
cout << "a的值:" << a << endl; // 输出10
cout << "a的地址:" << &a << endl; // 输出a在内存中的地址(比如0x61fe14)
cout << "p的值(a的地址):" << p << endl; // 输出和&a一样的地址
cout << "p指向的值(a的值):" << *p << endl; // *是解引用符,输出10
*p = 20; // 通过指针修改a的值
cout << "修改后a的值:" << a << endl; // 输出20
return 0;
}
指针避坑!⚠️
-
指针要 "初始化":不然会指向 "垃圾地址",修改
*p
会导致程序崩溃; -
别解引用 "空指针":
int *p = NULL; *p = 10;
会直接崩溃; -
入门阶段用得少:OI 新手先掌握前面的内容,指针在进阶算法(比如链表)里才常用。
第三章:算法入门 ------ 代码的 "解题思路"
语法学会了,就像学会了 "拼音和汉字",但要写出 "好文章"(解决 OI 问题),还需要 "写作思路"(算法)。算法就是 "解决问题的步骤",比如 "怎么找数组里的最大值""怎么排序",这些都是基础算法。
3.1 枚举算法:"暴力出奇迹",最简单的算法
枚举算法就是 "一个一个试",像 "找钥匙时把钥匙串上的钥匙挨个试"🔑,虽然笨,但对简单问题很有用。
适用场景:
问题的可能解数量不多,能一个个列举出来判断。
示例:找 1-100 中能被 3 和 5 同时整除的数
#include <iostream>
using namespace std;
int main()
{
cout << "1-100中能被3和5同时整除的数:" << endl;
for (int i = 1; i <= 100; i++)
{
if (i % 3 == 0 && i % 5 == 0)
{
cout << i << " ";
}
}
// 输出:15 30 45 60 75 90
return 0;
}
枚举避坑!⚠️
-
别 "枚举范围太大":比如找 1-1e9 中的某个数,枚举会超时(程序运行时间太长,OI 比赛里会判错);
-
优化枚举条件:比如找 "两个数的和为 100",可以枚举一个数
i
,另一个数就是100-i
,不用枚举两个数,节省时间。
3.2 查找算法:"找东西的技巧",比枚举快
查找就是 "在一堆数据里找某个目标",比如 "在成绩表里找小明的分数",常用的有 "顺序查找" 和 "二分查找"。
3.2.1 顺序查找:"从头摸到尾",简单但慢
顺序查找就是 "从第一个元素开始,挨个看是不是目标",像 "在书架上一本本找书"📚。
示例:在数组中找目标值,返回下标(没找到返回 - 1)
#include <iostream>
using namespace std;
int search(int a[], int n, int target)
{
for (int i = 0; i < n; i++)
{
if (a[i] == target)
{
return i; // 找到,返回下标
}
}
return -1; // 没找到
}
int main()
{
int a[5] = {10, 20, 30, 40, 50};
int target = 30;
int pos = search(a, 5, target);
if (pos != -1)
{
cout << "找到目标,下标为:" << pos << endl; // 输出2
}
else
{
cout << "没找到目标" << endl;
}
return 0;
}
3.2.2 二分查找:"折半找",快但要求数据有序
二分查找就像 "猜数字游戏":"我想的数字在 1-100 之间,你猜 50,我说太大,你就知道在 1-49 之间,再猜 25......",前提是数据 "从小到大排好序"。
二分查找步骤:
-
定义左边界
l
(初始 0)和右边界r
(初始 n-1); -
计算中间位置
mid = (l + r) / 2
; -
如果
a[mid] == target
,找到,返回 mid; -
如果
a[mid] > target
,目标在左边,r = mid - 1
; -
如果
a[mid] < target
,目标在右边,l = mid + 1
; -
重复 2-5,直到
l > r
,没找到,返回 - 1。
示例:有序数组中二分查找目标值
#include <iostream>
using namespace std;
int binary_search(int a[], int n, int target)
{
int l = 0, r = n - 1;
while (l <= r)
{
int mid = (l + r) / 2;
if (a[mid] == target)
{
return mid;
}
else if (a[mid] > target)
{
r = mid - 1;
}
else
{
l = mid + 1;
}
}
return -1;
}
int main()
{
int a[5] = {10, 20, 30, 40, 50}; // 必须有序!
int target = 40;
int pos = binary_search(a, 5, target);
if (pos != -1)
{
cout << "找到目标,下标为:" << pos << endl; // 输出3
}
else
{
cout << "没找到目标" << endl;
}
return 0;
}
二分查找避坑!⚠️
-
数据必须有序!这是二分查找的 "生命线",无序数组不能用;
-
防止
l + r
溢出:如果l
和r
很大(比如 1e9),l + r
会超过int
的范围,改成mid = l + (r - l) / 2
就安全了; -
边界条件别搞错:是
l <= r
还是l < r
,r = mid - 1
还是r = mid
,搞混会导致死循环或漏找。