一,链接
点击打开游戏 或者
(这个游戏叫yo拼图,有各种拼图模式,其中翻转模式是点亮所有的灯的模式,
这是我找到的唯一可以自由设置维度的点亮所有的灯小游戏。)
二,规则
游戏的棋盘可以多样化,本文只讨论n*n的棋盘。
游戏的规则是这样的:每个格子有4个邻居(如果在边界就只有3个邻居,4角落只有2个邻居),
每个格子有2种状态,亮的或者黑的。
每次点击任意一个格子,它和它的4个邻居(不存在就不管了,下文不再重述)就会改变状态。
现在让你来进行一系列不限步数的点击(可以反复点击同一格子),使得所有的格子均点亮。
(我希望我已经描述完全清楚了,如果没有,请点击上面的链接,很好玩的游戏)
三,子问题性质。
玩过魔方的人都知道,任何普通n(n>3)阶魔方都可以拼凑,转化成3阶魔方,最后用3阶魔方的方法复原。
虽然这肯定不是唯一方法,但是这是最有效的方法,玩n阶魔方的人几乎都是用这个方法。
但是这个问题有没有这么好的子问题性质呢?
很可惜,没有。只要稍微一想就知道,真的没有。
造成这种区别的关键是操作粒度的不同。魔方的操作粒度是任意的,可以转1层,可以转2层。。。
而本游戏中,操作是固定的(只有位置可选),操作给整个局面带来的影响也是固定的,都是与n无关的。
这个地方和N皇后问题很像:在一个N*N的棋盘上放置N个皇后,使其不能互相攻击。
如果你尝试把7皇后问题看成8皇后问题的子问题,那么,当这7个皇后放好了之后,第8个皇后就只能放在剩下的那一行剩下的那一列。没有选择的余地,很可能会因为对角线的冲突造成无解。
所以说,把7皇后问题看成8皇后问题的子问题是不可行的。本游戏亦如此。
四,术语定义
1,点操作:游戏规则中提到的操作,点击1个格子即为1次点操作
2,点状态:一个格子如果已经点亮(彩色的)那么称为好的状态,
如果未点亮(灰色的)那么称为坏的状态或者需要改变的状态。
3,行邻居:第i行的2个行邻居是第i-1行和第i+1行。如果i=1或者n,那么它就只有1个行邻居
4,(对第i(i<n)行进行的)往下行操作:对于第i行每个坏状态的格子,对在第i+1行中的那个邻居进行一次点操作。这样,第i行就复原了。
5,(对第i(i>1)行进行的)往上行操作:对于第i行每个坏状态的格子,对在第i-1行中的那个邻居进行一次点操作。这样,第i行就复原了。
6,全局操作:对n*n个格子中的每一个格子,要么就进行1次点操作,要么就不进行,这样的一系列操作称为1次全局操作。1个全局操作可以理解为1个大小为n*n的集合的子集,也可以理解为1个0-1矩阵。
7,中转状态:对于n阶的游戏的正确状态,依次对第1、2、3......n-1进行向下的行操作之后,除了第n行之外,肯定全部复原了。那么我们称此时的状态为中转状态。
五,解空间的结构
很显然解空间是一个复杂的图,而不是树,不过这个不是我们讨论的重点。
对于n*n的游戏,每个格子有2种状态,那么全局有2^(n*n)种状态。
这些状态中,不一定每个状态都能转移到复原状态(以下简称为可以复原的状态或者正确的状态)
所有的正确状态,按照一次转移定义的邻居,可以连成1个简单无向图,而且是连通的。
每个正确的状态都可以沿着某条路径转移到复原状态,对应的,复原状态也可以沿着这个路径转移到这个状态。
(以上皆为废话,重点来了)
这样的2条路径,方向相反但是边完全重合,他们对应的全局操作是完全一样的!
也就是说,对于一个可以复原的状态,如果它经过某个全局操作就复原了,那么对已经复原的状态再进行这个全局操作,又可以得到原本的全局操作。
六,全局状态与全局操作的关系
全局状态有2^(n*n)种,有些是可以复原的,有些是无法复原的。
全局操作也有2^(n*n)种,对于可以复原的状态,一定存在1个全局操作可以使得它复原。
对于无法复原的状态,一定不存在全局操作可以使得它复原。
对于已经复原的状态,进行任何一个全局操作,得到的皆为可以复原的操作。
所以说,存在一个十分自然的映射,从全局操作到全局状态的映射:
每个全局操作的映射结果为:复原状态经过这个全局操作得到的全局状态。
这个映射并不一定是双射,也就是说,可能有些全局操作是等价的,即2个全局操作对应同一个全局状态。
七,全局操作之间的关系
任何2个全局操作的叠加,得到的都是1个全局操作。
某些全局操作是等价的,比如:
01110 00000
10101 00000
11011 00000
10101 00000
01110 00000
这2个操作,显然是等价的。
所以,我们可以把所有的全局操作分成m个等价类。
神奇的是,不难证明,每个等价类的元素数目都是一样的。
所以,m是2^n的约数。
假设m=2^k,0<=k<=n,那么每个等价类都有2^(n-k)个元素。
这些等价类,和所有的正确状态,刚好可以一一对应。
八,编程之前的代数准备
对于一个正确的状态,如果用枚举法求出合适的全局操作,那效率非常低。
如果维度是6,那就要枚举2^36种全局操作,虽然中间会有很大剪枝,但是最后的效率还是很低。
因为我是在手机上玩这个游戏,所以可以先预处理一下,即化为中转状态。
这个时候再将最后一行的数据输入程序,输入也简单的多了。
这个时候来求合适的全局操作也很简单。
举例来说,如果n=3,那么全局状态一定是如下的形式:
为什么只能是这个形式呢?
因为中转状态经过这个全局操作之后必须要复原,所以可以从上而下推导出这个表格。
所以我们只需要枚举第一行的a,b,c,找出满足条件的全局操作(一定是存在的)即可。
举例来说:
对于这个中转状态,向程序输入1,1,1,程序就需要解这样的一个方程组:
b+c=1,a+b+c=1,a+b=1
这里的加都是异或,或者说最后都要%2
这里的b+c、a+b+c、a+b的得到方法,和前面的表格是一样的。
编程解这个方程,可以用高斯消元法求解异或方程,也可以暴力枚举。
解出来a=0,b=1,c=0
也就是说,先点击第一行第一列的格子,然后现在这个状态,她的中转状态即为复原状态。
也就是说,点击第一行第一列的格子之后,依次对第1、2、3......n-1进行向下的行操作之后,一定就能得到复原状态。
现在,编程就变得十分简单了。
九,代码
#include<iostream>
using namespace std;
int main()
{
int needlighten[10];
cout << "输入维度" << endl;
int n;//n阶点亮所有的灯
cin >> n;
cout << "点亮第1行,第2行。。。第" << n - 1;
cout << "行\n最后一行中,从左到右输入状态,1表示黑,0表示亮" << endl;
for (int nl = 0; nl<n; nl++) cin >> needlighten[nl];
int biao[12][12];
for (int number = 1; number<(1<<n); number++)
{
int num = number;
for (int k = 0; k<n; k++)
{
biao[1][k + 1] = num % 2;
num = num / 2;
}
for (int i = 0; i<n + 2; i++)for (int j = 0; j<n + 2; j++)
{
if (i == 0 || j == 0 || j == n + 1)biao[i][j] = 0;
else if (i != 1)biao[i][j] = (biao[i - 2][j] + biao[i - 1][j - 1] + biao[i - 1][j] + biao[i - 1][j + 1]) % 2;
}
bool buer = 1;
for (int u = 0; u<n; u++)if (biao[n + 1][u + 1] != needlighten[u])buer = 0;
if (buer)
{
cout << "点第一行的第";
for (int v = 0; v < n; v++)if (biao[1][v + 1])cout << v + 1 << " ";
cout << "个,再逐行完成即可";
}
if (buer)break;
}
system("pause > nul");
return 0;
}
有了这个代码,这个游戏玩起来就十分简单了。
但是,这不代表这个游戏就没有研究的价值了,相反,还有非常有意思的事情是代码显示不了的。
这个代码我没有用状态压缩,在 POJ - 3279 Fliptile(点亮所有的灯)里面有用到。
十,四阶游戏的策略
这个图看起来有些规律。
对于一个中转状态,要列方程的话,需要根据这个图求出4个表达式
(1)a+c+b+c+d+a+b+d,结果为0
(2)d+b+c+d+a+b+d+a+c+d,结果为0
(3)a+a+b+d+a+c+d+a+b+c,结果为0
(4)b+d+a+c+d+a+b+c,结果为0
我惊奇的发现,居然是4个0!
也就是说,除非是输入4个0,否则方程根本没有解。
更进一步,对于4阶的游戏,任何中转状态都是复原状态!
即:任何正确的状态,都可以通过3次往下行操作复原。
图示:
开始:
1,对第1行进行往下的行操作,即对第2行第1列进行点操作,操作之后如下:
2,对第2行进行往下行操作,即对第3行第2列、第3列进行点操作,操作之后如下:
3,对第3行进行往下的行操作,这个时候一定复原了,如下:
PS:
关于等价全局操作的分析,以及第十章发现的神器规律,在我的另一篇博客中有系统的分析。
参见:《点亮所有的灯》进阶分析——等价全局操作