技术笔记:算法1-1 模拟与高精度(NOIP经典真题解析)
模拟算法是编程竞赛中最基础、最常用的算法之一,核心思想是严格按照题目描述的规则,一步步还原问题的执行过程,不需要进行复杂的逻辑推导或优化,只需要准确、细致地实现题目要求。本文将通过三道NOIP经典真题(乒乓球11分制/21分制、扫雷游戏、玩具小人找眼镜),讲解模拟算法的实际应用,帮助理解模拟类题目的解题思路和代码实现技巧。
一、 乒乓球比赛结果统计(NOIP2003 普及组 T1)
题目核心要求
输入由W(华华得分)、L(对手得分)、E(结束标志)组成的字符串,分别按照11分制和21分制统计比赛结果:
- 一局比赛的结束条件:某方得分≥11(11分制)/≥21(21分制),且双方分差≥2
- 一局结束后立即开始下一局,最终输出所有已结束的局的比分,以及当前未结束局的比分
- 忽略E之后的所有字符
解题思路
- 首先读取输入,拼接成完整的有效字符串(仅保留E之前的W和L)
- 分别实现11分制和21分制的模拟统计:
- 初始化双方得分为0
- 遍历有效字符串,逐个更新得分
- 判断是否满足局的结束条件,满足则输出当前比分并重置得分
- 遍历结束后,输出最后一局未完成的比分
- 注意两部分结果之间用空行分隔
完整Java代码
java
package suan101;
import java.util.Scanner;
/**
* @author xuqiang
* @date 2026/2/3 15:50
* @description 乒乓球比赛结果统计(NOIP2003 普及组 T1)
*/
public class Main3 {
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
StringBuilder str = new StringBuilder(); // 用于拼接所有有效输入字符(W/L)
while (sc.hasNext()) {
String s = sc.next();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == 'E') { // 遇到E,终止输入读取,开始处理结果
go(str.toString());
sc.close();
return;
}
if (c == 'W' || c == 'L') { // 仅保留有效得分字符
str.append(c);
}
}
}
sc.close();
go(str.toString()); // 处理未遇到E的情况(输入末尾无E)
}
/**
* 核心模拟方法:分别处理11分制和21分制的比分统计
* @param s 有效得分字符串(仅包含W/L)
*/
public static void go(String s){
// 第一部分:11分制模拟
int w = 0,l = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if(c =='W') w++; // 华华得分+1
else l++; // 对手得分+1
// 11分制结束条件:某方≥11 且 分差≥2(关键判断,缺一不可)
if((w >= 11 || l >= 11) && Math.abs(w-l) >= 2){
System.out.println(w+":"+l);
w = 0; // 重置得分,开始下一局
l = 0;
}
}
System.out.println(w+":"+l); // 输出最后一局未完成的比分
System.out.println(); // 两部分结果之间的空行(符合题目输出格式)
// 第二部分:21分制模拟(逻辑与11分制一致,仅修改分数阈值)
w = 0;
l = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if(c =='W') w++;
else l++;
// 21分制结束条件:某方≥21 且 分差≥2(关键判断)
if((w >= 21 || l >= 21) && Math.abs(w-l) >= 2){
System.out.println(w+":"+l);
w = 0;
l = 0;
}
}
System.out.println(w+":"+l); // 输出最后一局未完成的比分
}
}
关键注意点
- 输入处理时,必须提前终止E之后的读取,且忽略非W/L/E的字符(符合题目附注要求)
- 局的结束条件是两个条件同时满足,缺一不可(例如11:10不满足分差≥2,不能结束一局)
- 两部分结果之间必须有且仅有一个空行,符合题目输出格式要求
二、 扫雷游戏(NOIP2015 普及组 T2)
题目核心要求
给定n行m列的雷区(*表示地雷,?表示非地雷),计算每个非地雷格周围8个方向的地雷数量,最终输出完整雷区(*保留,?替换为周围地雷数)。
解题思路
- 读取输入的雷区尺寸n和m,以及n行雷区数据,存储到二维数组中
- 遍历二维数组中的每一个格子:
- 若是地雷(*),直接保留,不做处理
- 若是非地雷(原?,代码中先替换为0),统计其上下左右、左上右上、左下右下8个方向的地雷数量
- 将统计的地雷数量转换为字符,替换原非地雷格,最终输出完整二维数组
完整Java代码
java
package suan101;
import java.util.Scanner;
/**
* @author xuqiang
* @date 2026/1/31 19:20
* @description 扫雷游戏(NOIP2015 普及组 T2)
*/
public class Main {
// 定义二维数组存储雷区,大小105*105(略大于题目最大100*100,避免边界判断越界)
private static char juzhen[][] = new char[105][105];
private static int n,m; // 雷区的实际行数和列数
/**
* 统计单个非地雷格周围的地雷数量并更新数组
* @param y 当前格子的行索引
* @param x 当前格子的列索引
*/
public static void countMine(int y,int x){
// 仅处理非地雷格(地雷格直接跳过)
if(juzhen[y][x] != '*'){
int count = 0; // 周围地雷数量计数器
// 遍历8个方向(dy: -1,0,1;dx: -1,0,1)
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
// 跳过当前格子本身(dy=0且dx=0)
if(dy == 0 && dx == 0) continue;
int nx = x + dx; // 相邻格子的列索引
int ny = y + dy; // 相邻格子的行索引
// 边界判断:确保相邻格子在雷区范围内(避免数组下标越界,关键)
if(ny >= 0 && ny < n && nx >= 0 && nx < m){
if(juzhen[ny][nx] == '*'){ // 相邻格子是地雷,计数器+1
count++;
}
}
}
}
// 将计数器转换为字符('0' + count 实现int到char的数字转换)
juzhen[y][x] = (char) ('0' + count);
}
}
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
sc.nextLine(); // 吸收换行符,避免读取后续字符串时出现异常
// 第一步:读取雷区数据,初始化二维数组
for (int i = 0; i < n; i++) {
String line = sc.next();
for (int j = 0; j < m; j++) {
juzhen[i][j] = line.charAt(j); // 提取每行的第j个字符,赋值给二维数组
// 非地雷格先替换为'0',方便后续统计更新
if(juzhen[i][j] == '?'){
juzhen[i][j] = '0';
}
}
}
// 第二步:遍历所有格子,统计非地雷格周围地雷数量
for (int i = 0; i < n; i++) {
for (int j = 0; j < m ; j++) {
countMine(i,j);
}
}
// 第三步:输出最终雷区结果
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
System.out.print(juzhen[i][j]);
}
System.out.println(); // 每行输出完毕后换行,符合题目格式
}
sc.close();
}
}
关键注意点
- 二维数组大小定义为105*105,是为了简化边界判断,避免因索引越界导致运行时异常
- 统计8个方向时,必须跳过当前格子本身(dy=0且dx=0),否则会误将当前格子计入统计
- int类型的地雷数量转换为char类型时,使用
'0' + count,利用ASCII码的特性实现快速转换(避免复杂的字符串拼接) - 读取输入时,注意吸收换行符,避免后续
sc.next()读取到空字符串
三、 玩具小人找眼镜(NOIP2016 提高组 D1T1)
题目核心要求
n个玩具小人围成一圈,每个小人有朝向(0朝内、1朝外)和职业,给定m条指令(向左/右数s个),模拟指令执行过程,最终输出到达的小人职业。关键规则:
- 朝向决定左右方向:
- 0(朝内):左=顺时针,右=逆时针
- 1(朝外):左=逆时针,右=顺时针
- 小人按逆时针顺序输入,围成一圈
- 从第一个读入的小人开始执行指令
解题思路
- 读取n和m,然后读取n个小人的朝向和职业,分别存储到两个数组中
- 初始化当前小人的索引为0(对应第一个读入的小人)
- 遍历每条指令,根据当前小人的朝向和指令内容,计算新的当前索引:
- 核心逻辑:判断指令方向与当前小人朝向是否一致,确定索引的增减方向
- 利用取模运算处理环形结构,避免索引越界
- 指令执行完毕后,输出当前索引对应的职业
完整Java代码
java
package suan101;
import java.util.Scanner;
/**
* @author xuqiang
* @date 2026/2/3 14:06
* @description 玩具小人找眼镜(NOIP2016 提高组 D1T1)
*/
public class Main2 {
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); // 玩具小人个数
int m = sc.nextInt(); // 指令条数
int[] dir = new int[n]; // 存储每个小人的朝向:0朝内,1朝外
String[] job = new String[n]; // 存储每个小人的职业
// 第一步:读取n个小人的朝向和职业
for (int i = 0; i < n; i++) {
dir[i] = sc.nextInt(); // 读取朝向
job[i] = sc.next(); // 读取职业
}
int curr = 0; // 当前所在小人的索引,初始为第一个读入的小人(索引0)
// 第二步:执行m条指令
for (int i = 0; i < m; i++) {
int a = sc.nextInt(); // 指令方向:0左数,1右数
int s = sc.nextInt(); // 指令数:数s个小人
// 核心逻辑:判断当前小人朝向与指令方向是否一致,确定索引移动方向
// 一致时,索引反向移动(s取负);不一致时,索引正向移动(s取正)
if(dir[curr] == a){
// 移动后索引 = (当前索引 - s + n) % n (+n是为了避免负数取模出现异常,关键)
curr = (curr - s + n) % n;
}else{
// 移动后索引 = (当前索引 + s) % n (环形结构,取模n保证索引在0~n-1范围内)
curr = (curr + s) % n;
}
}
// 第三步:输出最终当前小人的职业
System.out.println(job[curr]);
sc.close();
}
}
关键注意点
- 环形结构的索引处理:使用
% n保证索引始终在0 ~ n-1范围内,避免越界;当索引可能为负数时,先+ n再% n,确保结果为非负数 - 核心逻辑简化:无需纠结顺时针/逆时针的具体方向,只需通过当前小人朝向与指令方向是否一致来确定索引移动方向,大幅简化代码
- 输入顺序是逆时针,与数组索引顺序一致,直接存储即可,无需额外调整顺序
- 指令中的
s满足1≤s<n,无需处理s≥n的情况,简化了取模运算的复杂度
四、 模拟算法核心总结
- 模拟算法的核心是**"忠实还原题目规则"**,解题前需先理清题目中的所有条件和规则,避免遗漏关键细节(如局的结束条件、方向规则、边界判断)。
- 输入处理是模拟题的基础,需注意有效数据提取、换行符处理、结束标志判断,避免因输入问题导致程序异常。
- 边界处理是模拟题的常见坑点,需注意数组下标越界、环形结构索引、负数取模等问题,通常可通过扩大数组大小、取模运算、提前补正数值等方式解决。
- 代码实现时,可将重复逻辑提取为独立方法(如扫雷的统计方法、乒乓球的比分处理方法),提高代码可读性和可维护性。