第 31 章 模拟——让程序模仿现实
🏗️ 前情回顾:第 30 章你学会了枚举——把所有可能情况列出来逐个试。但有一类题目不要求你"找所有解",而是要求你按规则一步步演算——就像下棋、打牌,每一步的决策都依赖之前的状态。这种"把现实规则翻译成代码,让程序一步步走"的方法,叫模拟。
🎯 本章目标
学完这一章,你能:
- 理解模拟的核心思想:"让程序按规则一步步执行"
- 掌握读模拟题的四步法:圈关键词 → 定义变量 → 写步骤 → 验证样例
- 写出时间模拟、状态模拟、回合制游戏模拟程序
- 独立实现石头剪刀布、掷骰子等经典模拟
- 掌握模拟题的调试技巧(打印中间状态)
📖 故事引入
阿杰参加他的第一场信息学竞赛,翻开试卷看到这样一道题:
小明在玩一个游戏。初始生命值 100。第 1 回合受到 10 点伤害,恢复 5 点生命;第 2 回合受到 15 点伤害,恢复 8 点生命;第 3 回合……(后面列了 10 个回合的数据)。问:小明第几回合生命值降到 0 以下?
阿杰的第一反应:"不就是列方程吗?10 - 5 = 5,第 1 回扣 5 点;15 - 8 = 7,第 2 回合扣 7 点……100 ÷ 5……不对,后面数不一样啊。"
他越想越乱。旁边的队友用 3 分钟写完交卷了。阿杰凑过去看——对方根本没列什么方程,就写了一个循环:
int hp = 100;
for (int i = 1; i <= 10; i++) {
hp -= damage[i];
hp += recover[i];
if (hp <= 0) { cout << i; break; }
}"就这样?"阿杰震惊了。队友耸耸肩:"题目怎么写,代码就怎么翻——这就是模拟啊。"
阿杰恍然大悟:原来不是每道题都需要巧妙的数学公式。很多时候,把题目的规则一字一句翻译成代码,就是标准答案。
🧱 知识讲解
31.1 什么是模拟
模拟(Simulation):把题目描述的操作规则,按顺序翻译成程序语句,让计算机一步步执行。
类比:模拟就像菜谱和厨师。菜谱是题目(规则),你是翻译者(把中文翻成 C++),计算机是厨师(按步骤执行)。只要菜谱没有歧义,菜就做得出来——不需要厨师自己有创意。
题目规则 → 翻译成 C++ 语句 → 计算机逐步执行 → 输出结果模拟题的特点:题干长、规则多、但思路简单。它考的不是你的智商,而是你的细心和耐心。
31.2 读模拟题的四步法
很多同学怕模拟题——题干一长就懵了。试试这个四步法:
| 步骤 | 操作 | 示例(石头剪刀布) |
|---|---|---|
| ① 圈关键词 | 在草稿纸上圈出"谁和谁比""怎么判输赢""多少轮" | "两人出拳""赢的+1分""比 5 轮" |
| ② 定义变量 | 列出需要的所有变量和类型 | int scoreA=0, scoreB=0; char a, b; |
| ③ 写步骤 | 用伪代码或注释搭骨架 | // 循环5轮 {输入→判断→加分} |
| ④ 验证样例 | 用题目给的样例手工走一遍你的逻辑 | A 出石头,B 出剪刀 → A+1 分 ✓ |
先纸上谈兵,再动手敲代码。 这个习惯能省掉你一半的调试时间。
31.3 经典题①:石头剪刀布
题目:两人比赛 5 轮,每轮各自出拳(S 石头 / J 剪刀 / B 布),赢的得 1 分,平局各 0 分。5 轮后输出比分。
代码:
#include <iostream>
using namespace std;
int main() {
int scoreA = 0, scoreB = 0;
char a, b;
for (int i = 1; i <= 5; i++) {
cout << "第 " << i << " 轮,A 出拳(S/J/B):";
cin >> a;
cout << "B 出拳:";
cin >> b;
if (a == b) {
cout << "平局!" << endl;
} else if ((a == 'S' && b == 'J') ||
(a == 'J' && b == 'B') ||
(a == 'B' && b == 'S')) {
cout << "A 胜!" << endl;
scoreA++;
} else {
cout << "B 胜!" << endl;
scoreB++;
}
cout << "当前比分 A:" << scoreA << " B:" << scoreB << endl;
}
cout << "最终比分 A:" << scoreA << " B:" << scoreB << endl;
if (scoreA > scoreB) cout << "A 获胜!" << endl;
else if (scoreB > scoreA) cout << "B 获胜!" << endl;
else cout << "平局!" << endl;
return 0;
}🎯 注意
if条件的写法——把三种获胜情况用||连在一起。如果用switch也可以,但if在这种"多条件同结果"的场景下更简洁。
31.4 经典题②:掷骰子
题目:两人掷骰子,每人掷 10 次。单局规则:点数大的赢,平局各不得分。统计最终胜负和总分差。
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
int main() {
srand(time(0));
int scoreA = 0, scoreB = 0;
for (int i = 1; i <= 10; i++) {
int a = rand() % 6 + 1; // A 的骰子点数
int b = rand() % 6 + 1; // B 的骰子点数
cout << "第 " << i << " 局 A:" << a << " B:" << b;
if (a > b) { scoreA++; cout << " → A 胜" << endl; }
else if (b > a) { scoreB++; cout << " → B 胜" << endl; }
else { cout << " → 平局" << endl; }
}
cout << "A 胜 " << scoreA << " 局,B 胜 " << scoreB << " 局" << endl;
return 0;
}31.5 经典题③:时间模拟
很多竞赛题涉及时间计算——比如"从 08:30 开始,经过 n 分钟后是几点几分?"
int hour, minute, pass;
char colon; // 吞掉 ":"
cin >> hour >> colon >> minute; // 读入 "08:30"
cin >> pass; // 经过的分钟数
minute += pass; // 总分钟数
hour += minute / 60; // 溢出的小时
minute %= 60; // 剩余的分钟
hour %= 24; // 24 小时制循环
printf("%02d:%02d\n", hour, minute); // 补零输出💡 模拟题里的"进位"逻辑极常见——分钟加到 60 进位给小时,秒加到 60 进位给分钟……套路都是
大单位 += 小单位 / 进制; 小单位 %= 进制;
31.6 经典题④:状态模拟
题目:走廊有 n 盏灯(初始关闭),第 1 个人把所有灯打开,第 2 个人把每第 2 盏灯取反(开→关、关→开),第 3 个人把每第 3 盏灯取反……第 k 个人处理第 k 的倍数位置。问最后哪些灯亮着?
这类题的模拟思路:用一个数组记录状态,循环遍历更新状态。
int n, k;
cin >> n >> k;
bool light[101] = {false}; // false=关, true=开
for (int person = 1; person <= k; person++) {
for (int pos = person; pos <= n; pos += person) {
light[pos] = !light[pos]; // 状态取反
}
}
// 输出开着灯的编号
for (int i = 1; i <= n; i++) {
if (light[i]) cout << i << " ";
}🧩 这道题的本质是"模拟每个人的操作"——外层循环模拟不同的人,内层循环模拟这个人影响的灯。每一步都跟题目描述一一对应。
31.7 模拟题调试技巧
模拟题最容易出 bug 的原因:中间状态不透明。你不知道程序在第 3 步到底算出了什么。
调试三板斧:
① 打印中间状态 — 在关键步骤后加 cout:
cout << "第 " << i << " 轮后,变量 a=" << a << " b=" << b << endl;② 用小数据测试 — 先用 n=3、k=2 这种手动能验的数据跑,确认逻辑对再放大数据。
③ 对样例"人工演算" — 拿笔在纸上按你的代码逻辑走一遍样例,看是否和题目给的输出一致。如果纸上算出来的都不对,代码必然不对。
✋ 动手试试
试试 1:把石头剪刀布改为"先赢 3 局者胜"(可能不到 5 轮就结束)。提示:把 for 改成 while,条件为双方都不到 3 分。
试试 2:修改掷骰子程序,增加一条规则:"如果掷出 6 点,可以再掷一次",把额外掷出的点数加到本局总分上。(这就是"奖励回合"——模拟题里最常见的加规则方式。)
试试 3:写一个"自动存钱罐"模拟:每天存入当天日期对应的金额(1 号存 1 元,2 号存 2 元……),输出第 30 天结束时共存了多少钱。
⚠️ 容易犯的错
错 1:题目规则的顺序翻译错了
❌ 题目说"先扣血再回血",代码写成先回血再扣血 ✅ 严格按题目描述的顺序写——顺序一变,结果可能完全不同
错 2:状态变量忘了初始化
❌ int score; 没赋初值,后续 score++ 从垃圾值开始加 ✅ int score = 0; 所有计数器和累加器必须初始化
错 3:循环边界搞错
❌ 题目说"第 k 个人处理第 k 的倍数",循环写成 pos < n(漏了 n 本身) ✅ 仔细看题目是 < 还是 <=,是 n 还是 n-1
错 4:用 cin 读带格式的时间时忘了吞分隔符
❌ cin >> hour >> minute; 读 "08:30" → hour=8, minute 读到 ":" 失败 ✅ char c; cin >> hour >> c >> minute; 用 c 吞掉冒号
📝 练习
基础题
1. 选择题:模拟和枚举最本质的区别是什么? A. 模拟用 while,枚举用 for B. 模拟按规则一步步演算,枚举列所有可能 C. 模拟需要数组,枚举不需要 D. 模拟比枚举快
2. 填空题:读模拟题的四步法是 ①____ ②____ ③____ ④____。
3. 读代码:以下代码模拟了什么?
int total = 0;
for (int day = 1; day <= 7; day++) {
int steps;
cin >> steps;
total += steps;
}
cout << total / 7.0 << endl;提高题
4. 编程题 — 自动售货机
模拟一台自动售货机:可乐 3 元、雪碧 2.5 元、矿泉水 1 元。用户投币(1 元/5 元/10 元),选择商品,机器找零。如果金额不够提示"余额不足",如果够了出货并找零。
5. 编程题 — 井字棋裁判
输入 3×3 的棋盘(X 和 O),判断当前局面谁赢了(三子连线即获胜),或者平局,或者对局未结束。
6. 编程题 — 约瑟夫问题
n 个人围成一圈,从第 1 个人开始报数,报到 m 的人出列,下一个人重新从 1 报数。模拟整个过程,输出出列顺序。如 n=5, m=3 → 出列顺序 3 1 5 2 4。
挑战题
7. 编程题 — 贪吃蛇(简化版)
在 10×10 的棋盘上模拟贪吃蛇移动:初始蛇身长度 3,位于 (0,0)→(0,1)→(0,2),方向向右。用户连续输入方向指令(W/A/S/D),蛇按指令移动。如果撞墙或咬到自己就游戏结束。输出每步移动后的棋盘状态。
8. 编程题 — 多项式求值
输入一个整数 x 和一个多项式的系数序列(如 3 -2 0 5 代表 3x³ - 2x² + 0x + 5),模拟手工代入求值的过程,输出结果。不允许用 pow 函数。
🧠 本章小结
模拟算法 —— 按规则一步步演算:
【核心思想】
题目怎么写 → 代码怎么翻 → 计算机一步步执行
【读题四步法】
① 圈关键词 → 找出"谁做什么、怎么判断、循环几次"
② 定义变量 → 列所有需要的变量和类型
③ 写步骤 → 伪代码或注释搭骨架
④ 验证样例 → 用题目样例手工走一遍
【常见模拟类型】
回合制游戏 → 石头剪刀布、掷骰子
时间模拟 → 进位制(分钟→小时→天)
状态模拟 → 开关灯、数组标记状态变化
过程模拟 → 约瑟夫环、售货机
【调试技巧】
打印中间状态 | 小数据测试 | 手工演算样例📝 配套练习
共8题。模拟从简单计数→状态追踪→规则翻译→边界处理逐级深入。。★核心(课堂必做) ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | luogu-P1046 | https://hydro.ac/p/luogu-P1046 | 循环模拟、条件判断 |
| ◆ 拓展 | luogu-P1567 | https://hydro.ac/p/luogu-P1567 | 连续计数、状态变化 |
| ◆ 拓展 | luogu-P1980 | https://hydro.ac/p/luogu-P1980 | 逐位拆解、嵌套统计 |
| ◆ 拓展 | luogu-P1420 | https://hydro.ac/p/luogu-P1420 | 最长连号、状态重置 |
| ◆ 拓展 | luogu-P1320 | https://hydro.ac/p/luogu-P1320 | 游程编码、压缩模拟 |
| ◆ 拓展 | luogu-P1307 | https://hydro.ac/p/luogu-P1307 | 数字反转、边界处理 |
| ◆ 拓展 | luogu-P1089 | https://hydro.ac/p/luogu-P1089 | 月度预算、状态追踪、break |
| ◆ 拓展 | luogu-P1423 | https://hydro.ac/p/luogu-P1423 | 等比累加、阈值判断 |
💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 7 道◆拓展题,覆盖不同变式和细节。
配套练习
共8题。模拟从简单计数→状态追踪→规则翻译→边界处理逐级深入。★核心(课堂必做) · ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | luogu-P1046 | https://hydro.ac/p/luogu-P1046 | 循环模拟、条件判断 |
| ◆ 拓展 | luogu-P1567 | https://hydro.ac/p/luogu-P1567 | 连续计数、状态变化 |
| ◆ 拓展 | luogu-P1980 | https://hydro.ac/p/luogu-P1980 | 逐位拆解、嵌套统计 |
| ◆ 拓展 | luogu-P1420 | https://hydro.ac/p/luogu-P1420 | 最长连号、状态重置 |
| ◆ 拓展 | luogu-P1320 | https://hydro.ac/p/luogu-P1320 | 游程编码、压缩模拟 |
| ◆ 拓展 | luogu-P1307 | https://hydro.ac/p/luogu-P1307 | 数字反转、边界处理 |
| ◆ 拓展 | luogu-P1089 | https://hydro.ac/p/luogu-P1089 | 月度预算、状态追踪、break |
| ◆ 拓展 | luogu-P1423 | https://hydro.ac/p/luogu-P1423 | 等比累加、阈值判断 |
练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 7 道◆拓展题,覆盖不同变式和细节。
自查清单:
- [ ] 我能解释什么是模拟,它和枚举有什么不同
- [ ] 我会用四步法读一道模拟题
- [ ] 我能写出石头剪刀布和掷骰子的完整程序
- [ ] 我知道时间模拟中"进位"的套路
- [ ] 我会在模拟程序里加
cout打印中间状态来调试 - [ ] 我知道状态变量必须初始化
- [ ] 我知道题目规则的顺序不能随便改动
🚀 下章预告
到目前为止,你写的程序能存的数据量都不大——几个变量、一个数组。但如果要处理全班 50 个同学的成绩排名,或者统计一篇文章里每个单词出现了多少次……这就需要一个"数据容器"来批量存储和处理。
下一模块 M3,你将解锁 数组、二维数组和字符串——让你的程序从"一次处理一个"升级为"一次处理一批"。从"计算器"迈向真正的"数据处理工具"。