通过模拟解决问题,这里的“模拟”指的是编写程序时,让程序按照题目的描述方式执行得到最终的答案,也就是程序来模拟题目中描述的问题处理方法。正因为如此,模拟并不是一种真正意义上的算法。但是模拟的技巧是关注问题的细节,做到完整处理每个技术细节。而竞赛时,能够用模拟解决的题目,往往是送分题。
因为模拟不是一种具体的算法,所以这里主要通过几个例题供大家举一反三,可以使用模拟方法处理的题目往往描述内容较多,需要我们仔细阅读充分梳理处理环节,编程时需要特别注意细节的处理。
一、P2615 神奇的幻方
分析:直接模拟题目中的填数过程即可。因为填写数字\(K\)时,要对前一个数字\(K-1\)的填写位置进行判断来决定本次填写位置,所以可以使用两个变量\(x,y\)来记录填数过程中每个数填写的位置\((x,y)\)。
#include<iostream> using namespace std; int a[40][40]; int main(){ int n,x,y; cin>>n; a[x=1][y=n/2+1] = 1; //填写数字1 for(int i=2;i<=n*n;i++){ //依次填写剩下的数字 if(x==1 && y!=n) a[x=n][++y] = i; else if(x!=1 && y==n) a[--x][y=1] = i; else if(x==1 && y==n) a[++x][y] = i; else if(!a[x-1][y+1]) a[--x][++y] = i; else a[++x][y] = i; } for(int x=1;x<=n;x++){ for(int y=1;y<=n;y++){ cout<<a[x][y]<<" "; } cout<<endl; } return 0; }
二、P1161 开灯
分析:使用数组来记录每个灯的状态(是否开着),借助循环来模拟\(n\)次操作过程即可:
#include<iostream> using namespace std; const int N = 500000; int b[N+10]; //全局数组,每个元素初值全为0,正好表示开始时所有灯是关着的 int main(){ int n; cin>>n; for(int i=1;i<=n;i++){ double a; int t; cin>>a>>t; for(int j=1;j<=t;j++){ int k = a*j; if(k<=N) b[k] = 1-b[k]; } } for(int i=1;i<=N;i++){ if(b[i]){ cout<<i<<endl; break; } } return 0; }
三、P2670 扫雷游戏
分析:对于雷区第\(x\)行\(y\)列的格子\((x,y)\)来说,如果这个格子不是雷,那么就要统计其周围8个格子雷的数量。这里的坐标系和周围8个格子的坐标描述如下图所示:
那么如何高效地描述\((x,y)\)周围的8个格子呢?我们来看下面的双重循环:
//对于格子(x,y),其周围(包括自身)的格子可以用如下双重循环来描述 for(int i=-1;i<=1;i++){ for(int j=-1;j<=1;j++){ if(x+i>=1 && x+i<=n && y+j>=1 && y+j<=m) //a[x+i][y+j]; } }
可以通过双重循环遍历雷区每一个格子\((x,y)\),如果格子不是雷,那么再使用上面的方法统计周围8个格子的雷数量:
#include<iostream> using namespace std; int a[110][110]; int main() { int n,m; char ch; cin>>n>>m; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>ch; a[i][j] = ch=='*'?1:0; //标记是否是雷 } } for(int x=1;x<=n;x++){ for(int y=1;y<=m;y++){ if(a[x][y]) { //该格子是雷,输出* cout<<"*"; continue; } //该格子不是雷,统计周围格子雷的数量 int cnt = 0; for(int i=-1;i<=1;i++){ for(int j=-1;j<=1;j++){ if(x+i>=1 && x+i<=n && y+j>=1 && y+j<=m) cnt += a[x+i][y+j]; } } cout<<cnt; } cout<<endl; } return 0; }
其实还可以使用数组来记录格子\((x,y)\)周围的8个格子\(x\)坐标和\(y\)坐标的相对于\((x,y)\)的变化情况,通过遍历数组就能在\((x,y)\)的基础上计算出周围8个方向的格子的坐标:
int dxy[8][2] = { {-1,-1}, //左上(x-1,y-1) {-1,0}, //正上(x-1,y) {-1,1}, //右上(x-1,y+1) {0,-1}, //左(x,y-1) {0,1}, //右(x,y+1) {1,-1}, //左下(x+1,y-1) {1,0}, //正下(x+1,y) {1,1}, //右下(x+1,y+1) }; for(int i=0;i<8;i++){ int nx = x+dxy[i][0],ny = y+dxy[i][1]; if(nx>=1 && nx<=n && ny>=1 && ny<=m) //a[nx][ny]; }
完整的参考程序如下:
#include<iostream> using namespace std; int a[110][110]; int dxy[8][2] = { {-1,-1}, //左上(x-1,y-1) {-1,0}, //正上(x-1,y) {-1,1}, //右上(x-1,y+1) {0,-1}, //左(x,y-1) {0,1}, //右(x,y+1) {1,-1}, //左下(x+1,y-1) {1,0}, //正下(x+1,y) {1,1}, //右下(x+1,y+1) }; int main() { int n,m; char ch; cin>>n>>m; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>ch; a[i][j] = ch=='*'?1:0; } } for(int x=1;x<=n;x++){ for(int y=1;y<=m;y++){ if(a[x][y]) { //格子是雷,输出* cout<<"*"; continue; } //格子不是雷,统计周围区域雷的数量 int cnt = 0; for(int i=0;i<8;i++){ int nx = x+dxy[i][0],ny = y+dxy[i][1]; if(nx>=1 && nx<=n && ny>=1 && ny<=m) cnt += a[nx][ny]; } cout<<cnt; } cout<<endl; } return 0; }
四、P1563 玩具谜题
分析:题目中给出的数据规模还是比较大的,如果使用双重循环来模拟整个过程(对于每条指令玩具小人一个接一个计数)会出现超时。使用两个数组分别记录每个玩具小人的朝向和职业,可以使用变量来记录指令执行后达到的玩具小人的编号\(j\),对于要执行指令的玩具小人,如果玩具小人的朝向值和指令的方向值相同(玩具小人朝向圈内并且向左数数,或者玩具小人朝向圈外并且向右数数),那么下一个玩具小人的编号是\(j-s\);否则下一个玩具小人的编号是\(j+s\)。此外就是一些细节的处理了:
#include<iostream> using namespace std; int dir[100000]; char name[100000][20]; int main() { int n,m,i,j,a,s; cin>>n>>m; for(i=0;i<n;i++){ cin>>dir[i]>>name[i]; } j = 0; //j记录指令执行后到达的小人对应的数组下标 for(i=1;i<=m;i++){ cin>>a>>s; s %= n; //避免s太大(超过n) if(dir[j]==a){ //玩具小人的朝向值和指令的方向值相同 j -= s; if(j<0) j += n; }else{ //玩具小人的朝向值和指令的方向值不同 j += s; if(j>=n) j -= n; } } cout<<name[j]<<endl; return 0; }
五、P1328 生活大爆炸版石头剪刀布
分析:本问题的难点在于如何高效地比较两人出拳结果,从题目描述中给出的表格可以得到提示,可以使用二维数组来记录两人出拳的比较结果:
int score[5][5] = { {0,0,1,1,0}, {1,0,0,1,0}, {0,1,0,0,1}, {0,0,1,0,1}, {1,1,0,0,0} };
二维数组就是根据题目游戏结果表格得出来的。有了这个二维数组,可知如果小A出拳是a,小B拳是b,那么本轮小A的得分是score[a][b],本轮小B的得分是score[b][a]。剩下的问题就是在\(n\)次循环中,根据两人的出拳规律得出每次的出拳情况:
#include<iostream> using namespace std; int score[5][5] = { {0,0,1,1,0}, {1,0,0,1,0}, {0,1,0,0,1}, {0,0,1,0,1}, {1,1,0,0,0} }; int loopa[210],loopb[210]; int main() { int n,na,nb; cin>>n>>na>>nb; for(int i=0;i<na;i++) cin>>loopa[i]; for(int i=0;i<nb;i++) cin>>loopb[i]; int sa = 0,sb = 0; //两人得分 int ia = 0,ib = 0; //两人出拳(在出拳规律的数组中的下标) for(int i=1;i<=n;i++){ sa += score[loopa[ia]][loopb[ib]]; sb += score[loopb[ib]][loopa[ia]]; ia ++;if(ia==na) ia = 0; ib ++;if(ib==nb) ib = 0; } cout<<sa<<" "<<sb<<endl; return 0; }