一,链接

​点击打开游戏​​ 或者

(这个游戏叫yo拼图,有各种拼图模式,其中翻模式是点亮所有的灯的模式,

这是我找到的唯一可以自由设置维度的点亮所有的灯小游戏。)

点亮所有的灯_8皇后问题   

二,规则

游戏的棋盘可以多样化,本文只讨论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,那么全局状态一定是如下的形式:

点亮所有的灯_等价类_02

为什么只能是这个形式呢?

因为中状态经过这个全局操作之后必须要复原,所以可以从上而下推导出这个表格。

所以我们只需要枚举第一行的a,b,c,找出满足条件的全局操作(一定是存在的)即可。

举例来说:

点亮所有的灯_javascript_03

对于这个中状态,向程序输入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(点亮所有的灯)​​里面有用到。

十,四阶游戏的策略

点亮所有的灯_等价类_04

这个图看起来有些规律。

对于一个中状态,要列方程的话,需要根据这个图求出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次往下行操作复原。

图示:

开始:

点亮所有的灯_等价类_05

1,对第1行进行往下的行操作,即对第2行第1列进行点操作,操作之后如下:

点亮所有的灯_8皇后问题_06

2,对第2行进行往下行操作,即对第3行第2列、第3列进行点操作,操作之后如下:

点亮所有的灯_javascript_07

3,对第3行进行往下的行操作,这个时候一定复原了,如下:

点亮所有的灯_javascript_08

PS:

关于等价全局操作的分析,以及第十章发现的神器规律,在我的另一篇博客中有系统的分析。

参见:《点亮所有的灯》进阶分析——等价全局操作