设计点灯游戏前的总结

因c语言程序设计实践课,恰好选择了对点灯游戏的实现,则我们先来归纳如何去求点灯游戏的方案。

零——前置芝士

点灯游戏简介

一层大楼共有 \(n×n\)

点灯游戏规律

我们不难发现以下规律

\(1.\)按偶数次按钮相当于没有按。

\(2.\)无论按按钮顺序如何结果总是一样的。

因此我们有以下结论

\(1.\)对于盘面上的每一个按钮,我们只需要考虑其按开或关的状态。

\(2.\)每一个按钮的状态都是互相独立的,不需要考虑按按钮的顺序。

壹——解决算法

1.完全穷举法,\(O(2^{n^2})\)

对于每一个按钮,只有开和关两种状态。

而如果所有按钮的状态确定了,那么灯的状态也就确定了。

那么,我们就将点灯的问题转化为了对按钮状态的统计,只需要将所有按钮的所有可能的状态列举出来,算出灯所对应的状态并判断灯是否全部点亮即可。

不难发现,一个按钮的状态有开关 \(2\) 种,因此x个按钮的状态则为 \(2^x\) 种。一个房间共有 \(n×n\) 个按钮,因此共 \(2^{n×n}\) 种状态。复杂度则为 \(O(2^{n^2})\)

\[注:此处应为 O(2^{n^2}+n^2),因为每个灯的计算还需要O(1),但是n^2远小于2^{n^2},\\故可忽略不记。 \]

2.首行穷举法, \(O(2^n)\)

对于算法1,当 \(n=6\) 时,状态已达到 \(2^{36}=68719476736\)

比如只按1或2个按钮的状态显然不成立,可以快速排除。

我们将整个房间的每一排从上到下看作有顺序的,那么从第一行开始按按钮。

假设我们已经决定了第一行按钮的状态,此时只有第一行和第二行的灯的状态发生了改变。

由于只有第一行和第二行才会改变第一行灯的状态,那么为了使第一行全亮,而且此时第一行按钮状态已确定,我们只能通过改变第二行按钮的状态来使第一行全亮。

同理,为了使第二行全亮,因第二行按钮已确定,所以要改变第三行按钮的状态,以此类推,整个房间的按钮都被第一行的按钮所确定,而房间里除了最后一行也都是全亮的。

因此,我们不需要把房间里所有的按钮可能的状态列出来,只需第一行。对于每一种第一行的状态,再按上面的步骤计算后面 \(n-1\)

第一行共 \(n\) 个灯,因此共 \(2^n\) 种状态。复杂度为 \(O(2^n)\)

3.完全方程法,\(O(n^6)\)

虽然算法2已经将复杂度降到了 \(O(2^n)\) ,但是当 \(n>30\)

在我们从 \(1 \rightarrow 2\) 时,发现了一行灯的状态由它的上一行,这一行和下一行按钮决定,而不是所有按钮,是行与行的关系。

这启发我们是不是对于同一行的按钮或者灯的状态也能找出来关系呢?

答案是肯定的。我们知道,一个按钮可以影响它和它周围4个灯的状态,反过来,一个灯的状态则由它本身和周围最多4个按钮决定的。

\[\color{gold}{——————————奇迹时间到——————————} \]

我们假设方块为按钮,圆圈为灯,黑表示开,白表示关。

再假设一个灯由两个按钮决定状态,那么具体可以表示为:

\[\Box+\Box= \circ\\ \blacksquare+\Box=\bullet\\ \Box+\blacksquare=\bullet\\ \blacksquare+\blacksquare= \circ \]

看起来好像可以转化为数学式子,假设

\[\Box=0,\blacksquare=1,\circ=1,\bullet=1 \]

那么,上面四个等式可转化为

\[0+0=0\\ 1+0=1\\ 0+1=1\\ 1+1=0 \]

欸,这个+好像不是加法,而是(逻辑上的)异或运算 \(\oplus\)

现在,把灯转化为一般情况,那么就由本身以及上下左右四个按钮来决定灯的状态。例如

\[\Box12+\Box21+\Box22+\Box23+\Box32=\circ22 \]

那么,对于房间里所有按钮和灯,我们可以列出 \(n*n\) 个式子,而且因为我们的目的是将所有灯点亮,那么灯的状态应均为 \(\bullet\) ,若此时以按钮为未知数,即为 \(n^2\)

我们只需要解方程就可以了(✿◡‿◡)

说到解方程,大家可能立马会想到矩阵。没错,我们把方程式表示成矩阵的形式,然后对矩阵高斯消元即可。

高斯消元的过程等同于求逆矩阵,而所得的矩阵的每一行就表示需要单独点亮一个灯,需要按哪些按钮。

而且可以通过矩阵秩的运算求出多解,这里不过多说明。

高斯消元本身的复杂度是 \(O(n^3)\) ,而矩阵边长为 \(n^2\) ,所以总时间复杂度为 \(O(n^6)\)

4.首行方程法, \(O(n^3)\)

如同从1到2,3到4也是实现了由整体到第一行,不再过多说明。

高斯消元复杂度为 \(O(n^3)\) ,由第一行按钮推算全部的按钮复杂度为 \(O(n^2)\) ,总体为 \(O(n^3)\)

5.n不同时解的数量

挖个坑,学完线性代数再回来填。

秩真不明白。

贰——点灯游戏代码的实现

这里使用的是第二种方式——首行穷举法

#undef UNICODE
#undef _UNICODE//vs默认unicode会使一些easyx的函数用不了,需要取消

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include<graphics.h>//下载easyx才能使用的图形库
#include<conio.h>
#include<mmsystem.h>
#pragma comment(lib,"winmm.lib")
#define WIN_WIDTH 640
#define WIN_HEIGHT 480

int GRID_NUM; //每一行每一列的格子数
struct Grid //格子
{
	int top; //上面一条线的x坐标
	int down;//下面一条线的x坐标
	int left;//左边一条线的y坐标
	int right;//右边边一条线的y坐标
	int foot; //步数
	int map[50][50];
}grid;
IMAGE img;
//开始界面
int GRID_WIDTH = 30;
void Welcome()
{
	settextcolor(RGB(156, 156, 156));
	settextstyle(64, 0, "黑体");
	outtextxy(70, 50, "   点灯游戏");
	settextcolor(RGB(137, 104, 205));
	settextstyle(16, 0, "宋体");
	outtextxy(100, 220, "每点一个格子,上下左右的格子也会做出于现状相反的动作");
	settextstyle(16, 0, "黑体");
	outtextxy(400, 320, "made by 张岩森");
	outtextxy(400, 340, "(*^-^*)");

	while (!_kbhit())
	{
		settextcolor(RGB(rand() % 256, rand() % 256, rand() % 256));
		outtextxy(200, 400, "按任意键继续 ");
		Sleep(200);
	}
	int n = _getch();//按任意键继续
	cleardevice();
	settextcolor(WHITE);
	outtextxy(70, 50, "输入你想看到的长度 N ");
	scanf_s("%d", &GRID_NUM);

	if (GRID_NUM > 13) GRID_WIDTH = GRID_WIDTH * 13 / GRID_NUM;
}
void GameInit()
{
	// 游戏区域大小,屏幕中心位置x,y,减去总格子宽度的一半
	grid.left = WIN_WIDTH / 2 - GRID_WIDTH * GRID_NUM / 2;
	grid.right = WIN_WIDTH / 2 + GRID_WIDTH * GRID_NUM / 2;
	grid.top = WIN_HEIGHT / 2 - GRID_WIDTH * GRID_NUM / 2;
	grid.down = WIN_HEIGHT / 2 + GRID_WIDTH * GRID_NUM / 2;
	grid.foot = 0;
	for (int i = 0; i < GRID_NUM; i++)
	{
		for (int k = 0; k < GRID_NUM; k++)
		{
			grid.map[i][k] = 1;
		}
	}
}
void GameDraw()
{
	cleardevice();
	putimage(0, 0, &img);
	// 绘制格子
	setlinecolor(RGB(151, 255, 255));
	//循环画格子
	for (int x = grid.left; x <= grid.right; x += GRID_WIDTH)
	{
		line(x, grid.top, x, grid.down);
	}
	for (int y = grid.top; y <= grid.down; y += GRID_WIDTH)
	{
		line(grid.left, y, grid.right, y);
	}
	// 外边框
	for (int x = 20; x > 10; x--)
	{
		line(grid.left - x, grid.top - x, grid.right + x, grid.top - x); //画上面的线
		line(grid.left - x, grid.down + x, grid.right + x, grid.down + x);//画下面的线
		line(grid.left - x, grid.top - x, grid.left - x, grid.down + x);//画左面的线
		line(grid.right + x, grid.top - x, grid.right + x, grid.down + x);//画右面的线
	}
	//绘制格子颜色
	int x, y;
	for (int i = 0; i < GRID_NUM; i++)
	{
		for (int k = 0; k < GRID_NUM; k++)
		{
			x = i * GRID_WIDTH + grid.left;
			y = k * GRID_WIDTH + grid.top;
			if (grid.map[i][k] == 1)
			{
				setfillcolor(BLACK);
				solidrectangle(x + 1, y + 1, x + GRID_WIDTH - 1, y + GRID_WIDTH - 1);//加1减一是为了让每个格子的边框显示出来
			}
			else if (grid.map[i][k] == -1)
			{
				setfillcolor(RGB(255, 250, 205));
				solidrectangle(x + 1, y + 1, x + GRID_WIDTH - 1, y + GRID_WIDTH - 1);//加1减一是为了让每个格子的边框显示出来
			}
		}
	}
	char foot[100] = "";
	sprintf_s(foot, " n=%d 步数:%d", GRID_NUM, grid.foot);
	settextcolor(WHITE);
	outtextxy(10, 10, foot);
}

void GameControl(int x, int y)
{
	grid.map[x][y] = -grid.map[x][y];
	if (x >= 0 && x < GRID_NUM - 1)grid.map[x + 1][y] = -grid.map[x + 1][y];//右边变色
	if (x > 0 && x <= GRID_NUM - 1)grid.map[x - 1][y] = -grid.map[x - 1][y];//左边变色
	if (y >= 0 && y < GRID_NUM - 1)grid.map[x][y + 1] = -grid.map[x][y + 1];//下边变色
	if (y > 0 && y <= GRID_NUM - 1)grid.map[x][y - 1] = -grid.map[x][y - 1];//上边变色
	grid.foot++;
}
int GameJudge()
{
	for (int i = 0; i < GRID_NUM; i++)
	{
		for (int k = 0; k < GRID_NUM; k++)
		{
			if (grid.map[i][k] == 1)
			{
				return 0;
			}
		}
	}
	return 1;
}

#define MAX_ROW 32
#define MAX_RESULT_NUM 300

typedef struct _Matrix
{
	unsigned long mtrx[MAX_ROW];
	int width;
	int height;
}Matrix;

int Cacu_Line(unsigned long line, int width) //计算一行
{
	int i = 0, sum = 0;
	unsigned long mark = 1;
	for (i = 0; i < width; ++i)
	{
		sum += ((line & (mark << i)) ? 1 : 0);
	}
	return sum;
}

int Cacu_Solution(Matrix* result) //计算一个解的步数
{
	int i = 0, sum = 0;
	for (i = 0; i < result->height; ++i)
	{
		sum += Cacu_Line(result->mtrx[i], result->width);
	}
	return sum;
}

void ProcessRow(Matrix* m, int r_num, unsigned long r_mode)
{
	unsigned long mask;
	int i = 0;
	for (i = 0; i < m->width; ++i)
	{
		mask = 1 << i;
		if ((r_mode & mask) != 0)
		{
			if (r_num > 0) m->mtrx[r_num - 1] ^= mask;
			if (r_num + 1 < m->height) m->mtrx[r_num + 1] ^= mask;

			mask = mask | (mask << 1) | (mask >> 1);
			m->mtrx[r_num] ^= mask;
		}
	}
}

void Solve(Matrix* m, Matrix** result_list)
{
	unsigned long i = 0;
	int j = 0;
	int total_count = (int)pow(2, m->width); //共需枚举的次数
	int counter = 0;

	Matrix* result = (Matrix*)malloc(sizeof(Matrix));
	result->width = m->width;
	result->height = m->height;

	for (i = 0; i < total_count; ++i) //枚举第一行的情况
	{
		memset(m->mtrx, 0, sizeof(m->mtrx));

		result->mtrx[0] = i;
		ProcessRow(m, 0, result->mtrx[0]);//处理第一行

		for (j = 1; j < m->height; ++j)
		{
			result->mtrx[j] = ~(m->mtrx[j - 1]);//根据上一行处理当前行
			ProcessRow(m, j, result->mtrx[j]);
		}

		unsigned long mark = ~0ul << m->width;

		if ((m->mtrx[m->height - 1] | mark) == ~0ul) //判断最后一行是否全为 1
		{
			result_list[counter++] = result;

			//重新分配一个解的空间
			result = (Matrix*)malloc(sizeof(Matrix));
			result->width = m->width;
			result->height = m->height;
			if (counter + 1 > MAX_RESULT_NUM) break;
		}
	}
}

int main()
{
	int step = 10000, tnt = 0, i = 0;
	initgraph(640, 480);
	Welcome();
	Matrix m;
	Matrix* results[MAX_RESULT_NUM + 1] = { NULL };
	m.width = m.height = GRID_NUM;
	Solve(&m, results);
	while (results[i] != NULL) {
		if (Cacu_Solution(results[i]) < step) step = Cacu_Solution(results[i]), tnt = i;
		i++;
	}
	GameInit();
	BeginBatchDraw();

	for (i = 0; i < GRID_NUM; i++)
	{
		for (int j = 0; j < GRID_NUM; j++)
		{
			unsigned long mark = 1;
			if ((results[tnt]->mtrx[i] & (mark << j))) {
				GameControl(i, j);
				GameDraw();
				FlushBatchDraw();
				Sleep(500);
				if (GameJudge())
				{
					HWND hwd = GetHWnd();
					MessageBox(hwd, "你赢了~", "提示:", MB_OK);
					exit(666);
				}
			}

		}
	}
	return 0;
}