模拟
竞赛中有一类问题,被称为“模拟题”,设计程序完整地按照题目叙述的方式运行得到答案,这一类问题通常对思维与算法设计的要求不高,但要求扎实的编程基本功。
例题:P2670 [NOIP2015 普及组] 扫雷游戏
给出一个
的网格,有些格子埋有地雷。求问这个棋盘上每个没有地雷的格子周围(上、下、左、右和斜对角)的地雷数。
输入第一行的两个整数和 ,表示网格的行数和列数,接下来的 行,每行 个字符,描述了雷区中的地雷分布情况。字符 * 表示相应格子是地雷格,字符 ? 表示相应格子是非地雷格。 和 不超过 。
输出包含行,每行 个字符,描述整个雷区。用 * 表示地雷格,用周围的地雷个数表示非地雷格。
分析:根据题意,对于每个非地雷格的格子,只需要统计其八个边上方向的格子中的地雷数量。如果需要统计的格子的坐标是
一般情况下,竞赛程序中涉及的坐标系表示方法(b)相对于日常接触的直角坐标系(a)略有不同,比如在二维数组里,一般原点对应的是左上角,这样就能很方便地将行数和
没有必要列举出 8 个 if 语句逐一判定这个格子的 8 个方向是否有雷,只要预先定义好的这 8 个方向的格子相对于这个格子的偏移量,然后对这个偏移量数组循环即可。
还有一点需要注意的是边界问题。在枚举某些边界上的格子时,它的某些方向会超出这个二维阵列,如果不经过特殊处理很容易造成数组越界。有两种方法可以解决这个问题,一是对需要查询的坐标进行范围判断;二是可以在这个二维阵列外围加一圈“空白的虚拟非雷区”参与统计。
#include <cstdio> const int N = 105; char mine[N][N]; // 全局变量区定义的变量默认初始化为0,注意这个0是指'\0',而不是字符'0' int ans[N][N]; // 某点的8个方向相邻位置的偏移量 int dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1}; int dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1}; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%s", mine[i] + 1); } for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { for (int k = 0; k < 8; k++) { int x = i + dx[k], y = j + dy[k]; // 因为地雷数组占用的范围是[1~n][1~m],相当于外面有一圈“虚拟边框” if (mine[x][y] == '*') ans[i][j]++; } } } for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (mine[i][j] == '*') printf("*"); else printf("%d", ans[i][j]); } printf("\n"); } return 0; }
例题:P1042 [NOIP2003 普及组] 乒乓球
华华和朋友打乒乓球,得到了每一球的输赢记录:W 表示华华得一分,L 表示对手得一分,E 表示比赛信息结束。一局比赛刚开始时比分为 0:0。每一局中达到 11 分或者 21 分(赛制不同),且领先对方 2 分或以上的选手可以赢得这一局。现在要求输出在 11 分制和 21 分制两种情况下的每一局得分。输入数据每行至多有 25 个字母,最多有 2500行。
分析:根据题意,只需要对读入的内容进行统计即可。使用数组 rec
记录下从开始到结束的得分情况,如果是华华赢了就记为 1,反之记为 0,读到 E 的时候就直接退出读入过程,读到换行符时要忽略,同时记录他们一共打了多少球 len
。
然后分别对两种赛制进行计算。首先记分板上双方(c1
和 c2
)都是 0,如果华华赢了,c1
就增加 1,否则 c2
就增加 1,如果发现记分板上得分高的一方达到了赛制要求的球数,而且分差也足够,就将记分板的得分输出,同时记分板清零开始下一局。到最后还要输出正在进行中的比赛的得分。
#include <cstdio> #include <cmath> #include <algorithm> using std::max; using std::abs; const int N = 1e5 + 5; int rec[N]; int win[2] = {11, 21}; // 两种赛制的获胜得分 int main() { int len = 0; while (true) { char ch; scanf("%c", &ch); // 不断读入比赛记录 if (ch == 'E') break; // 注意ch可能会读到换行符,所以必须只记录W和L if (ch == 'W') rec[++len] = 1; // 华华赢 else if (ch == 'L') rec[++len] = 0; // 华华输 } for (int i = 0; i < 2; i++) { // 两种赛制循环 int c1 = 0, c2 = 0; for (int j = 1; j <= len; j++) { if (rec[j] == 1) c1++; else c2++; // 获胜者达到对应分数并且超出对手两分 if (max(c1, c2) >= win[i] && abs(c1 - c2) >= 2) { printf("%d:%d\n", c1, c2); c1 = c2 = 0; } } printf("%d:%d\n", c1, c2); // 还要输出最终的即时比分 if (i == 0) printf("\n"); } return 0; }
本题思路很简单,直接根据题意和生活常识模拟即可,但还是有一些需要注意的地方:
- 记录比赛记录的数组要开够,看清楚题目描述中所说的最多输入行数以及每行最多的字母数量。
- 读到 E 就可以停止读入了,后面的都忽略掉,同时遇到换行符之类的与比赛结果无关的字符也要忽略。
- 注意要分差 2 分及以上才能结算一局比赛的结果。
- 最后还要输出最后一局正在进行中比赛比分,就算是刚刚完成一局也要输出 0:0,以及第一种赛制和第二种赛制间有一个空行。
题目中的描述可能包含关键信息,所以一定要认真审题。
例题:P1563 [NOIP2016 提高组] 玩具谜题
个玩具小人依次逆时针围成一个圈,有些小人面向圈内,有些面向圈外。一开始第一个小人手里有眼镜。从这只小人开始进行 次传递。拿着眼镜的小人将眼镜传递给左手 / 右手边的第 个人。已知每个玩具小人的职业(长度不超过 的字符串)和朝向( 表示朝圈内, 表示超圈外),以及每次传递的方向( 表示往这只小人的左边, 表示往右边)和传过的玩具人数,求最后眼镜在谁手上,输出职业。
分析:如果模拟现实中击鼓传花的过程,一个传一个,模拟眼镜在谁的手上,考虑到数据范围很大,两重循环的做法会超时。
把手里拥有眼镜的第一个玩具小人称为
如果小人面朝外,那么方向和加减的关系就和上面一种情况反过来,但是依然需要调整到
#include <cstdio> const int N = 1e5 + 5; const int LEN = 15; struct Toy { // 使用结构体存储玩具的朝向和职业 int face; char job[LEN]; }; Toy t[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 0; i < n; i++) { scanf("%d%s", &t[i].face, t[i].job); } int now = 0; for (int i = 1; i <= m; i++) { int a, s; scanf("%d%d", &a, &s); if (t[now].face == 0 && a == 1) { // 朝向内,向右,编号+s now = (now + s) % n; } else if (t[now].face == 0 && a == 0) { // 朝向内,向左,编号-s now = (now + n - s) % n; } else if (t[now].face == 1 && a == 1) { // 朝向外,向右,编号-s now = (now + n - s) % n; } else if (t[now].face == 1 && a == 0) { // 朝向外,向左,编号+s now = (now + s) % n; } } printf("%s\n", t[now].job); return 0; }
这段代码还能进行一些精简,利用异或运算将四种情况合并,参考代码如下:
参考代码
#include <cstdio> const int N = 1e5 + 5; const int LEN = 15; struct Toy { // 使用结构体存储玩具的朝向和职业 int face; char job[LEN]; }; Toy t[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 0; i < n; i++) { scanf("%d%s", &t[i].face, t[i].job); } int now = 0; for (int i = 1; i <= m; i++) { int a, s; scanf("%d%d", &a, &s); int flag = (t[now].face ^ a) * 2 - 1; now = (now + n + flag * s) % n; } printf("%s\n", t[now].job); return 0; }
习题:P4924 [1007] 魔法少女小Scarlet
首先把
到 的正整数按照从左往右,从上至下的顺序填入 的二维数组中,并进行 次操作,使二维数组上一个奇数阶方阵按照顺时针或者逆时针旋转 。每次操作给出 个整数 ,表示把以第 行第 列为中心的 阶矩阵按照某种时针方向旋转,其中 表示顺时针, 表示逆时针。要求输出最终所得的矩阵。
参考代码
#include <cstdio> const int N = 505; int a[N][N], tmp[N][N]; int main() { int n, m; scanf("%d%d", &n, &m); int num = 0; for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { num++; a[i][j] = num; } } for (int i = 1; i <= m; i++) { int x, y, r, z; scanf("%d%d%d%d", &x, &y, &r, &z); // 涉及到的子矩阵范围是 (x-r,y-r)~(x+r,y+r) for (int j = x - r; j <= x + r; j++) { for (int k = y - r; k <= y + r; k++) { // 不能在原地旋转,需要先将旋转后的结果放到另一个临时数组 if (z == 0) { // 顺时针 // 在子矩阵中,原来的第一行旋转到结果的最后一列,第二行转到倒数第二列,…… // 原来的第一列旋转到结果的第一行,第二列转到第二行,…… tmp[x - r + k - (y - r)][y + r - (j - (x - r))] = a[j][k]; } else { // 逆时针 // 在子矩阵中,原来的第一行旋转到结果的第一列,第二行转到第二列,…… // 原来的第一列旋转到倒数第一行,第二列转到倒数第二行,…… tmp[x + r - (k - (y - r))][y - r + j - (x - r)] = a[j][k]; } } } // 最后再将旋转后的结果拿回来 for (int j = x - r; j <= x + r; j++) { for (int k = y - r; k <= y + r; k++) { a[j][k] = tmp[j][k]; } } } for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { printf("%d ", a[i][j]); } printf("\n"); } return 0; }
习题:P1328 [NOIP2014 提高组] 生活大爆炸版石头剪刀布
参考代码
#include <cstdio> int res[5][5] = { {0, -1, 1, 1, -1}, {1, 0, -1, 1, -1}, {-1, 1, 0, -1, 1}, {-1, -1, 1, 0, 1}, {1, 1, -1, -1, 0}}; // 制作结果表,1表示小A赢,-1表示小B赢 int a[205], b[205]; int main() { int n, na, nb; scanf("%d%d%d", &n, &na, &nb); // 利用取余的周期性,从0位置开始存储 for (int i = 0; i < na; ++i) scanf("%d", &a[i]); for (int i = 0; i < nb; ++i) scanf("%d", &b[i]); int sa = 0, sb = 0; for (int i = 0; i < n; ++i) { // 利用取余的周期性 int r = res[a[i % na]][b[i % nb]]; if (r == 1) ++sa; else if (r == -1) ++sb; } printf("%d %d\n", sa, sb); return 0; }
习题:P1067 [NOIP2009 普及组] 多项式输出
参考代码
#include <cstdio> #include <cmath> using std::abs; int main(){ int n; scanf("%d",&n); for (int i = n; i >= 0; i--) { int x; scanf("%d",&x); if (x != 0) { // 将每一项拆成几个部分 // 非最高项系数为正时要有加号 if (i != n && x > 0) printf("+"); // 系数为负数时必然有负号 if (x < 0) printf("-"); // 系数为1时不显示(常数项除外) if (abs(x) > 1 || i == 0) printf("%d", abs(x)); // 除了常数项以外需要有x if (i > 0) printf("x"); // 除了常数项和一次项以外都有指数 if (i > 1) printf("^%d", i); } } }
习题:P1098 [NOIP2007 提高组] 字符串的展开
参考代码
#include <cstdio> #include <iostream> #include <string> using std::string; using std::cin; using std::cout; bool islower(char ch) { return ch >= 'a' && ch <= 'z'; } bool isdigit(char ch) { return ch >= '0' && ch <= '9'; } int main() { int p1,p2,p3; string s; cin>>p1>>p2>>p3>>s; int len = s.size(); string ans = ""; for (int i = 0; i < len; i++) { if (s[i] != '-') { ans += s[i]; } else { // s[i]是减号 // 检查是否能够展开?题目条件: // 减号两侧同为小写字母或同为数字, // 且按照 ASCII 码的顺序,减号右边的字符严格大于左边的字符。 string tmp="-"; // 如果展开失败,就加减号 char l=s[i-1], r=s[i+1]; if (i-1>=0 && i+1<len) { // 得有两侧 if (islower(l)&&islower(r) || isdigit(l)&&isdigit(r)) { if (l < r) { // 符合展开条件,进行展开过程 tmp = ""; // 展开的字符实际上是s[i-1]+1~s[i+1]-1 if (p3==2) { // 逆序 for (char ch=r-1;ch>=l+1;ch--) { // 错误写法:直接让ch表达要加的字符 // 这样会破坏循环过程 // 正确方式:单独用一个变量表示加什么字符 char add=ch; if (p1==3) add='*'; if (p1==2 && islower(add)) add-=32; for (int j=1; j<=p2; j++) tmp+=add; } } else { for (char ch=l+1;ch<=r-1;ch++) { char add=ch; if (p1==3) add='*'; if (p1==2 && islower(add)) add-=32; for (int j=1; j<=p2; j++) tmp+=add; } } } } } ans += tmp; } } cout<<ans<<"\n"; return 0; }
习题:P1518 [USACO2.4] 两只塔姆沃斯牛 The Tamworth Two
参考代码
#include <cstdio> const int N = 15; // 上,右,下,左 顺时针方向 int dx[4] = {-1, 0, 1, 0}; int dy[4] = {0, 1, 0, -1}; char a[N][N]; bool vis[N][N][4][N][N][4]; // 牛处于某位置某方向与John处于某位置某方向的状态是否出现过 bool valid(int x, int y) { // 验证(x,y)位置是否能走 // 在地图范围内 + 不是障碍物 // 注意不能用空地来判断,因为一开始牛和John的位置也能走 return x >= 1 && x <= 10 && y >= 1 && y <= 10 && a[x][y] != '*'; } int main() { for (int i = 1; i <= 10; i++) { scanf("%s", a[i] + 1); } int x_cow, y_cow, d_cow = 0; int x_farmer, y_farmer, d_farmer = 0; for (int i = 1; i <= 10; i++) { for (int j = 1; j <= 10; j++) { // 寻找牛和John的初始位置 if (a[i][j] == 'C') { x_cow = i; y_cow = j; } if (a[i][j] == 'F') { x_farmer = i; y_farmer = j; } } } vis[x_cow][y_cow][0][x_farmer][y_farmer][0] = true; int ans = 0; // 时长 while (true) { // 模拟一分钟的变化 ans++; int x = x_cow + dx[d_cow]; int y = y_cow + dy[d_cow]; if (valid(x, y)) { x_cow = x; y_cow = y; } else { d_cow = (d_cow + 1) % 4; // 转向 } x = x_farmer + dx[d_farmer]; y = y_farmer + dy[d_farmer]; if (valid(x, y)) { x_farmer = x; y_farmer = y; } else { d_farmer = (d_farmer + 1) % 4; } if (x_farmer == x_cow && y_farmer == y_cow) { // 相遇了 break; } if (vis[x_cow][y_cow][d_cow][x_farmer][y_farmer][d_farmer]) { // 说明这个局面之前已经出现过了,再继续走下去也只是重复 // 不可能再有机会相遇了 ans = 0; break; } // 还未相遇,标记此局面 vis[x_cow][y_cow][d_cow][x_farmer][y_farmer][d_farmer] = true; } printf("%d\n", ans); return 0; }
习题:P1065 [NOIP2006 提高组] 作业调度方案
参考代码
#include <cstdio> #include <algorithm> using namespace std; const int INF = 1e9; int seq[405]; // 输入的安排顺序 int machine_id[25][25]; // machine_id[x][y]: 工件x的第y道工序在哪台机器上加工 int time_cost[25][25]; // time_cost[x][y]: 工件x的第y道工序的加工时间 int finish[25]; // finish[x]: 工件x已完成多少道工序 int end_time[25]; // end_time[x]: 工件x上一道工序的完成时刻 int machine[25][25][2]; // machine[x][y][0/1]: 机器x的第y个空闲时间段 // [0/1]分别为空闲开始时间和结束时间 // 初始时统一为 0 ~ 无穷 int sz[25]; // sz[x]代表机器x上有多少个空闲时间段 int main() { int m, n; scanf("%d%d", &m, &n); for (int i = 1; i <= m; i++) { machine[i][1][1] = INF; sz[i] = 1; } for (int i = 0; i < m * n; i++) scanf("%d", &seq[i]); for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) scanf("%d", &machine_id[i][j]); for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) scanf("%d", &time_cost[i][j]); int ans = 0; for (int i = 0; i < m * n; i++) { int cur = seq[i]; // 当前对哪个工件进行加工 int order = finish[cur] + 1; // 该工件即将进行第几道工序 int start_time = end_time[cur]; // 该工件在机器上至少要从哪个时刻开始可以加工 int mch_id = machine_id[cur][order]; // 该工件当前这道工序放在哪台机器上加工 int cost = time_cost[cur][order]; // 该工件当前这道工序所需的加工时间 // 寻找机器上符合要求的第一个可以用来加工的空闲时间段 for (int j = 1; j <= sz[mch_id]; j++) { int bg = machine[mch_id][j][0], ed = machine[mch_id][j][1]; // 该空闲时间段为[bg,ed] if (ed >= start_time) { // 至少要在start_time时刻之后 int available_time = ed - max(start_time, bg); if (available_time >= cost) { // 该空闲时间段可用来加工 int occupy_bg = max(start_time, bg); int occupy_ed = occupy_bg + cost; // 这次加工将占用[occupy_bg,occupy_ed]这个时间段 // 分类讨论: // 1.[bg,ed]全用完; // 2.[bg,ed]前面/后面被占用; // 3.[bg,ed]被拆成两段空闲时间 if (ed - bg == occupy_ed - occupy_bg) { // [bg,ed]被完全占用 // 此时这个空闲时间段将被完全占用,将其从空闲时间段里删除 for (int k = j; k < sz[mch_id]; k++) { machine[mch_id][k][0] = machine[mch_id][k + 1][0]; machine[mch_id][k][1] = machine[mch_id][k + 1][1]; } sz[mch_id]--; } else if (occupy_bg == bg) { // [bg,ed]的前半段被占用 // 此时直接更新这个空闲时间段的开始时间即可 machine[mch_id][j][0] = occupy_ed; } else if (occupy_ed == ed) { // [bg,ed]的后半段被占用 // 此时直接更新这个空闲时间段的终止时间即可 machine[mch_id][j][1] = occupy_bg; } else { // 此时[bg,ed]被拆成[bg,occupy_bg]和[occupy_ed,ed] machine[mch_id][j][1] = occupy_bg; // 将[occupy_ed+1,ed]插入到数组中 for (int k = sz[mch_id]; k > j; k--) { machine[mch_id][k + 1][0] = machine[mch_id][k][0]; machine[mch_id][k + 1][1] = machine[mch_id][k][1]; } machine[mch_id][j + 1][0] = occupy_ed; machine[mch_id][j + 1][1] = ed; sz[mch_id]++; } finish[cur]++; end_time[cur] = occupy_ed; if (occupy_ed > ans) ans = occupy_ed; break; } } } } printf("%d\n", ans); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?