puzzle(1311)《操作》点亮所有的灯

目录

一,点亮所有的灯

二,子问题性质

三,术语定义

四,解空间的结构

五,全局状态、全局操作的关系

六,编程求解

POJ 3279 Fliptile

POJ 1222 EXTENDED LIGHTS OUT

七,四阶、五阶策略

1,四阶策略

2,五阶策略

八,全局操作的等价类分析

1,问题

2,术语定义

3,编程枚举,找规律

4,理论分析

5,系数矩阵的规律

(1)枚举系数矩阵,观察规律

(2)系数矩阵的单项定义

(3)系数矩阵的按行释义和按列释义

(4)系数矩阵的按列构造方法

(5)证明规律四:系数矩阵都是对称矩阵

(6)证明规律五:系数矩阵都是中心对称矩阵

(7)用规律六推导规律四、规律五

6,遗留问题和思路梳理

7,9阶系数矩阵分析

8,编程求系数矩阵

9,高斯消元解方程组

(1)方程组AX=0

(2)方程AX=b

九,系数矩阵的规律

1,求系数矩阵

2,显示查看说明

3,形态规律

4,数值规律——1的个数

5,拓展形态规律——独立的1和聚拢的1的个数


一,点亮所有的灯

点击打开游戏 或者 资源分享汇总 里面的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行之外,肯定全部复原了。那么我们称此时的状态为中转状态。

8,可复原状态:经过若干次操作之后可以变成复原状态的状态,叫可复原状态,也叫正确状态。

四,解空间的结构

很显然解空间是一个复杂的图,而不是树,不过这个不是我们讨论的重点。

对于n*n的游戏,每个格子有2种状态,那么全局有2^(n*n)种状态。

这些状态中,不一定每个状态都是正确状态,所有的正确状态,按照一次转移定义的邻居,可以连成1个简单无向图,而且是连通的。

每个正确的状态都可以沿着某条路径转移到复原状态,对应的,复原状态也可以沿着这个路径转移到这个状态。

(以上皆为废话,重点来了)

这样的2条路径,方向相反但是边完全重合,他们对应的全局操作是完全一样的!

也就是说,对于一个可以复原的状态,如果它经过某个全局操作就复原了,那么对已经复原的状态再进行这个全局操作,又可以得到原本的全局操作。

PS:这其实是一个群。

五,全局状态、全局操作的关系

(1)

全局状态有2^(n*n)种,有些是可以复原的,有些是无法复原的。

全局操作也有2^(n*n)种,对于可以复原的状态,一定存在1个全局操作可以使得它复原。

对于无法复原的状态,一定不存在全局操作可以使得它复原。

对于已经复原的状态,进行任何一个全局操作,得到的皆为可以复原的操作。

所以说,存在一个十分自然的映射,从全局操作到全局状态的映射:

每个全局操作的映射结果为:复原状态经过这个全局操作得到的全局状态。

这个映射并不一定是双射,也就是说,可能有些全局操作是等价的,即2个全局操作对应同一个全局状态。

(2)

任何2个全局操作的叠加,得到的都是1个全局操作。

某些全局操作是等价的,比如:

01110  00000
10101  00000
11011  00000
10101  00000
01110  00000

这2个操作,显然是等价的。

所以,我们可以把所有的全局操作分成m个等价类

神奇的是,不难证明,每个等价类的元素数目都是一样的,所以,m是2^(n*n)的约数。

假设m=2^k,那么每个等价类都有2^t个元素,其中t=n*n-k

这些等价类,和所有的正确状态,刚好可以一一映射。

进一步,不难推出,0<=t<=n,因为所有和0矩阵等价的矩阵都可以由第一行确定。

六,编程求解

对于一个正确的状态,如果用枚举法求出合适的全局操作,那效率非常低。

如果维度是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

 题目:

Description

Farmer John knows that an intellectually satisfied cow is a happy cow who will give more milk. He has arranged a brainy activity for cows in which they manipulate an M × N grid (1 ≤ M ≤ 15; 1 ≤ N ≤ 15) of square tiles, each of which is colored black on one side and white on the other side.

As one would guess, when a single white tile is flipped, it changes to black; when a single black tile is flipped, it changes to white. The cows are rewarded when they flip the tiles so that each tile has the white side face up. However, the cows have rather large hooves and when they try to flip a certain tile, they also flip all the adjacent tiles (tiles that share a full edge with the flipped tile). Since the flips are tiring, the cows want to minimize the number of flips they have to make.

Help the cows determine the minimum number of flips required, and the locations to flip to achieve that minimum. If there are multiple ways to achieve the task with the minimum amount of flips, return the one with the least lexicographical ordering in the output when considered as a string. If the task is impossible, print one line with the word "IMPOSSIBLE".

Input

Line 1: Two space-separated integers: M and N 
Lines 2.. M+1: Line i+1 describes the colors (left to right) of row i of the grid with N space-separated integers which are 1 for black and 0 for white

Output

Lines 1.. M: Each line contains N space-separated integers, each specifying how many times to flip that particular location.

Sample Input

4 4
1 0 0 1
0 1 1 0
0 1 1 0
1 0 0 1

Sample Output

0 0 0 0
1 0 0 1
1 0 0 1
0 0 0 0

这个题目的意思是输入m,n,输入m*n的0-1矩阵,要求输出1个m*n的0-1矩阵。

除了尺寸不是正方形之外,其他的和我的描述都一样,输入全局状态(0-1矩阵),输出全局操作(0-1矩阵)

 题目的意思是,如果有多个可行的方案的话,输出字典序最小的方案。

但是标程里面的字典序有问题,详情:点击打开链接

因为前面是针对我自己玩游戏的情况,而这里是针对ACM编程,所以,虽然是同样的问题,但是代码略有区别。

在这里,我用到了状态压缩,即用1个整数表示1行。

不过这样写出来的代码确实很难看懂,

f(k)表示的是,一个k代表的行操作对应的行增量,即对某一行进行k代表的行操作,那么这一行会有什么影响。

比如说,如果k代表的是a b c d e,那么对应的影响就是a+b    a+b+c    b+c+d    c+d+e    d+e

这个影响,可以看成3个影响的叠加:(1)a b c d e(2)b c d e 0(3)0 a b c d

状态压缩之后就是f里面的计算方法了。

代码:

#include<iostream>
using namespace std;

int m, n, a;
int l[17];
int temp[17];

int f(int k)
{
	int t = (1 << n) - 1;
	return (k * 2 ^ k^k / 2)&t;
}

bool ok(int k)
{
	for (int i = 0; i < 17; i++)temp[i] = l[i];
	temp[0] = k;
	for (int i = 1; i < m; i++)
	{
		temp[i] = (temp[i] ^ f(temp[i - 1]));
		temp[i + 1] = (temp[i + 1] ^ temp[i - 1]);
	}
	return temp[m]==f(temp[m-1]);
}

void out(int k)
{
	for (int i = 0; i < 17; i++)temp[i] = l[i];
	temp[0] = k;
	for (int i = 1; i <= m; i++)
	{
		for (int j = 0; j < n; j++)
		{
			cout << (temp[i - 1] >> j) % 2;
			if (j < n - 1)cout << " ";
		}
		cout << endl;
		temp[i] = (temp[i] ^ f(temp[i - 1]));
		temp[i + 1] = (temp[i + 1] ^ temp[i - 1]);
	}
}


int main()
{
	cin >> m >> n;
	for (int i = 0; i < 17; i++)l[i] = 0;
	for (int i = 1; i <= m; i++)for (int j = 0; j < n; j++)
	{
		cin >> a;
		l[i] += (a << j);
	}
	for (int k = 0; k < (1 << n); k++)if (ok(k))
	{
		out(k);
		return 0;
	}
	cout << "IMPOSSIBLE" << endl;
	return 0;
}

AC了

POJ 1222 EXTENDED LIGHTS OUT

Description

In an extended version of the game Lights Out, is a puzzle with 5 rows of 6 buttons each (the actual puzzle has 5 rows of 5 buttons each). Each button has a light. When a button is pressed, that button and each of its (up to four) neighbors above, below, right and left, has the state of its light reversed. (If on, the light is turned off; if off, the light is turned on.) Buttons in the corners change the state of 3 buttons; buttons on an edge change the state of 4 buttons and other buttons change the state of 5. For example, if the buttons marked X on the left below were to be pressed,the display would change to the image on the right.


The aim of the game is, starting from any initial set of lights on in the display, to press buttons to get the display to a state where all lights are off. When adjacent buttons are pressed, the action of one button can undo the effect of another. For instance, in the display below, pressing buttons marked X in the left display results in the right display.Note that the buttons in row 2 column 3 and row 2 column 5 both change the state of the button in row 2 column 4,so that, in the end, its state is unchanged.


Note:
1. It does not matter what order the buttons are pressed.
2. If a button is pressed a second time, it exactly cancels the effect of the first press, so no button ever need be pressed more than once.
3. As illustrated in the second diagram, all the lights in the first row may be turned off, by pressing the corresponding buttons in the second row. By repeating this process in each row, all the lights in the first
four rows may be turned out. Similarly, by pressing buttons in columns 2, 3 ?, all lights in the first 5 columns may be turned off.
Write a program to solve the puzzle.

Input

The first line of the input is a positive integer n which is the number of puzzles that follow. Each puzzle will be five lines, each of which has six 0 or 1 separated by one or more spaces. A 0 indicates that the light is off, while a 1 indicates that the light is on initially.

Output

For each puzzle, the output consists of a line with the string: "PUZZLE #m", where m is the index of the puzzle in the input file. Following that line, is a puzzle-like display (in the same format as the input) . In this case, 1's indicate buttons that must be pressed to solve the puzzle, while 0 indicate buttons, which are not pressed. There should be exactly one space between each 0 or 1 in the output puzzle-like display.

Sample Input

2
0 1 1 0 1 0
1 0 0 1 1 1
0 0 1 0 0 1
1 0 0 1 0 1
0 1 1 1 0 0
0 0 1 0 1 0
1 0 1 0 1 1
0 0 1 0 1 1
1 0 1 1 0 0
0 1 0 1 0 0

Sample Output

PUZZLE #1
1 0 1 0 0 1
1 1 0 1 0 1
0 0 1 0 1 1
1 0 0 1 0 0
0 1 0 0 0 0
PUZZLE #2
1 0 0 1 1 1
1 1 0 0 0 0
0 0 0 1 0 0
1 1 0 1 0 1
1 0 1 1 0 1

和上一题一样。

#include<iostream>
#include<string>
#include<vector>
#include<iomanip>
#include<algorithm>
using namespace std;

int m, n, a;
int l[17];
int temp[17];

int f(int k)
{
	int t = (1 << n) - 1;
	return (k * 2 ^ k ^ k / 2) & t;
}

bool ok(int k)
{
	for (int i = 0; i < 17; i++)temp[i] = l[i];
	temp[0] = k;
	for (int i = 1; i < m; i++)
	{
		temp[i] = (temp[i] ^ f(temp[i - 1]));
		temp[i + 1] = (temp[i + 1] ^ temp[i - 1]);
	}
	return temp[m] == f(temp[m - 1]);
}

void out(int k)
{
	for (int i = 0; i < 17; i++)temp[i] = l[i];
	temp[0] = k;
	for (int i = 1; i <= m; i++)
	{
		for (int j = 0; j < n; j++)
		{
			cout << (temp[i - 1] >> j) % 2;
			if (j < n - 1)cout << " ";
		}
		cout << endl;
		temp[i] = (temp[i] ^ f(temp[i - 1]));
		temp[i + 1] = (temp[i + 1] ^ temp[i - 1]);
	}
}


int main()
{
	int t;
	cin >> t;
	for (int id = 1; id <= t; id++) {
		m = 5, n = 6;
		for (int i = 0; i <= 17; i++)l[i] = 0;
		for (int i = 1; i <= m; i++)for (int j = 0; j < n; j++)
		{
			cin >> a;
			l[i] += (a << j);
		}
		cout << "PUZZLE #" << id << endl;
		for (int k = 0; k < (1 << n); k++)if (ok(k))
		{
			out(k);
			break;
		}
	}
	return 0;
}

AC了

七,四阶、五阶策略

1,四阶策略

这个图看起来有些规律。

对于一个中转状态,要列方程的话,需要根据这个图求出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行进行往下的行操作,这个时候一定复原了,如下:

2,五阶策略

按照n色五阶的公式, 当n=2时,方程化为

八,全局操作的等价类分析

1,问题

有多少全局操作和0矩阵是等价的?

即,有多少个n阶0-1矩阵,使得每个元素的上、下、左、右的位置上的元素(如果存在)加上这个元素本身为偶数?

每个等价类都有2^t个元素,0<=t<=n,现在我们来求t

2,术语定义

(1)标准递推式

x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2

(2)递推所得矩阵

对于任意给定的第一行(元素为0,1),通过标准递推式唯一确定的n行n列矩阵。

(3)拓展递推所得矩阵

对于任意给定的第一行(元素为0,1),通过标准递推式唯一确定的n+1行n列矩阵。

(4)递推拓展行

拓展递推所得矩阵的最后一行。

(5)等价矩阵

如果2个递推所得矩阵x和y的递推拓展行相同,即对应的全局操作是等价的,那么x和y为等价矩阵。

(6)0等价矩阵

与0矩阵互为等价矩阵的矩阵

注1:标准递推式中,数组越界的项直接忽略

注2:任意第一行、递推所得矩阵、拓展递推所得矩阵都是一一对应的。

注3:“递推拓展行全为0” 等价于 “递推所得矩阵是0等价矩阵”

注4:求0等价矩阵的数量,其实就是要求递推拓展行全为0的条件

3,编程枚举,找规律

(1)思路

枚举第一行,判断递推拓展行是否全为0

(2)代码

#include<iostream>
using namespace std;

int main()
{
	int n, ans, x[30][30], num, k, i, j, flag;
	cout << "输入维度" << endl;
	while (cin >> n)
	{
		ans = 1;
		for (int number = 1; number < (1 << n); number++)
		{
			num = number, flag = 1;
			for (k = 0; k < n; k++)x[1][k + 1] = num % 2, num /= 2;
			for (i = 0; i < n + 2; i++)for (j = 0; j < n + 2; j++)
			{
				if (i == 0 || j == 0 || j == n + 1)x[i][j] = 0;
				else if (i > 1)x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2;
			}			
			for (i = 0; i < n; i++)if (x[n + 1][i + 1] != 0)flag = 0;
			if (flag)ans++;
		}
		cout << ans << " ";
	}
	return 0;
}

(3)输入n
1 2 3 …… 27 28

(4)输出0等价矩阵的数量
    (1-10)1 1 1 16 4 1 1 1 256 1
    (11-20)64 1 1 16 1 256 4 1 65536 1
    (21-28)1 1 16384 16 1 1 1 1

(5)分析规律

n从1到28,t的值依次是

(1-10)0 0 0 4 2 0 0 0 8 0

(11-20)6 0 0 4 0 8 2 0 16 0

(21-28)0 0 14 4 0 0 0 0

可以看到如下几条规律,

规律一:所有的t都是偶数

规律二:只有当n=4时t=n,其他情况下,t<n

规律三:t大部分是0,少部分是2的幂,极少数不是2的幂

4,理论分析

(1)思路

列出递推所得矩阵的通式,判断成为0等价矩阵的条件。

用n个变量表示第一行的n个数,根据递推式求出拓展递推所得矩阵的最后一行,判断每个数都为0的条件。

(2)举例

(2.1)n=3

递推所得矩阵:(省略了所有%2,下同)

递推拓展行为b+c, a+b+c, a+b

全为0的条件是a=b=c=0,只有一种情况,所以2^t=1,t=0

(2.2)n=4

递推所得矩阵:

递推拓展行为0,0,0,0,所以t=4

(2.3)n=5

拓展递推所得矩阵: (省略了所有加号,下同)

递推拓展行为bce,abc,abde,cde,acd

全为0的条件即bce=abc=abde=cde=acd=0,化简得a=e=b+c,b=d

所以2^t=4,t=2

(3)统一表示法——系数矩阵

以(2.3)为例,式子bce=abc=abde=cde=acd=0可以表示为方程组AX=0,其中A是方程组的系数矩阵,X是向量(a b c d e)的转置,即竖向量。

\begin{pmatrix} 0 &1 &1 &0 &1\\ 1 &1 &1 &0 &0 \\ 1 &1 &0 & 1 &1\\ 0 & 0 &1 &1 &1\\ 1 &0 &1 &1 &0 \end{pmatrix}\begin{pmatrix} a \\b \\c \\d \\e \end{pmatrix}=\begin{pmatrix} 0 \\0 \\0 \\0 \\0 \end{pmatrix}

满足条件的X向量的个数是2^(n-R(A)),其中R(A)是A矩阵的秩

同理可得出:

对于任意n和任意拓展递推所得矩阵,其递推拓展行中的每个数可以表述为拓展递推所得矩阵的第一行的若干个数的和(最少0个,最多n个)的形式,所以递推拓展行为0等价于对应的方程组AX=0

所以,我们得到,0等价矩阵的个数是2^(n-R(A)),即t=n-R(A)

5,系数矩阵的规律

(1)枚举系数矩阵,观察规律

当n=2,3,4,5时,系数矩阵分别是

\begin{pmatrix} 1 &0 \\ 0 & 1 \end{pmatrix} \begin{pmatrix} 0 &1 &1 \\ 1 &1 &1 \\ 1 & 1 &0 \end{pmatrix} \begin{pmatrix} 0 &0 &0 &0 \\ 0 &0 &0 & 0 \\ 0 & 0 &0 &0 \\ 0 &0 &0 &0 \end{pmatrix} \begin{pmatrix} 0 &1 &1 &0 &1\\ 1 &1 &1 &0 &0 \\ 1 &1 &0 & 1 &1\\ 0 & 0 &1 &1 &1\\ 1 &0 &1 &1 &0 \end{pmatrix}

规律四:系数矩阵都是对称矩阵

规律五:系数矩阵都是中心对称矩阵

规律六:系数矩阵都满足递推式x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j + 1]) % 2,即每个数周围的4个数加起来是偶数

要想证明规律五,首先要看系数矩阵的每个数代表什么。

(2)系数矩阵的单项定义

对于n阶系数矩阵A,用Aij表示A的第i行第j列,取值范围为0或1

Aij=1表示方程组AX=0的第i个方程中有X的第j个分量,即递推拓展行第i个数的表达式中有第j个变量

Aij=0表示递推拓展行第i个数的表达式中没有第j个变量

(3)系数矩阵的按行释义和按列释义

举例来说,

5阶系数矩阵的第1行是0 1 1 0 1,表明递推拓展行的第1个数的表达式中没有ad,有bce,即它的值为bce

5阶系数矩阵的第1列是0 1 1 0 1,表明递推拓展行的第1,4个数的表达式中没有a,第2,3,5个数的表达式中没有a

递推拓展行为bce,abc,abde,cde,acd

(4)系数矩阵的按列构造方法

这里首先要引入一个概念:

(4.1)单变量矩阵

如果一个拓展递推所得矩阵的第一行只有1个1,其他n-1个都是0,那么这个拓展递推所得矩阵我们称之为单变量矩阵。

如果单变量矩阵的第一行第i个数为1,其他为0,那么我们称之为第i个变量的单变量矩阵。

举例来说,n=5时,第2个变量的单变量矩阵是(空格子表示0,下同)

(4.2)单变量矩阵与系数矩阵的关系

不难发现,第i个变量的单变量矩阵的递推拓展行,转置变成列之后,与系数矩阵的第i列是一样的。

举例来说,上述n=5时,第2个变量的单变量矩阵表明,5阶系数矩阵的第2列是1 1 1 0 0

PS:在证明系数矩阵都是对称矩阵之前,这里还是不要搞错了,不是系数矩阵的第i行,而是系数矩阵的第i列。

(4.3) 系数矩阵的按列构造方法

枚举n个单变量矩阵,依次得到系数矩阵的每一列,即可得到系数矩阵

(5)证明规律四:系数矩阵都是对称矩阵

用递推拓展行可以从下往上把整个系数矩阵求出来,且计算方法和用第一行往下是一样的,所以系数矩阵是对称矩阵(关于主对角线对称)。

(6)证明规律五:系数矩阵都是中心对称矩阵

第i个变量的单变量矩阵和第n+1-i个变量的单变量矩阵是互相左右对称的

所以第i列和第n+1-i列是中心对称的,所以整个矩阵是中心对称的。

(7)用规律六推导规律四、规律五

首先,主对角线本身是关于主对角线对称的。

其次,由主对角线的前n-1个元素的上、下、左、右的位置上的元素(如果存在)之和为偶数依次可以推出,与主对角线相邻的两条斜线的n-1个数和n-1个数依次相等,即2条斜线对称。

接着再同理证明旁边2条斜线对称

......

最后同理证明左下角和右上角对称。

所以,整个矩阵关于主对角线对称,同理,整个矩阵关于副对角线对称。

所以整个矩阵也是中心对称矩阵。

PS:虽然规律六可以推导出四和五,但是规律六如何证明我还没想出来。

6,遗留问题和思路梳理

n从1到28,t的值依次是
(1-10)0 0 0 4 2 0 0 0 8 0
(11-20)6 0 0 4 0 8 2 0 16 0
(21-28)0 0 14 4 0 0 0 0

截止目前的遗留问题一共有4个:

规律一、规律二、规律三、规律六

现在,直接分析为什么t都是偶数,没什么思路,而且规律很可能是和2的幂有关,所以除了n=2,3,4,5的情况,我决定再看看n=9的情况。

7,9阶系数矩阵分析

9阶系数矩阵的计算量比较大,这里我们利用上述系数矩阵的按列构造方法来进行构造

n=9,第1,2,3,4,5个变量的单变量矩阵依次是

这样就得到了系数矩阵的前5列

因为系数矩阵是中心对称矩阵,所以只需要这5列就够了。

得到的系数矩阵:

所以t=8,对应的方程组是a+c+e+g+i=0


8,编程求系数矩阵

按照上述方法,可以在O(n^3)的时间内求出系数矩阵,时间复杂度大大降低。

代码:

#include<iostream>
using namespace std;

int n,x[303][303];

int main()
{
	for (n = 1; n < 300;n++)
	{
		cout << n << endl;
		for (int i = 1; i <= n;i++)
		{
			for (int k = 1; k <= n; k++)x[1][k] = (k==i);
			for (int i = 0; i < n + 2; i++)for (int j = 0; j < n + 2; j++)
			{
				if (i == 0 || j == 0 || j == n + 1)x[i][j] = 0;
				else if (i > 1)x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2;
			}
			for (int k = 1; k <= n; k++)cout<<x[n + 1][k];
			cout << "  ";
			for (int k = 1; k <= n; k++)cout << (x[n+1][k] ? '1' : ' ');//输出系数矩阵的转置矩阵
			cout << endl;
		}
	}
	return 0;
}

说明:

对于每个n,我输出了2个矩阵,左边的是系数矩阵的转置矩阵(因为我没有证明系数矩阵是对称矩阵,所以还是说清楚点比较好),右边的是只把1输出,0用空格代替。

输出:

看左边的可能还有点眼花缭乱,但是右边的矩阵就很明显,非常有规律。

关于这个规律有太多的东西要写,而且图片多篇幅长,所以我另写了一篇博客。

参考下文:《点亮所有的灯》进阶分析——系数矩阵的规律。

规律是有一些模糊的规律的,但是并没有发现统一的标准规律,也没有任何推理产物。

9,高斯消元解方程组

(1)方程组AX=0

这里的方程组,其实就是异或方程组,可以用高斯消元快速计算A的秩,即k的值,

高斯消元的时间复杂度为O(n^3),可以迅速求出对于不同的n时,k的值

代码:

#include<iostream>
using namespace std;

int n, x[303][303], y[303][303];//y存系数矩阵
bool flag[303];

int main()
{
	for (n = 1; n <= 300; n++)
	{
		//cout << n << endl;
		for (int i = 1; i <= n; i++)
		{
			for (int k = 1; k <= n; k++)x[1][k] = (k == i);
			for (int i = 0; i < n + 2; i++)for (int j = 0; j < n + 2; j++)
			{
				if (i == 0 || j == 0 || j == n + 1)x[i][j] = 0;
				else if (i > 1)x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2;
			}
			for (int k = 1; k <= n; k++)y[k][i] = x[n + 1][k];
		}
		int ans = 0;//y矩阵的秩
		for (int i = 1; i <= n; i++)flag[i] = true;
		for (int i = 1; i <= n; i++)for (int j = 1; j <= n; j++)
		{
			if (flag[j] && y[j][i])
			{
				for (int k = j + 1; k <= n; k++)if (flag[k] && y[k][i])
					for (int xi = 1; xi <= n; xi++)y[k][xi] ^= y[j][xi];
				flag[j] = false, ans++;
				break;
			}
		}
		cout << n - ans << " ";
		if (n % 10 == 0)cout << endl;
		if (n % 50 == 0)cout << endl;
	}
	return 0;
}

结果:

n从1到300,n-k分别是:

0 0 0 4 2 0 0 0 8 0
6 0 0 4 0 8 2 0 16 0
0 0 14 4 0 0 0 0 10 20
0 20 16 4 6 0 0 0 32 0
2 0 0 4 0 0 30 0 8 8

0 0 2 4 0 0 0 0 22 0
40 24 0 28 42 0 32 0 8 0
14 0 0 4 0 0 2 0 64 0
0 0 6 12 0 0 0 0 10 0
0 20 0 4 62 0 0 20 16 0

18 0 0 4 0 0 6 0 8 0
0 0 2 4 0 0 0 8 46 0
0 0 80 4 50 56 0 56 56 0
86 0 0 4 64 0 2 0 16 0
0 0 30 4 0 0 0 0 10 0

0 8 0 24 6 0 0 0 128 0
2 0 0 24 0 0 14 0 24 36
0 0 2 4 0 0 0 0 22 0
0 0 0 4 42 8 0 24 8 0
126 0 0 28 0 0 42 0 32 0

0 0 38 24 0 0 0 0 10 0
0 0 0 4 14 20 0 0 16 8
2 0 0 4 0 0 6 0 8 20
0 0 2 4 0 0 16 0 94 0
0 0 0 4 2 0 160 0 8 0

102 0 112 140 0 144 114 0 112 0
0 0 174 4 0 0 0 0 10 0
128 0 0 4 6 0 0 20 32 0
2 0 0 4 0 0 62 8 8 0
0 0 2 4 0 20 0 0 22 0

可以看出,确实全部是偶数,也确实大部分为0

(2)方程AX=b

这其实就是,把上面的代码,用枚举法解方程组改成用高斯消元法,得到的就是游戏本身的解。

至于代码,只要把上面的代码改一改就行了。

九,系数矩阵的规律

1,求系数矩阵

#include<iostream>
using namespace std;

int n,x[303][303];

int main()
{
	freopen("D:\\1.txt", "w", stdout);
	for (n = 1; n < 300;n++)
	{
		cout << n << endl;
		for (int i = 1; i <= n;i++)
		{
			for (int k = 1; k <= n; k++)x[1][k] = (k==i);
			for (int i = 0; i < n + 2; i++)for (int j = 0; j < n + 2; j++)
			{
				if (i == 0 || j == 0 || j == n + 1)x[i][j] = 0;
				else if (i > 1)x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2;
			}
			//for (int k = 1; k <= n; k++)cout << x[n + 1][k];
			//cout << "  ";
			for (int k = 1; k <= n; k++)cout << (x[n+1][k] ? '1' : ' ');//输出系数矩阵的转置矩阵
			cout << endl;
		}
	}
	return 0;
}

2,显示查看说明

(1)用重定向到文本,可以存到txt中,方便查看,比如搜索查看。

(2)控制台上的屏幕比例比较好,看起来比较舒服,而txt的1和空格所占长度不一样,看起来有些变形。

比如n=27时,控制台和txt的对比如下:

   

看最下面中间,控制台是真实的,有规律的空出来的三角形,而txt里面看不出来。

(3)使用旧版控制台来显示,字比较小,一屏能显示的行数更多

(4)当n较大时,控制台和txt都会发生换行,不利于查看,可以复制到word缩小字体查看。

3,形态规律

(1),n为偶数时,所有的1和1之间都是分散开来的,没有相邻的,n为奇数时,大部分1都会和某个1相邻,单独分布的1非常少

(2),当n+1为2的幂时,n阶系数矩阵非常有规律

         

这也就印证了我在《《点亮所有的灯》进阶分析——等价全局操作》一文中的猜想,系数矩阵的规律是和2的幂有关的。

经过我认真的思考,我发现这些矩阵可以用统一的规律来表述:

当n+1为2的幂时,系数矩阵都是用这个小矩阵平铺得到的:

平铺方法:

从右上角开始,往左往下平铺,直到铺满为止,然后保留主对角线以及对角线上面的部分,通过对称得到对角线下面的部分。

PS:虽然我没有证明系数矩阵都是对称矩阵,但是本文只阐述规律,没有推理,所以用对称来阐述是可以的。

其实对于很多n,都类似于用某种平铺,只是规律可能没这么明显。

(3),X的形状很普遍

一种是空心的大X,一种是实心的小X

   

n=19的情况下,平铺规律还是比较明显,以我画的X为基本单元,按照上面n=31,63的情况我描述的平铺方法,即可得到这个矩阵。

   

n=37的情况比较隐蔽,仔细看有个大X

4,数值规律——1的个数

代码:

#include<iostream>
using namespace std;

int n, x[303][303], s;

int main()
{
	//freopen("D:\\1.txt", "w", stdout);
	while (cin>>n)
	{
		//cout << n << endl;
		s = 0;
		for (int i = 1; i <= n;i++)
		{
			for (int k = 1; k <= n; k++)x[1][k] = (k==i);
			for (int i = 0; i < n + 2; i++)for (int j = 0; j < n + 2; j++)
			{
				if (i == 0 || j == 0 || j == n + 1)x[i][j] = 0;
				else if (i > 1)x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2;
			}
			//for (int k = 1; k <= n; k++)cout << x[n + 1][k];
			//cout << "  ";
			//for (int k = 1; k <= n; k++)cout << (x[n+1][k] ? '1' : ' ');//输出系数矩阵的转置矩阵
			for (int k = 1; k <= n; k++)if (x[n + 1][k])s++;
		}
		cout << s << " ";
	}
	return 0;
}

输出:

n从1到30:

1 2 7 0 16 12 27 20 25 28
68 36 81 56 115 66 136 60 175 80
217 126 284 126 297 180 303 204 400 234

目测没什么规律,或者说规律太复杂。

5,拓展形态规律——独立的1和聚拢的1的个数

这里只看行,即如果1个1的左边右边都是0或边界那就是独立的1,如果左边或右边有1那就是聚拢的1

基于系数矩阵是对称矩阵的规律,左边没有邻居就代表上下没有邻居,也就是独立的1

代码:

#include<iostream>
using namespace std;

int n, x[303][303], s1,s2;

int main()
{
	//freopen("D:\\1.txt", "w", stdout);
	for (n = 1; n < 300;n++)
	{
		//cout << n << endl;
		s1 = s2 = 0;
		for (int i = 1; i <= n;i++)
		{
			for (int k = 1; k <= n; k++)x[1][k] = (k==i);
			for (int i = 0; i < n + 2; i++)for (int j = 0; j < n + 2; j++)
			{
				if (i == 0 || j == 0 || j == n + 1)x[i][j] = 0;
				else if (i > 1)x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2;
			}
			//for (int k = 1; k <= n; k++)cout << x[n + 1][k];
			//cout << "  ";
			//for (int k = 1; k <= n; k++)cout << (x[n+1][k] ? '1' : ' ');//输出系数矩阵的转置矩阵
			for (int k = 1; k <= n; k++)if (x[n + 1][k])
			{
				if (x[n + 1][k - 1] == 0 && x[n + 1][k + 1] == 0)s1++;
				else s2++;
			}
		}
		//cout << s2 ;
		cout << s1 << " " << s2 << endl;
	}
	return 0;
}

结果:

1 0
2 0
0 7
0 0
2 14
12 0
3 24
20 0
25 0
28 0
10 58
36 0
3 78
56 0
0 115
66 0
12 124
60 0
0 175
80 0
21 196
126 0
18 266
126 0

,,,,,,

当n是2到300的偶数时,s2都是0,即没有聚拢的0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单词搜索迷宫(Word Search Puzzle)问题是一个经典的算法问题,其输入是一个二维的字符数组和一组单词,目标是找出字符数组网格中的所有单词。这些单词可以是水平的、垂直的或者是任意的对角线方向,所以需要查找8个不同的方向。解决这个问题的一种常见方法是使用回溯算法,具体步骤如下: 1. 遍历二维字符数组,对于每个字符,以其为起点开始搜索,搜索的方向包括水平、垂直和对角线方向。 2. 对于每个搜索到的单词,将其记录下来。 3. 重复步骤1和2,直到遍历完整个二维字符数组。 下面是一个使用C#语言实现的单词搜索迷宫算法的示例代码: ```csharp class WordSearchPuzzle { private char[,] grid; private HashSet<string> words; public WordSearchPuzzle(char[,] grid, HashSet<string> words) { this.grid = grid; this.words = words; } public void Solve() { int rows = grid.GetLength(0); int cols = grid.GetLength(1); for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { Search(i, j, new StringBuilder()); } } } private void Search(int row, int col, StringBuilder sb) { if (row < 0 || row >= grid.GetLength(0) || col < 0 || col >= grid.GetLength(1)) { return; } sb.Append(grid[row, col]); string word = sb.ToString(); if (words.Contains(word)) { Console.WriteLine("Found '{0}' at [{1}, {2}] to [{3}, {4}]", word, row, col, row - sb.Length + 1, col - sb.Length + 1); } if (word.Length < 3) { Search(row + 1, col, sb); Search(row - 1, col, sb); Search(row, col + 1, sb); Search(row, col - 1, sb); Search(row + 1, col + 1, sb); Search(row - 1, col - 1, sb); Search(row + 1, col - 1, sb); Search(row - 1, col + 1, sb); } sb.Remove(sb.Length - 1, 1); } } // 使用示例 char[,] grid = new char[,] { {'t', 'h', 'i', 's'}, {'w', 'a', 't', 's'}, {'o', 'a', 'h', 'g'}, {'f', 'g', 'd', 't'} }; HashSet<string> words = new HashSet<string>() { "this", "two", "fat", "that" }; WordSearchPuzzle puzzle = new WordSearchPuzzle(grid, words); puzzle.Solve(); ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值