关灯游戏的简介就不介绍了,还不了解的朋友可点击笔者的上一篇博文----关灯游戏 Lights out (一)(极速求解)
在这一篇给大家介绍“关灯游戏”的枚举算法。
枚举算法很简单,就是枚举所有开关操作,然后逐一检索经过各种操作后局面的最终状态,当灯全灭时输出解。
对一个m×n的灯阵,并不需要枚举整个局面2^(m×n)种开关组合。因为任意一种操作,首行的操作就决定了后面所有行的操作都是唯一确定的,不存在其它可能。
原因很简单,因为能影响第一行灯的亮/灭状态,只可能是第二行的操作组合,并且能让第一行灯全灭的操作是唯一的;接下来,能影响第二行灯的亮/灭状态,只可能是第三行的操作组合,并且能让第二行灯全灭的操作是唯一的...依此类推。所以,当第一行的开关操作确定了,为了使得每一行的灯全灭,后面所有行的开关操作也都确定了。
请看下面图示例子:
一个5×5的灯阵,假设一开始全部灯全亮,如上图所示。
假如某一种开关操作,它的第一行操作如上图所示(1代表对该灯开关操作一次,下同)。
第一行开关操作完毕,局面灯的亮/灭状态如上图所示。
此时我们看到,第一行两端盏两盏灯灭了,中间三盏灯还是亮的。为了使第一行灯全灭,接下来对第二行必须对中间三盏灯开关操作一次,并且不可以操作第二行两端两盏灯,第二行的操作是唯一确定的。(对以后所有行的操作依此类推)
操作完第二行后,局面灯的亮灭状态如上图所示。
操作完第三行后,局面灯的亮灭状态如上图所示。
操作完第四行后,局面灯的亮灭状态如上图所示。
操作完第五行后,局面灯的亮灭状态如上图所示。
当所有行都操作完毕后,检查一下局面,如果局面的灯全灭,则输出该解。如果局面还有灯亮,说明从一开始,第一行的操作就是错的。
上面图示例子,到最后局面的灯全灭了,表明我们找到了解法,就是最后一个图标出的操作组合。
通过上面分析,枚举时,只需要枚举第一行所有的操作组合,对一个m行n列灯阵,枚举量为2^n。时间复杂度是O(2^n),猛一看上去似乎这种算法效率太差。其实不然,因为开/关操作和灯的亮/灭可以对应计算机的二进制数据,这样一来,整个过程可以用位运算来完成,速度极快,求解20×20的灯阵是毫秒内的事情,应付游戏娱乐足够了。
枚举算法非常简单,易于编程实现,求解小关卡速度也很快,所以枚举算法是个不错的选择,没有必要动不动就用线性代数算法求解。
当第一行的开/关操作确定下来后,后面所有行的操作,都必须确保前一行的灯全灭,那么最后检查局面灯是否全灭时,只需要检查最后一行灯是否全灭即可。
所有行开关操作完毕时,如果局面灯全灭,输出该解。
枚举完第一行2^n种不同操作,如果每种操作,最后都无法使得灯全灭,则说明初始局面无解。
灯亮用1表示,灯灭用0表示;对任意一盏灯,有开关操作用1表示,没有操作用0表示(开关操作偶数次等价于0次,开关操作奇数次等价于1次)。
m行n列灯阵,可以用数组Lights[m]来表示灯的亮/灭状态,数组中的每一个数据表示一整行灯的亮/灭状态;用数组press[m]来表示开关操作,数组中的每一个数据表示一整行的开关操作情况。为了读入初始化局面,再增加一个initializtion[m]数组记录初始化灯阵的亮/灭状态。以上三个数组的数据,二进制长度都是n位。
编程很简单,核心代码不过十行左右。下面是首行枚举+位运算完整C++代码( 程序默认初始状态的灯全亮,搜索目标状态为灯全灭;可以读入初始化局面,自动判断有没有解,如果有解,将输出所有解):
#include
using namespace std;
long Row,Column,guide,statistic=0;
void specification()
{
int i;
cout<<"\n 关灯游戏(Lights out)解题程序\n\n";
cout<<" 本程序用于求解智力难题----“关灯游戏”,可以求解任意初始局面的灯阵,默认初始";
cout<<"状态灯全亮,求解的目标状态是灯全灭。\n";
cout<<" 解题开始前要设置行数和列数。行数不限,但列数受数值位数限制,不可以大于30。如";
cout<<"果列数不等于行数,那么最好设置行数大于列数,因为本程序行数没有限制,并且,求解一";
cout<<"个30行2列的局面,远远快于2行30列的局面。\n";
cout<<" 程序允许选择手动输入自定义初始布局。\n";
cout<<" 程序会自动判断局面有无解,如果有解,将给出所有解!\n\n";
for(i=0; i<80; i++)cout<<"=";
}
int choose(long *initializtion)
{
long i,j,opening,check=1;
char C;
cout<<"请选择,是否输入自定义初始局面? (Y/N):";
cin>>C;
if((C=='Y')||(C=='y'))
{
cout<<"\n请输入初始局面,1代表亮灯,0代表灭灯。灯之间用空格隔开,输完一行后用回车换行。\n";
cout<<"例如,输入一个2行3列,并且有两盏亮灯的初始局面为:\n0 0 1\n0 1 0\n";
for(i=0; i<80; i++)cout<<"-";
cout<<"\n";
for(i=0; i>opening;
initializtion[i]<<=1;
initializtion[i]+=opening;
}
check|=initializtion[i];
}
if(check==0) //检查用户输入的局面,如果灯已经全灭,则程序结束
cout<<"\n灯已经全灭。\n";
}
return check;
}
void solve(long *lights,long *press,long *initializtion)
{
long i,j,L,R,permutation,temp;
for(permutation=0; permutation>1;
lights[i]^=R;
}
if(lights[Row]==0) //检查局面,如果灯全灭,则输出解
{
cout<<"\n\n";
statistic++; //解数量计数器
for(i=1; i<=Row; i++) //输出解
{
temp=guide>>1;
for(j=0; j>=1;
}
cout<>Row;
cout<<"请输入列数:";
cin>>Column;
long *lights=new long [Row+2]; //lights[]用于保存灯的局面状态
long *press=new long [Row+2]; //press[]用于保存按灯位置
long *initializtion=new long[Row+2]; //initializtion[]用于保存用户输入初始化局面
if(Column>30||Row*Column==0) //这里限制用户输入列不可以大于30,或者行数和列数不可以等于0
{
if(Column>30)cout<<"\n列数不可超过30\n";
goto end;
}
guide=1<
上面程序,因为输入输出以及各种判断比较繁琐,导致代码量巨大。笔者曾打算为程序写一个图形界面,但考虑到这样使用起来也不是很复杂,而写GUI会很麻烦,于是作罢。
下面这是笔者用上面程序得到的统计表(独立解和最少操作次数的统计,需要对程序进行改动)