Skip to content

第 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 分钟写完交卷了。阿杰凑过去看——对方根本没列什么方程,就写了一个循环:

cpp
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 轮后输出比分。

代码

cpp
#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 次。单局规则:点数大的赢,平局各不得分。统计最终胜负和总分差。

cpp
#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 分钟后是几点几分?"

cpp
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 的倍数位置。问最后哪些灯亮着?

这类题的模拟思路:用一个数组记录状态,循环遍历更新状态。

cpp
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

cpp
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. 读代码:以下代码模拟了什么?

cpp
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 的棋盘(XO),判断当前局面谁赢了(三子连线即获胜),或者平局,或者对局未结束。

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-P1046https://hydro.ac/p/luogu-P1046循环模拟、条件判断
◆ 拓展luogu-P1567https://hydro.ac/p/luogu-P1567连续计数、状态变化
◆ 拓展luogu-P1980https://hydro.ac/p/luogu-P1980逐位拆解、嵌套统计
◆ 拓展luogu-P1420https://hydro.ac/p/luogu-P1420最长连号、状态重置
◆ 拓展luogu-P1320https://hydro.ac/p/luogu-P1320游程编码、压缩模拟
◆ 拓展luogu-P1307https://hydro.ac/p/luogu-P1307数字反转、边界处理
◆ 拓展luogu-P1089https://hydro.ac/p/luogu-P1089月度预算、状态追踪、break
◆ 拓展luogu-P1423https://hydro.ac/p/luogu-P1423等比累加、阈值判断

💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 7 道◆拓展题,覆盖不同变式和细节。


配套练习

共8题。模拟从简单计数→状态追踪→规则翻译→边界处理逐级深入。★核心(课堂必做) · ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心luogu-P1046https://hydro.ac/p/luogu-P1046循环模拟、条件判断
◆ 拓展luogu-P1567https://hydro.ac/p/luogu-P1567连续计数、状态变化
◆ 拓展luogu-P1980https://hydro.ac/p/luogu-P1980逐位拆解、嵌套统计
◆ 拓展luogu-P1420https://hydro.ac/p/luogu-P1420最长连号、状态重置
◆ 拓展luogu-P1320https://hydro.ac/p/luogu-P1320游程编码、压缩模拟
◆ 拓展luogu-P1307https://hydro.ac/p/luogu-P1307数字反转、边界处理
◆ 拓展luogu-P1089https://hydro.ac/p/luogu-P1089月度预算、状态追踪、break
◆ 拓展luogu-P1423https://hydro.ac/p/luogu-P1423等比累加、阈值判断

练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 7 道◆拓展题,覆盖不同变式和细节。

自查清单

  • [ ] 我能解释什么是模拟,它和枚举有什么不同
  • [ ] 我会用四步法读一道模拟题
  • [ ] 我能写出石头剪刀布和掷骰子的完整程序
  • [ ] 我知道时间模拟中"进位"的套路
  • [ ] 我会在模拟程序里加 cout 打印中间状态来调试
  • [ ] 我知道状态变量必须初始化
  • [ ] 我知道题目规则的顺序不能随便改动

🚀 下章预告

到目前为止,你写的程序能存的数据量都不大——几个变量、一个数组。但如果要处理全班 50 个同学的成绩排名,或者统计一篇文章里每个单词出现了多少次……这就需要一个"数据容器"来批量存储和处理。

下一模块 M3,你将解锁 数组、二维数组和字符串——让你的程序从"一次处理一个"升级为"一次处理一批"。从"计算器"迈向真正的"数据处理工具"。