先上例题:
1.解题思路:为何是枚举
首先我们需要理清思路:对于整个矩阵来说,所有的按钮灯都只有两种状态:要么是打开的;要么是关闭的。这句话可不是废话文学。从这句话出发,我们就可以确定一种解题思路了:枚举。因为每个按钮或者被按下,或者不被按下,而由于按下一个已经被按过的按钮,即同一个按钮按下两次,会导致该按钮灯及其周边的灯回转到初始状态:也就是按两次等于没按。由此可得,对于这5*6个按钮,我们只需要枚举每一种情况即可。
但是明显2^30次枚举是不被接受的:10亿级别就坐等TLE吧。所以我们反过来考虑:如何把枚举的量级减小。
2.如何减少枚举次数?
假如我们可以确定这30个按钮其中29个的状态,那么剩余的一个是按下还是不按我们是可以推算出的:这是最后一个按钮,如果现在还有灯亮着,按下它一定会使灯全灭。因为前29个按钮状态已经固定是正确的,我们只需要枚举这最后一个按钮“按”与“不按”这两种情况即可(虽然剩两种简单清晰可见的情况根本不用枚举)。也就是说,我们可以只枚举2^29次而不是2^30次。沿着这种思路,我们考虑的问题简化成:最少需要枚举出多少种情况,才能按照当前枚举的情况,推算出剩余按钮的状态,使得所有灯全灭。
既然是最少,那我们就从枚举两种情况(按下按钮或不按下按钮)开始:假设其中只有一个按钮被按下,继而就可以确定所有的按钮是否该被按下了吗?答案是不能。我们用0来代表一个按钮灯是暗的,1来代表它是亮的。并且我们用0来代表不按该按钮,1表示按下。我们考虑样例的情况:
假设[1][1]处的按钮被按下,能否推算出整个矩阵上按钮的状态呢?现在[1][1]处按钮被按下:
此时我们要怎么确定[1][2]处的按钮是否应该被按下呢?
可以决定一盏灯是亮是暗的因素分别是:它自身是否亮暗,它本身和它四周的按钮是否被按下。换言之,我们要推算出一处的按钮是否要被按下,需要同时确定:它本身是亮是暗;它四周的按钮是否被按下。只有明确地知道以上两点,我们才能正确的推算出这个按钮是否要被按下。现在对于[1][2],我们虽然知道它是暗的,但是并不知道他周围[1][3]处和[2][2]处是否被按下。所以我们无法确定[1][2]处的按钮是否要被按下。
既然无法确定[1][2]处的按钮是否要被按下,那我们就将[1][2]一起纳入枚举范围:枚举[1][1]与[1][2]两处的按钮状态。但我们同样会面临[1][3]无法被确定的困境。所以我们需要枚举处一整行的情况:[1][1]-[1][6]的所有情况,共2^6种。然后根据第一行的按钮状态,确定第二行的状态,直到最后一行。
如果看到这里你还不理解为什么要枚举第一行的所有情况,没关系,我自己也不咋能看得懂,那我就要用人类的最终奥义——摸鱼来给你讲解了。
3.我只干昨天的活,今天的明天再做!
假设这个5*6的矩阵是一个大项目,由你这个苦逼打工人每天处理一行,工期五天。结果第一天你想:“就着30个破按钮,我明天一天就做出来,今天我直接一个鱼的摸!”于是你快快乐乐的在老板眼皮子底下摸了一天鱼,临走前随意按下了第一排的几个按钮就走了。
第二天,你忽然意识到:这是个难题。看到上面那张图,你陷入了沉思。一阵思考过后,你决定了:今天也还是摸鱼!
但是你想到如果自己一直这样摸下去,最后任务不完成,可就再也不能摸了。为了可持续性的竭泽而渔,你决定:今天不管第二行是啥,我只把第一行全都弄灭!明天再管第二行,一直到第五天,我把前四行都弄灭之后,只剩下一行弄不掉,就跟老板说“这个任务虽然很艰巨,但我还是在五天内就处理了80%!”于是第二天你摸了一整天鱼,临走前,你按下了第二行的按钮,把第一行的按钮全都通过第二行弄灭了。
第三天,第四天也是如此……
到了第五天,你打算今天把第四行的灯全都弄灭,于是你按了第五行的按钮。啪,整个矩形的灯全都灭了。你惊喜异常,开开心心的去提前交了项目,然后涨了工资……你的同事问你,为什么你可以轻轻松松就把整个矩形的灯全都灭掉,而他们就要枚举2^30次呢?你回答说:运气好罢了。
你确实是运气好,不过你一次蒙对的概率可不是2^30分之一,而是1/64。你在第一天按下的第一行按钮恰好是正确的,这也就使得你第二行的按钮一定是正确的。因为如果你第一行的按钮正确,那么就只有第二行的按钮可以灭掉第一行剩余的灯了。而想灭掉第一行的灯,第二行按钮的按法也是绝对正确的。层层递进下去,直到最后一行你按下的按钮都是正确的。
所以,我们只需要枚举第一行的情况,然后根据第一行的情况推算出第二行,根据第二行推算第三行……直到按下第五行的按钮后,如果所有的灯都被关闭,说明我们第一行的情况是正确的。反之,则需要枚举下一种情况。这样,我们只需要枚举2^6(64)次即可。
4.实现
既然每个按钮只有“1”和“0”两种状态,我们可以用二进制数字来表示第一行的情况。比如000001,就代表第一个按钮被按下,而其他的不按。101001,代表第一个,第四个,第六个按钮被按下。我们用数组mapmap记录初始灯的状态,ananan(按按按)数组来记录需要按下哪些按钮。对于每一次枚举,我们用数组mapcopy来模拟矩阵的变化。最后只需要判断矩阵mapcopy是否是全灭的,即可验证当前按下的按钮(ananan)是不是正确的。上代码!
#include<iostream>
#include<list>
#include<algorithm>
#include<vector>
#include<map>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
using namespace std;
int mapmap[10][10]={0};//用于记录初始灯的状态
int ananan[10][10]={0};//用于记录枚举当前按下的按钮
int mapcopy[10][10]={0};//用于模拟矩阵,判断每种按下情况
int judge()//判断当前是否还有灯亮着
{
for(int i=1;i<=5;i++)
{
for(int j=1;j<=6;j++)
{
if(mapcopy[i][j]==1)
{
return 0;//至少有一个灯是亮着的,说明不行,需要考虑其他情况
}
}
}
return 1;//没有一个灯是亮着的,说明按照这样去按下按钮可以解决问题
}
void an(int i)//在mapcopy中模拟按下按钮后的情况,参数i为当前行
{
for(int j=1;j<=6;j++)
{
if(ananan[i][j]==1)//如果这个按钮被按下了,就把它四周的灯都改变状态
{
mapcopy[i][j]=!mapcopy[i][j];
mapcopy[i-1][j]=!mapcopy[i-1][j];
mapcopy[i+1][j]=!mapcopy[i+1][j];
mapcopy[i][j-1]=!mapcopy[i][j-1];
mapcopy[i][j+1]=!mapcopy[i][j+1];
}
}
}
int main()
{
for(int i=1;i<=5;i++)//输入数据
{
for(int j=1;j<=6;j++)
{
scanf("%d",&mapmap[i][j]);
}
}
for(int i=0;i<=64;i++)//枚举64种情况
{
for(int a=1;a<=5;a++)//初始化拷贝的副本map与按钮状态
{
for(int b=1;b<=6;b++)
{
mapcopy[a][b]=mapmap[a][b];
ananan[a][b]=0;
}
}
int ii=i;
for(int j=6;j>=1;j--)//解析当前第一排按钮情况
{
ananan[1][j]=ii%2;//取出第一位
ii/=2;
}
an(1);//调用模拟按下按钮的函数
for(int j=2;j<=5;j++)//考虑从第二行到第五行,根据其上一行的情况决定本行哪些按钮被按下
{
for(int k=1;k<=6;k++)//遍历六个灯
{
if(mapcopy[j-1][k]==1)//如果它上面的灯亮着,那么应该按下这个按钮使其熄灭
{
ananan[j][k]=1;
}
else
{
ananan[j][k]=0;
}
}
an(j);//将本行的按钮情况传给模拟an(按)函数
}
if(judge())//判断是否还有灯亮着,如果没有了
{
for(int p=1;p<=5;p++)//输出当前按下情况
{
for(int q=1;q<=6;q++)
{
if(q==1)
{
printf("%d",ananan[p][q]);
}
else
{
printf(" %d",ananan[p][q]);
}
}
printf("\n");
}
return 0;
}
}
return 0;
}
AC截图:
5.优化与总结
这个矩阵是5*6的,那么同样他也是6*5的(废话)。所以我们从行来枚举或是从列来枚举,效果都是一样的。而从列来枚举则只需要枚举2^5次。不过从列来枚举代表着要处理六列而不是五行的模拟按按钮。所以在选取行枚举和列枚举上还需要考虑。同样,这个矩阵是对称的,所以我们可以事先判断这个矩阵中所有亮着的灯是否对称排布:如果是的话,那么对称两端按下的按钮也一定对称。
从这两点入手,也许可以解决一些时间复杂度。
摸了n多天鱼,重新开始学习的时候却被这道看似简单的题卡住了。于是继续摸了n多天鱼……隔几天后,当我理清思路重新思考这道题时,所谓的难度一下子降为0,甚至还用摸鱼的思想轻松解决。很多时候并不是题难,而是思路被打乱了。也许适当的摸鱼,真的能让工作和学习效率变得更高吧。