模拟

竞赛中有一类问题,被称为“模拟题”,设计程序完整地按照题目叙述的方式运行得到答案,这一类问题通常对思维与算法设计的要求不高,但要求扎实的编程基本功。

例题:P2670 [NOIP2015 普及组] 扫雷游戏

给出一个 \(n \times m\) 的网格,有些格子埋有地雷。求问这个棋盘上每个没有地雷的格子周围(上、下、左、右和斜对角)的地雷数。
输入第一行的两个整数 \(n\)\(m\),表示网格的行数和列数,接下来的 \(n\) 行,每行 \(m\) 个字符,描述了雷区中的地雷分布情况。字符 * 表示相应格子是地雷格,字符 ? 表示相应格子是非地雷格。\(m\)\(n\) 不超过 \(100\)
输出包含 \(n\) 行,每行 \(m\) 个字符,描述整个雷区。用 * 表示地雷格,用周围的地雷个数表示非地雷格。

分析:根据题意,对于每个非地雷格的格子,只需要统计其八个边上方向的格子中的地雷数量。如果需要统计的格子的坐标是 \((x,y)\),那么它左上角的坐标是 \((x-1,y-1)\)

image

一般情况下,竞赛程序中涉及的坐标系表示方法(b)相对于日常接触的直角坐标系(a)略有不同,比如在二维数组里,一般原点对应的是左上角,这样就能很方便地将行数和 \(x\) 对应起来,列数与 \(y\) 对应起来,比如第一行第二列可以表示成 \((1,2)\),便于存储和查询。

没有必要列举出 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

然后分别对两种赛制进行计算。首先记分板上双方(c1c2)都是 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;
}

本题思路很简单,直接根据题意和生活常识模拟即可,但还是有一些需要注意的地方:

  1. 记录比赛记录的数组要开够,看清楚题目描述中所说的最多输入行数以及每行最多的字母数量。
  2. 读到 E 就可以停止读入了,后面的都忽略掉,同时遇到换行符之类的与比赛结果无关的字符也要忽略。
  3. 注意要分差 2 分及以上才能结算一局比赛的结果。
  4. 最后还要输出最后一局正在进行中比赛比分,就算是刚刚完成一局也要输出 0:0,以及第一种赛制和第二种赛制间有一个空行。

题目中的描述可能包含关键信息,所以一定要认真审题。

例题:P1563 [NOIP2016 提高组] 玩具谜题

\(n \ (n<100000)\) 个玩具小人依次逆时针围成一个圈,有些小人面向圈内,有些面向圈外。一开始第一个小人手里有眼镜。从这只小人开始进行 \(m \ (m<100000)\) 次传递。拿着眼镜的小人将眼镜传递给左手 / 右手边的第 \(s \ (s<n)\) 个人。已知每个玩具小人的职业(长度不超过 \(10\) 的字符串)和朝向(\(0\) 表示朝圈内,\(1\) 表示超圈外),以及每次传递的方向(\(0\) 表示往这只小人的左边,\(1\) 表示往右边)和传过的玩具人数,求最后眼镜在谁手上,输出职业。

分析:如果模拟现实中击鼓传花的过程,一个传一个,模拟眼镜在谁的手上,考虑到数据范围很大,两重循环的做法会超时。

把手里拥有眼镜的第一个玩具小人称为 \(0\) 号小人(这里从 \(0\) 开始比较方便,最后一个小人是 \(n-1\) 号)。当他面朝内时,往右边传递 \(3\) 只小人,就会变成 \(0+3=3\) 号小人拥有礼物。如果 \(0\) 号小人往左边传递 \(3\) 只小人,就会变成 \(0-3=-3\) 号小人拿着眼镜,显然没有 \(-3\) 号小人。假设总共有 \(7\) 只小人,考虑 \(0\) 号小人和 \(7\) 号小人等价(因为 \(6\) 号小人的下一个就是 \(0\) 号小人),\(1\) 号小人和 \(8\) 号小人等价,……,可以推出:\(-3\) 号小人和 \(-3+7=4\) 号小人等价。面朝内的小人往右边数 \(s\) 个就是编号增加 \(s\),往左边数就是编号减少 \(s\)。如果得到的编号不在 \(0 \sim n-1\) 的范围内,则需要通过增减 \(n\) 调整到这个范围内。为了达到这个目的,可以使用取余数的方式。

如果小人面朝外,那么方向和加减的关系就和上面一种情况反过来,但是依然需要调整到 \(0 \sim n-1\) 的范围内。

#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

首先把 \(1\)\(n^2\) 的正整数按照从左往右,从上至下的顺序填入 \(n \times n \ (n \le 500)\) 的二维数组中,并进行 \(m \ (m \le 500)\) 次操作,使二维数组上一个奇数阶方阵按照顺时针或者逆时针旋转 \(90°\)。每次操作给出 \(4\) 个整数 \(x,y,r,z\),表示把以第 \(x\) 行第 \(y\) 列为中心的 \(2r+1\) 阶矩阵按照某种时针方向旋转,其中 \(z=0\) 表示顺时针,\(z=1\) 表示逆时针。要求输出最终所得的矩阵。

参考代码
#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;
}
posted @ 2024-08-01 08:14  RonChen  阅读(39)  评论(0编辑  收藏  举报