题目背景
大家知道我说的这个Magician是哪个网站吗?
嘿嘿,实际上它的全称叫“编程魔法师”,是《信息学竞赛宝典》的配套网站。链接在这:编程魔法师http://www.magicoj.com/site/index
网站中题量也是足够大家做的。
不多打广告,咱们进入正题。
题干
注:输出数据有误,应为19。
读题完毕,大概意思就是:
用一个(字符)数组表示完好电脑(*)、病毒侵入电脑(@)、有杀毒软件电脑的分布(#)。每小时病毒会向上、下、左、右四个方向入侵,装了杀毒软件的电脑没事,求过m小时后有几台电脑没事。
题面分析
这道题有很多人直接用算术法(或者别的新鲜的方法)解,但是……
毕竟作为一道有“基础算法”标识的题,那咱们应该想一想是什么算法……
有人回答:搜索。
当然,又是数组结构,又有一种模拟思路的倾向,很自然会想到搜索。
但搜索的根本性特征之一是什么?可能有多种解法。
这题有好几个答案吗?当然没有。
故此,这不是搜索,没有那么复杂,这就是一道纯纯粹粹的模拟题。
问题来了。
有的人在脑子里模拟了这样一个过程:每一个‘@’都会向外扩张,而且是一轮一轮的。
递归!
可以吗?当然可以。
但是请注意,这道题还有一个附加参数:总时间。
如果你不断进行递归,那需要在某个返回的时候计算小时数。但是若是逻辑出现问题,极难进行调整,甚至难以发现这种问题。
所以,容易失误的算法,一般不会第一考虑。
但是有人会说:“这道题的特点就是子问题嵌套子问题啊,不用递归岂不是太麻烦了?”
那么好,请仔细考虑以下优化后的算法方案。
原来递归的方式,是提供每一个坐标作为参数向外进行模拟扩张的操作,但递归只是写起来省事,并不代表执行效率就高(大家可以试一试,欢迎发在讨论区)。所以我可以每次进行单纯的查找,查找到'@'进行扩张,重复m次。最后在完成以后输出‘@’的总个数,收尾。
这个算法何尝不优秀呢?
那么既然前面说到,这个算法需要执行多次,所以当然是把它封装成函数。请注意,这是一个没有返回值,不含任何参数的算法函数。
注:这个函数虽然也可以以一个二维数组作为参数,但是请大家自己尝试,估计当大家按照大家想象的方式写出来以后,会遇到一种问题。
那么开始写吧。
算法实现
1.首先,开一个字符数组。由于有n<=100,所以无需太多,char arr[110][110]即可。
#include<iostream>
using namespace std;
char arr[186][186];
2.需要在堆空间定义m, n两个整型变量。然后写好main函数里面的输入等内容。
int m,n;
//something else
int main()
{
//ans的定义
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>arr[i][j];
cin>>m;
//something else
return 0;
}
3.最后要统计‘@’的个数,需要定义int ans=0。
4.按照我刚才的思路,同样一个查找操作要进行m次,实则用while循环的话,就可以写成
while(m--) //或者用for(int i=1;i<=m;i++)替代
expand(); //expand()就是马上要写的查找操作
好了,main函数就可以写成这样,请大家自行理解。
#include<iostream>
using namespace std;
char arr[186][186];
int m,n;
void expand()
{
//not finished
}
int main()
{
int ans=0;
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>arr[i][j];
cin>>m;
while(m--) expand();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(arr[i][j]=='@') ans++;
cout<<ans;
return 0;
}
那么最棘手的expand()怎么办?
1.遍历的初步实现,非常简单。
void expand()
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
//something else
}
2.遍历是为了做什么?很明显,为了进行填充。但是并不是说每一个位置都可以扩张。很明显如果一个位置的病毒想要扩张,需要满足三个条件:
①这个位置确确实实是病毒计算机。可以表示为某物==‘@’。
②欲要填充的位置没有杀毒软件。可以表示为某个周边位置==‘*’。
③这个位置确实是“周围位置”。举个例子,arr[2][2]是‘*’,那么请问arr[5][5]的'@'能够一次性扩张到那里吗?很明显不能。那么这里有两种方法,一种用两层循环操作,另外一种就是我使用的:
方向数组。
方向数组可以这样写:
int dir[4][2]={ {-1,0},{0,1},{1,0},{0,-1} };
4个数组中的两个元素分别代表四组坐标偏移量(注意:题目中指的扩张,是向上下左右四个方向生长),dir[][0]表示行号偏移量,dir[][1]表示列号偏移量。那么就可以写成这样。
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int k=0;k<4;k++)
if(arr[i+dir[k][0]][j+dir[k][1]]=='*'&&arr[i][j]=='@')
arr[i+dir[k][0]][j+dir[k][1]]='@';
那么,expand()就完成了。提交。
这题到这里就完了
吗?
没有。这个AC只是一个侥幸中的侥幸。
大家设想一个例子。
假设输入是这样的:
5
@****
*****
*****
*****
****@
1
这代表的是输入了一个5x5的表格,过1小时后查看侵入的情况。
那么过程应该是这样的:
从这样↓
@ | * | * | * | * |
* | * | * | * | * |
* | * | * | * | * |
* | * | * | * | * |
* | * | * | * | @ |
变成这样↓
@ | @ | * | * | * |
@ | * | * | * | * |
* | * | * | * | * |
* | * | * | * | @ |
* | * | * | @ | @ |
很明显答案是6。
但是如果按目前的算法,答案是:
问题出在哪了呢?
咱们输出一下最终的概况:
原因很简单。
回看原来的实现方法。
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int k=0;k<4;k++)
if(arr[i+dir[k][0]][j+dir[k][1]]=='*'&&arr[i][j]=='@')
arr[i+dir[k][0]][j+dir[k][1]]='@';
在从第一行第一个开始查找的时候,发现此处正是‘@’,按照原来的方法,很明显他进行了即时操作(就是每发现一个‘@’立刻执行扩展操作),那么这样的话,整个概况已经从这样↓
@ | * | * | * | * |
* | * | * | * | * |
* | * | * | * | * |
* | * | * | * | * |
* | * | * | * | @ |
变成了这样↓
@ | @ | * | * | * |
@ | * | * | * | * |
* | * | * | * | * |
* | * | * | * | * |
* | * | * | * | @ |
那当第一行第一个结束,重启第二个的时候,第二个因为'@'又被识别为病毒计算机。那么又进行以下的扩张↓
@ | @ | @ | * | * |
@ | @ | * | * | * |
* | * | * | * | * |
* | * | * | * | * |
* | * | * | * | @ |
然后走到第一行第三个,那么又成了:
@ | @ | @ | @ | * |
@ | @ | @ | * | * |
* | * | * | * | * |
* | * | * | * | * |
* | * | * | * | @ |
以此类推最后总会填满。
但是问题是,第一轮所填充出来新增的东西(我们把其称作第一轮的“产物”)所出现的产物并不能再次进行扩张,正所谓,每个‘@’只能扩张一次,所以,每一轮的产物,都是第二次操作的中心。所以,咱们要分清操作中心和产物。可以用不同的符号来表示,比如我用另外一种'&'来表示产物。但是,第一轮的产物,在第二轮会成为操作中心,故此,咱们将一轮扩张完全进行完(从[1][1]到[n][n]完全遍历完成)之后,再将'&'恢复为'@',统计个数就没问题了。(当然也有其他的方法,大家可以试试,欢迎发送在讨论区)
上代码
故此,修改后的真正正确的代码在这:
#include<iostream>
using namespace std;
char arr[186][186];
int dir[4][2]={ {-1,0},{0,1},{1,0},{0,-1} };
int m,n;
void expand()
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int k=0;k<4;k++)
if(arr[i+dir[k][0]][j+dir[k][1]]=='*'&&arr[i][j]=='@')
arr[i+dir[k][0]][j+dir[k][1]]='&';
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(arr[i][j]=='&') arr[i][j]='@';
}
int main()
{
int ans=0;
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>arr[i][j];
cin>>m;
while(m--) expand();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(arr[i][j]=='@') ans++;
cout<<ans;
return 0;
}
同样可以AC。但是这个AC,是真正的AC。