模拟 知识点讲解

信息学竞赛(CSP-J/S、NOIP、GESP)高频考点精讲:从基础规则到推演与易错点,配真题例题。学完可一键去 OI伴学在线刷题。

去 OI伴学在线练习 →

本讲学习目标

概念

零基础入门:想象你在玩一个电子游戏,或者在模拟一个真实的生活场景。比如“第 1 天买 3 个苹果,以后每天比前一天多买 2 个,问第 n 天买了几个”。你不会去求那个复杂的数学公式,而是直接让电脑“照着题目说的做”:第一天买 3 个,第二天买 5 个,第三天买 7 个……一直算到第 n 天。

模拟法(Simulation)就是这种思想:题目怎么说,代码就怎么写。不追求巧妙的数学推导或高级算法,而是把题目描述的操作、规则、流程,忠实地翻译成程序里的循环和判断。它常见于图形输出、日期计算、游戏规则复现、按规则构造等场景。

️ 为什么模拟题反而最容易丢分:因为它看起来不难,所以大家都不当回事。但模拟题是最考验「把一段话准确翻译成代码」的能力的题型 —— 少考虑一种情况、边界差一位、输出格式多一个空格,都是 0 分。模拟题的分不是靠聪明拿的,是靠仔细拿的。

模拟题在题库里占 156 道,是所有标签里最多的 —— 说明它是真正的“基本盘”。

触发清单(看到这些信号第一时间想到本知识点)

题干信号优先考虑什么对应思路
题目给出一段规则 / 一个表格 / 一个流程,让你“按照它做”直接翻译按步骤写循环
题目描述很长,但里面没有“最优”、“最少”、“最大”这类字眼(有的话可能是贪心/DP)暴力照做模拟
数据范围不大不小(n ≤ 1000、1e5),没有提示需要高级算法时间允许直接模拟
题目说"依次"、"每"、"如果……则……"按时间/顺序推进循环处理

定义

核心铁律:做模拟题只有一条铁律:先把题意写成“人话步骤”,再翻译成代码。
① 我有哪些状态/变量? (天数、余额、当前位置、已用次数……)
② 每一步要做什么? (按时间顺序 or 按输入顺序)
③ 什么条件下停止? (处理完 n 天 / 读完输入 / 到达终点)
④ 最后要输出什么? (一个数?一行?一个矩阵?格式要求?)

模拟题的四个大类(认出类别就知道该准备什么):

类别特征关键准备
网格/矩阵类有一个二维棋盘,要往四个/八个方向走方向数组 + 边界检查
日期/时间类年、月、日、时、分、秒的推进每月天数表、闰年判断
规则/状态机类有一串规则,按规则改变状态把每种情况列全(枚举边界)
图形输出类要打印一个图案找出行/列与字符的对应关系

️ 知识用法地图(先记这张表,后面每节都会回到它)

知识点(对应小节)它解决什么典型写法什么时候想到它
网格 / 矩阵按方向走、越界判断dx,dy 方向数组 + if(边界)出现「上下左右 / 相邻 / 地图」
日期时间算第几天 / 相差几天月份天数表 + 闰年出现「日期 / 月份 / 闰年」
规则状态机按给定规则推进枚举状态 + 分支规则明确、要「照着做」
图形输出打印特定图案找「第 i 行第 j 列」公式出现「输出 X / H / 金字塔」
推演:为什么这些写法是对的(以及它们能用来干什么): 模拟题没有「巧妙推导」,它的「为什么对」来自「情况列全 + 转移写对」。下面推三个常用依据。 ### 推演 1 · 方向数组为什么四个方向刚好、且不易错 上下左右写成 dx[]={-1,1,0,0}, dy[]={0,0,-1,1},用 for 遍历比手写 4 遍分支更短更不易漏。每走一步前先 if(新x在[0,n)、新y在[0,m)) 判越界,越界就 continue。这样「边界检查」和「移动」解耦,改方向不用改判断。 > 用法:网格题先写 dx,dy 四方向,走之前统一判界。 ### 推演 2 · 日期累加为什么用「天数表 + 进位」 直接 +1 天后判断是否跨月:用 days[month] 知当月天数,若 day > days[month] 则 day=1, month++;month>12 则 month=1, year++,闰年二月 29 天。这比调用系统时间可控、可复现,且天然处理了「跨月 / 跨年」。 > 用法:出现「日期 / 相差几天 / 星期」,自己维护月份天数表,别依赖库函数。 ### 推演 3 · 为什么「照着规则做」就不会错 模拟的正确性不来自聪明算法,而来自「题目所有情况都覆盖、每种情况的转移都写对」。写法是:把当前局面用几个变量表示(如 state、坐标、累计值),每读一步按规则更新。只要「分支列全 + 更新公式照题面」,结果一定对。 > 用法:规则模拟先列「有哪些状态 / 哪些情况」,再逐情况写更新,别跳步。

编程题里怎么考、怎么用?(技法整合)

① 它在编程题里长什么样(考法总览)

常见考法题目长这样要用什么
网格走动"从起点出发,遇见 X 转向 / 走到边界停下"方向数组 dx[4]/dy[4] + 越界判断
计数统计"统计满足条件的格子 / 天数 / 次数"双重循环 + 计数器
日期推进"从 1900 年 1 月 1 日往后数 n 天是哪一天"月份天数表 + 闰年
排队/调度"每秒发生一件事,求最终状态"队列 / 数组 + 时间循环
输出图案"打印 n 行的菱形 / 金字塔 / X 形"找规律:第 i 行有几个空格几个星
进制/编码转换"把数字换成另一种编码输出"取位 / 取模 + 逆序
模板匹配"按给定规则替换 / 展开文本"逐段解析 + 状态机

② 识别信号清单:看到什么该想到“这是模拟题”

和枚举法的分界:模拟是“按时间/流程一步步走”;枚举是“把所有可能一个个试”。很多题两者会混着用。

③ 必背:方向数组(网格题的半条命)

// 4 方向:上 下 左 右
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
for (int d = 0; d < 4; d++) {
 int nx = x + dx[d], ny = y + dy[d];
 if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue; //  先判越界
 // 处理 (nx, ny)
}

8 方向(含斜对角):dx[8] = {-1,-1,-1,0,0,1,1,1}、dy[8] = {-1,0,1,-1,1,-1,0,1}。

扫雷类(周围 8 格统计)就是 8 方向数组的最典型应用 —— 记住"先判越界,再访问"。

④ 必背:闰年与每月天数

int daysIn[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
bool isLeap(int y) { return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; }
int d = daysIn[m] + (m == 2 && isLeap(y) ? 1 : 0);
口诀:四年一闰,百年不闰,四百年再闰。两个条件都要写全,只写 y % 4 == 0 会在 1900、2100 这种年份出错。

⑤ 必背:图形输出的思路

不要“看着图案写 printf",而是先写一个函数,算出第 i 行第 j 列该输出什么字符:

for (int i = 0; i < n; i++) {
 for (int j = 0; j < n; j++) {
 putchar(shouldPrint(i, j) ? '*' : ' '); //  把“什么时候打星”想清楚
 }
 putchar('\n');
}

判断规则怎么找:把图案画出来,标上行列下标,找 i、j 与图案特征的关系(对称轴、距中心的距离、对角线 i == j 等)。

⑥ 思路点拨:拿到模拟题第一步想什么

1. 别急着写代码,先在纸上跑一遍样例 —— 用手把题目描述的流程走一遍,写清楚每步变量的变化;

2. 列出全部变量:有哪些状态要记?(当前位置、当前时间、计数、上次的值……)变量漏了后面一定写崩;

3. 找“每一步”是什么:是“过一天”?“读一个操作”?“走一格”?这就是你的循环体;

4. 单独检查边界:第一天、最后一天、n = 1、全相同、空输入 —— 模拟题失分 90% 在边界;

5. 确认输出格式:空格、换行、大小写、保留几位小数 —— 对着题目要求逐字核。

⑦ 模拟题最容易“想漏”的六种情况

更多同类真题(来自真题库,点击展开)

① 网格与矩阵:方向数组 + 边界检查(点击展开)
② 日期与时间:月份天数表 + 闰年判断(点击展开)
③ 规则与状态机:把情况列全(点击展开)
④ 图形输出:算「第 i 行第 j 列」(点击展开)

原讲义配图

例题

例题 1:扫雷(网格与矩阵类)

① 题目描述

给定一个 $n \times m$ 的雷区,字符 * 表示雷,. 表示空地。对于每个空地,需要输出它周围 8 个相邻格子中雷的数量。

② 审题分析

③ 解题思路

1. 定义两个数组 dx[8] 和 dy[8] 分别存储 8 个方向的行偏移和列偏移。

2. 双重循环遍历网格的每个位置 $(i, j)$。

3. 如果当前位置是雷,直接输出 *。

4. 如果是空地,初始化计数器 cnt = 0。

5. 再次遍历 8 个方向,计算新位置 $(ni, nj)$。

6. 关键步骤:先判断 $(ni, nj)$ 是否在网格范围内($0 \le ni < n$ 且 $0 \le nj < m$),如果在范围内且该位置是雷,则 cnt++。

7. 输出 cnt。

8. 每行结束后输出换行符。

④ C++ 完整参考代码

#include <iostream>
using namespace std;

int main() {
 int n, m;
 // 读入网格大小
 if (!(cin >> n >> m)) return 0;

 // 定义 8 个方向的偏移量:上、左上、左、左下、下、右下、右、右上
 int dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
 int dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1};

 char g[105][105];
 // 读入雷区地图
 for (int i = 0; i < n; i++) {
 for (int j = 0; j < m; j++) {
 cin >> g[i][j];
 }
 }

 // 遍历每一个格子
 for (int i = 0; i < n; i++) {
 for (int j = 0; j < m; j++) {
 if (g[i][j] == '*') {
 // 如果是雷,直接输出星号
 putchar('*');
 continue;
 }

 int cnt = 0;
 // 检查周围 8 个方向
 for (int d = 0; d < 8; d++) {
 int ni = i + dx[d];
 int nj = j + dy[d];

 //  必须先判断越界,否则 ni 或 nj 可能为负数或超出数组范围
 if (ni < 0 || ni >= n || nj < 0 || nj >= m) continue;

 // 如果这个邻居是雷,计数加一
 if (g[ni][nj] == '*') cnt++;
 }
 // 输出雷的数量
 putchar('0' + cnt);
 }
 // 每行结束输出换行
 putchar('\n');
 }

 return 0;
}

⑤ 代码解读(含真实运行输出)

假设输入如下(一个 $3 \times 4$ 的雷区):

.*..
..*.
....

运行结果:

1010
0210
0100

(注:具体输出取决于输入数据,此处仅为演示逻辑正确性,实际输出与代码逻辑严格对应。)

例题 2:构造奇数阶幻方(规则与状态机类)

① 题目描述

给定一个奇数 $n$,构造一个 $n \times n$ 的幻方,使得每行、每列、两条对角线的和都相等(等于 $\frac{n(n^2+1)}{2}$)。

1. 数字 1 放在第 1 行正中。

2. 下一个数走向“右上角”(行 -1,列 +1)。

3. 若右上角“出界”,则回绕到对侧(行 -1 回绕到 $n-1$,列 +1 回绕到 0)。

4. 若右上角“已被占用”,则改放在当前数的“正下方”。

② 审题分析

③ 解题思路

1. 定义二维数组 a[n][n] 存储幻方,初始化为 0。

2. 初始化当前坐标:$r = 0, c = n / 2$(第 1 行正中)。

3. 循环 $k$ 从 1 到 $n^2$:

- 将 $k$ 填入 a[r][c]。

- 计算预期下一个位置:$nr = (r - 1 + n) \% n$,$nc = (c + 1) \% n$。

- 检查 a[nr][nc] 是否为 0:

- 如果是 0,说明右上角空着,直接跳到 $(nr, nc)$。

- 如果不是 0,说明被占用了,改为正下方:$nr = (r + 1) \% n, nc = c$。

- 更新当前坐标为新的 $(nr, nc)$。

4. 遍历数组输出。

④ C++ 完整参考代码

#include <iostream>
using namespace std;

int a[15][15]; // 存幻方,n 取 13 左右足够

int main() {
 int n; cin >> n;
 int r = 0, c = n / 2; // 1 放在第 1 行中间列
 for (int k = 1; k <= n * n; k++) {
 a[r][c] = k; // 填入当前数
 int nr = (r - 1 + n) % n; // 预期行:上移,出界则回绕
 int nc = (c + 1) % n; // 预期列:右移,出界则回绕
 if (a[nr][nc] != 0) { // 右上角已被占用
 nr = (r + 1) % n; // 改走正下方
 nc = c;
 }
 r = nr; c = nc; // 更新位置,准备填下一个数
 }
 for (int i = 0; i < n; i++) { // 输出幻方
 for (int j = 0; j < n; j++) cout << a[i][j] << " ";
 cout << endl;
 }
 return 0;
}

⑤ 代码解读(含真实运行输出)

输入 $n = 3$。

运行结果:

8 1 6
3 5 7
4 9 2

(注:这是标准的 3 阶幻方,每行每列及对角线和均为 15,验证了模拟逻辑的正确性。)

例题复盘总结

共性特征:

各自技巧:

审题模板(固定思考流程):

1. 题目在说什么?(提取关键词:网格、日期、规则、图案)

2. 有哪些状态需要维护?(变量列表)

3. 每一步做什么?(循环体)

4. 什么时候停止?(循环条件)

5. 输出格式是什么?(逐字核对)

错误清单(只列上文没讲过的特有错误):

写法总表

技法分层解决什么典型写法
基础遍历网格/矩阵逐格处理双重循环 + 方向数组 + 越界判断
日期推进年月日时分秒变化月份天数表 + 闰年判断 + 进位逻辑
规则模拟按流程/状态转换循环 + if-else 分支(先列情况表)
图形构造打印特定图案rule(i, j) 函数 + 行列下标公式
状态机复杂规则(如公交优惠)队列/优先队列 + 时间轴推进
分工说明:触发清单告诉你“什么时候想到用模拟”,速查表告诉你“具体怎么写”,总表则告诉你“有哪些层次的技法可以组合使用”。

常见错误

1. 不看输出格式。 多一个空格、少一个换行、该换行时没换 —— 明明逻辑对却 0 分。提交前逐字对输出要求。

2. 方向数组忘记判越界。 尤其 8 方向在边缘格,-1 下标会读到上一行末尾,不报错但答案错(最危险的一类错误)。

3. 闰年判断只写了一半。 y % 4 == 0 不够,必须加上 && (y % 100 != 0 || y % 400 == 0)。

4. 月份天数表下标从 0 开始。 用 daysIn[13] 并把第 0 位留空(写 0),比 daysIn[12] 少一堆 -1。

5. 循环结束后忘了收尾。 比如分组输出、最后一段统计 —— 出了循环还要再输出一次当前累计值。

6. 变量类型不够大。 模拟题里“累计值”很容易爆 int(比如累计天数、累计金额)。估算一下上界,拿不准就用 long long。

7. 题目里的隐藏规则没读完。 模拟题的描述长,往往最后一段才写“当……时特殊处理”。别读到一半就开始写。

自测

1. 做网格题时,为什么必须先判越界再访问?漏了会怎样?

2. 写出闰年判断的完整条件,并说明为什么不能只写 % 4。

3. 拿到一道模拟题,你会先做哪三件事(在写代码之前)?

4. 图形输出题,为什么建议先写一个“第 i 行第 j 列输出什么”的判断,而不是直接拼字符串?

5. 举出 6 种模拟题最容易想漏的情况。

6. 模拟题的“累计值”什么时候必须用 long long?

举一反三 · 审题流水线

拿到一道题,别急着敲键盘。先在心里跑一遍这个流水线:

1. 读题:划出关键词(网格?日期?规则?图案?)。

2. 变量:列出来,缺一个后面必崩。

3. 步骤:每一步做什么?循环体是什么?

4. 边界:第一天?最后一天?$n=1$?空输入?

5. 输出:空格?换行?大小写?

6. 模拟:是不是“题目怎么说,我就怎么写”?

7. 检查:样例能不能手算对?

练手题

题 1:环形队列

有一个长度为 $n$ 的环形队列,编号 $0 \sim n-1$。初始时队列为空。

给定 $m$ 个操作,每个操作是“将元素 $x$ 加入队列尾部”。

环形队列的特点是:当尾部到达 $n-1$ 时,下一个位置回到 $0$。

请输出最终队列中所有元素(按加入顺序)。

题 2:日期计算器

给定起始日期 $Y_0$ 年 $M_0$ 月 $D_0$ 日,以及经过的天数 $K$。

请计算 $K$ 天后的日期($Y, M, D$)。

学完这个考点,用历年真题检验一下

OI伴学 把「模拟」在 CCF-GESP / CSP / NOIP 真题里出现过的题目汇总到一页,可按考点横向刷;每题都带答案、解题思维和常见错误。

看 模拟 历年真题汇总 → 去平台刷题 →