零基础入门:想象你在玩一个电子游戏,或者在模拟一个真实的生活场景。比如“第 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 行有几个空格几个星 |
| 进制/编码转换 | "把数字换成另一种编码输出" | 取位 / 取模 + 逆序 |
| 模板匹配 | "按给定规则替换 / 展开文本" | 逐段解析 + 状态机 |
n ≤ 1000、1e5),没有提示需要高级算法;和枚举法的分界:模拟是“按时间/流程一步步走”;枚举是“把所有可能一个个试”。很多题两者会混着用。
// 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. 确认输出格式:空格、换行、大小写、保留几位小数 —— 对着题目要求逐字核。
z 后面回到 a、周日后面回到周一;i==j || i+j==n-1 处打印 X。① 题目描述
给定一个 $n \times m$ 的雷区,字符 * 表示雷,. 表示空地。对于每个空地,需要输出它周围 8 个相邻格子中雷的数量。
② 审题分析
*;如果是空地,则检查周围 8 个方向,统计雷的数量。if 会非常冗长且容易漏写;如果不处理边界,访问到数组外会程序崩溃或读到错误数据。③ 解题思路
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(注:具体输出取决于输入数据,此处仅为演示逻辑正确性,实际输出与代码逻辑严格对应。)
① 题目描述
给定一个奇数 $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,验证了模拟逻辑的正确性。)
共性特征:
各自技巧:
if-else 分支,注意分支顺序(特殊在前,普通在后)。rule(i, j) 函数,找行列下标与图案特征的数学关系。审题模板(固定思考流程):
1. 题目在说什么?(提取关键词:网格、日期、规则、图案)
2. 有哪些状态需要维护?(变量列表)
3. 每一步做什么?(循环体)
4. 什么时候停止?(循环条件)
5. 输出格式是什么?(逐字核对)
错误清单(只列上文没讲过的特有错误):
int(比如累计天数、累计金额)。估算一下上界,拿不准就用 long long。if (j) putchar(' ') 之类的方式避免。| 技法分层 | 解决什么 | 典型写法 |
|---|---|---|
| 基础遍历 | 网格/矩阵逐格处理 | 双重循环 + 方向数组 + 越界判断 |
| 日期推进 | 年月日时分秒变化 | 月份天数表 + 闰年判断 + 进位逻辑 |
| 规则模拟 | 按流程/状态转换 | 循环 + 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 真题里出现过的题目汇总到一页,可按考点横向刷;每题都带答案、解题思维和常见错误。
看 模拟 历年真题汇总 → 去平台刷题 →