OI?原来这么简单-语法&算法入门篇

各位未来的算法大佬们,大家好!👋 是不是刚听说 OI(信息学奥林匹克竞赛)时,以为是什么歪门邪道?其实非也非也,这玩意儿全称是信息学奥林匹克竞赛,说白了就是用代码解决数学和逻辑问题的 "脑力奥运会"🏆。今天咱就从最基础的语法开始,一步步爬向算法入门的门槛,保证全程无废话、多笑点、代码接地气,变量名绝不搞 "bad_apples [apple_number]" 这种花里胡哨的操作,主打一个 "b [n] 就能解决" 的朴实无华~

第一章:OI 入门先搭台 ------ 环境配置像开黑前装游戏

在敲代码前,得先有个 "战场" 吧?就像打游戏要先装客户端,OIer 的第一站就是配置编程环境。目前主流的有 Dev-C++、Code::Blocks、VS Code,新手建议从 Dev-C++ 入手,轻便如 "手机小游戏"📱,不像 VS Code 那样需要装一堆插件,省事儿!

1.1 安装 Dev-C++:三步搞定,比泡方便面还快

  1. 百度搜 "Dev-C++ 官方下载",认准带 "Bloodshed" 标识的官网(别点到广告!❌);

  2. 下载后双击安装,一路点 "Next",路径选个好记的(比如 D 盘根目录,别藏太深找不到);

  3. 安装完成后打开,看到 "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/25/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 分支结构:程序 "选路走",像岔路口一样

生活中总要做选择:"如果下雨就带伞,否则不带";程序里也一样,靠分支结构实现 "选路走"。主要有ifswitch两种。

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 语句:"多选一",像选择题一样

当条件是 "等于某个固定值" 时,用switchif-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吧?这时候就需要循环结构,相当于 "复读机开关"🔁。主要有forwhiledo-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......",前提是数据 "从小到大排好序"。

二分查找步骤:

  1. 定义左边界l(初始 0)和右边界r(初始 n-1);

  2. 计算中间位置mid = (l + r) / 2

  3. 如果a[mid] == target,找到,返回 mid;

  4. 如果a[mid] > target,目标在左边,r = mid - 1

  5. 如果a[mid] < target,目标在右边,l = mid + 1

  6. 重复 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溢出:如果lr很大(比如 1e9),l + r会超过int的范围,改成mid = l + (r - l) / 2就安全了;

  • 边界条件别搞错:是l <= r还是l < rr = mid - 1还是r = mid,搞混会导致死循环或漏找。