一、题目描述
\(25\)盏灯排成一个\(5x5\)的方形。每一个灯都有一个开关,游戏者可以改变它的状态。每一步,游戏者可以改变某一个灯的状态。游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。
我们用数字“\(1\)”表示一盏开着的灯,用数字“\(0\)”表示关着的灯。
二、题目分析
- 每个灯最多只能被按一次,按下多次其实是没用的,偶数次不如不按,奇数次和一次的效果是一样的。
- 按的次序是无所谓的,先按哪个再按哪个都是一样的。
三、\(bfs\)解法
\(bfs\)可以解决这道题 时间复杂度:\(2^{25}\)
#include <bits/stdc++.h>
using namespace std;
// bfs的思路采用的是逆向思维法
// 从终止状态倒推6步,先做一遍预处理。看看从终止状态可以从哪些状态在6步之内到达。
// 将状态用hash方法保存下来,2^{25},最多是3000W左右个状态,但合法状态不是很多。
//当前状态,当前到最终状态所需步数
unordered_map<int, int> vis;
//改变这个灯及其上下左右相邻的灯的状态
int turn(int st, int idx) { // idx下标从0开始
st ^= (1 << idx); //改变第idx个灯
if (idx % 5) st ^= 1 << idx - 1; //左,不为最左一个,就将左侧灯改变
if (idx >= 5) st ^= 1 << idx - 5; //上,不为第一排;就将上面的灯改变
if (idx < 20) st ^= 1 << idx + 5; //下,不为最后一排;就将下面的灯改变
if ((idx % 5) < 4) st ^= 1 << idx + 1; //右,不为右一个,就将最右面的灯改变
return st;
}
//从最终状态逆序遍历,遍历所有的状态,所以不用管地图什么样,直接bfs完,查对应map就完事了
void bfs() {
// 0-2^25-1(25个1),共2^25种状态
int st = (1 << 25) - 1; //左移 右移的优先级是最低的,比加减还要低。所以这里的括号是必需的
queue<int> q;
q.push(st);
vis[st] = 0;
while (q.size()) {
auto t = q.front();
q.pop();
if (vis[t] == 6) break; //判断6步以内使所有的灯都变亮
for (int i = 0; i < 25; i++) { //尝试当前状态的每盏灯
st = turn(t, i); //尝试改变每盏灯的状态=>新的状态
if (!vis.count(st)) { //该状态未被遍历过
vis[st] = vis[t] + 1;
q.push(st);
}
}
}
}
int main() {
bfs();
int T;
cin >> T;
while (T--) {
int g = 0; // g是地图的含义
for (int i = 0; i < 25; i++) {
char ch;
cin >> ch;
g += ((ch - '0') << i); // 25个字符二进制压缩成数字
}
if (vis.count(g) == 0)
cout << -1 << endl;
else
cout << vis[g] << endl;
}
return 0;
}
四、递推解法
从上往下递推,还有一道题叫扫雷,有时间可以做一下。
- 二进制枚举+递推可以解决这道题 \(2^5\times 25 \times 5 \times 500\)
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 6;
// char 数组版本
char g[N][N], bg[N][N]; //工作的临时数组 和 原始状态数组
//上右下左中
int dx[] = {-1, 0, 1, 0, 0};
int dy[] = {0, 1, 0, -1, 0};
// 按一下第x行第y列的开关
void turn(int x, int y) {
for (int i = 0; i < 5; i++) {
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= 5 || b < 0 || b >= 5) continue;
g[a][b] ^= 1; //'0':48 '1':49
// (48)_{10}=(110000)_{2} (49)_{10}=(110001)_{2}
// 由于48的最后一位是0,而49最后一位是1,所以,异或就可以实现两者之间的切换,真是太神奇了~
}
}
int main() {
int T;
cin >> T;
while (T--) {
//按一行的字符串读取
for (int i = 0; i < 5; i++) cin >> bg[i]; //原来的字符状态数组
//预求最小,先设最大
int res = INF;
//因为这一题里面的op代表着对第一行的操作,1表示按下,0表示不按下,并不是描述灯的状态
//枚举对第1行的所有操作,注意:是操作,不是状态噢~
for (int op = 0; op < (2 << 5); op++) { //从0 到 2^5-1,共 2^5=32个二进制状态模拟,就可以描述出第一行灯的点亮与否的所有可能
int cnt = 0;
memcpy(g, bg, sizeof g); //将原始状态复制出一份放入g数组,准备变形
// 操作第一行的开关
for (int i = 0; i < 5; i++)
if (op >> i & 1) { //如果此种状态op模拟情况下,第i位的灯是亮的
turn(0, i); //将第1行,第i列的状态改变
cnt++; //用去了一次机会
}
// 递推出第1~4行开关的状态
// 第0行推第1行,第1行推第2行,...
for (int i = 0; i < 4; i++)
for (int j = 0; j < 5; j++)
if (g[i][j] == '0') {
turn(i + 1, j);
cnt++; //又用去一次机会
}
// 检查最后一行灯是否全亮
bool success = true;
for (int i = 0; i < 5; i++)
if (g[4][i] == '0') success = false;
if (success) res = min(res, cnt);
}
//题意要求,大于6次,算失败
if (res > 6) res = -1;
printf("%d\n", res);
}
return 0;
}
五、高斯消元解法
- 高斯消元可以解决这道题 时间复杂度:\(n^6\),\(n\)是指边长。
思路:为了练习高斯消元找来的题目,当然是用高斯消元了……
解:既然要解方程,那么首先我们要清楚我们要求的是什么(虽然看起来像废话,但是我一开始真的想了半天
显然,我们需要求每个开关是否打开。对于开关来说,只有开与不开两种情况,可以用\(0\) \(1\)表示
输入有\(n\)个开关(本题中\(n=5\times 5=25\)),所以我们需要有\(n=25\)个方程。然后我们需要建立方程
怎么把开关与灯的状态联系起来?
很明显,这是一个 异或 的方程组~,为啥呢?原来是亮的,再点一下就灭了;原来是灭的,再点一下就亮了;这不是异或是啥?是啥?没做过高斯消元解异或方程组吗?
对于灯\(1\)来说,一共有\(n=25\)个开关,我们可以把能改变灯\(1\)状态的开关设成\(1\),不能改变的设成\(0\),来描述不同的开关对于灯\(1\)的影响。
显然这里要一个\(n*n\)的二维数组,代码中用变量\(mat\)来记录。
对于方程的结果,可以根据灯\(1\)的开始状态和结束状态设置,相同为\(0\),不同为\(1\)。
那么我们可以得到以下方程: \(use[1]\)表示开关\(1\),简写成\(u[1]\),\(relate[1][2]\)表示开关\(1\)对灯\(2\)的影响 简写成\(r[1][2]\)
\(u[1]*r[1][1]\oplus u[2]*r[2][1]\oplus u[3]*r[3][1]\oplus u[4]*r[4][1]……\oplus u[n]*r[n][1] = start[1]\oplus end[1]\) //start,end 表示初始状态,结尾状态
列出\(n\)个以后再把要求的\(u[1]\)到\(u[n]\)提出来,就可以得到矩阵
这时候会发现,\(r[i][j]\),和正常的有点不一样,所以等会输入的时候需要处理一下(行列互换)
到这里,前期的准备工作算是\(OK\)了,下面开始解方程
所谓高斯消元法,就和我们平时解方程一样,通过不断地带入消除未知数,拿到一个变量的解以后再带回到其他方程,得到其它变量的解。
在矩阵里,我们可以把矩阵转化为上三角的形式,然后通过原矩阵与增广矩阵的秩,来判断有没有解(如果有唯一解的话,可以带回去把解求出来,当然这题不用求)
关于秩和解的关系,这里稍微列一下,因为我的线代其实也忘的差不多……
#include <bits/stdc++.h>
using namespace std;
//方向
int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};
//原始状态
char mp[8][8];
//增广矩阵 25*26,25行,26列
bool mat[30][30];
//返回x在二进制下1的个数
#define lowbit(x) (x & (-x))
inline int nbit(int x) {
int cnt = 0;
while (x) {
cnt++;
x -= lowbit(x);
}
return cnt;
}
//高斯消元
int gauss(int row, int col) {
for (int i = 1; i <= row; ++i) {
if (!mat[i][i]) {
int r = i;
while (++r <= row)
if (mat[r][i]) break;
if (r > row) continue;
for (int j = i; j <= col; j++) swap(mat[i][j], mat[r][j]);
}
for (int r = 1; r <= row; r++) {
if (r == i || mat[r][i] == 0) continue;
for (int j = i; j <= col; j++) mat[r][j] ^= mat[i][j];
}
}
//以上就是正常的消元
int num = 0; //自由变量的个数
for (int i = 1; i <= row; i++) {
if (mat[i][i] == 0 && mat[i][col] == 1) return -1; //无解
if (mat[i][i] == 0) num++; //+1
}
int res[30]; //自由变元对应的系数(二进制)
for (int i = 1; i <= row - num; i++) {
res[i] = 0;
for (int j = row - num + 1; j < col; ++j) res[i] = (res[i] << 1) + mat[i][j];
}
int ans = 7;
//枚举自由变量的取值
for (int i = 0; i < (1 << num); i++) {
int t_ans = nbit(i); //自由变元里操作的次数
for (int r = 1; r <= row - num; r++) t_ans += (nbit(res[r] & i) + mat[r][col]) & 1;
//两个二进制数按位与相当于对应二进制位相乘
ans = min(ans, t_ans);
}
return ans > 6 ? -1 : ans;
}
//将坐标位置,变换为灯的编号,注意位置坐标从1开始
int id(int i, int j) {
return (i - 1) * 5 + j;
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
//多测,还原增广矩阵初始状态
memset(mat, 0, sizeof mat);
//读入原始状态
for (int i = 1; i <= 5; i++) scanf("%s", mp[i] + 1);
//构造增广矩阵
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= 5; j++) {
for (int k = 0; k < 4; k++) {
int x = i + dx[k], y = j + dy[k];
//不出界
if (x <= 0 || x >= 6 || y <= 0 || y >= 6) continue;
mat[id(i, j)][id(x, y)] = 1; //灯(i,j)会改变灯(x,y)的状态
}
mat[id(i, j)][id(i, j)] = 1; //灯(i,j)会改变灯(i,j)自己的状态
mat[id(i, j)][26] = (mp[i][j] - '0') ^ 1; //结果(右侧结果数据),保存的是char,减'0'还原为数字0或1,再异或1,就变成相反数
}
}
//高斯消元,判断是不是有唯一解
printf("%d\n", gauss(25, 26));
}
return 0;
}