Magician_201006 详解详析

题目背景

大家知道我说的这个Magician是哪个网站吗?

嘿嘿,实际上它的全称叫“编程魔法师”,是《信息学竞赛宝典》的配套网站。链接在这:编程魔法师icon-default.png?t=N7T8http://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。 

  • 33
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值