费解的开关
你玩过“拉灯”游戏吗?25盏灯排成一个5x5的方形。每一个灯都有一个开关,游戏者可以改变它的状态。每一步,游戏者可以改变某一个灯的状态。游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。
我们用数字“1”表示一盏开着的灯,用数字“0”表示关着的灯。下面这种状态
10111
01101
10111
10000
11011
在改变了最左上角的灯的状态后将变成:
01111
11101
10111
10000
11011
再改变它正中间的灯后状态将变成:
01111
11001
11001
10100
11011
给定一些游戏的初始状态,编写程序判断游戏者是否可能在6步以内使所有的灯都变亮。
输入格式
第一行输入正整数n,代表数据中共有n个待解决的游戏初始状态。
以下若干行数据分为n组,每组数据有5行,每行5个字符。每组数据描述了一个游戏的初始状态。各组数据间用一个空行分隔。
输出格式
一共输出n行数据,每行有一个小于等于6的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。
对于某一个游戏初始状态,若6步以内无法使所有灯变亮,则输出“-1”。
数据范围
0<n≤500
输入样例:
3
00111
01011
10001
11010
11100
11101
11101
11110
11111
11111
01111
11111
11111
11111
11111
输出样例:
3
2
-1
第一个方法(反向bfs) 反向暴力,从(1 << 25 - 1) 枚举6步之内可以到达的状态,用map记录状态和对应的次数。看输入的状态是不是在map中。这个方法,擦边超时。
#include<iostream>
#include<map>
#include<queue>
#define ll long long;
using namespace std;
map <long long, int>ans;
int dx[5] = {0, -1, 1, 0, 0};
int dy[5] = {0, 0, 0, -1, 1};
long long scan() {
string s;
long long ans = 0;
for (int i = 1; i <= 5; i++) {
cin >> s;
for (int j = 0; j < 5; j++) {
ans = ans << 1;
if (s[j] == '1') {
ans = ans | 1;
}
}
}
return ans;
}
long long turn(long long s, int i) {
long long tem = s;
int x = i / 5;
int y = i % 5;
for (int i = 0; i <= 4; i++) {
int tx = x + dx[i];
int ty = y + dy[i];
if (tx < 0 || tx > 4 || ty < 0 || ty > 4) continue;
int p = tx * 5 + ty;
tem = (tem >> p & 1) ? (tem - (1 << p)) : (tem + (1 << p));
}
return tem;
}
void bfs() {
queue<pair<long long, int > > que;
que.push(make_pair((1 << 25) - 1, 0));
ans[(1 << 25) - 1] = 0;
while(!que.empty()) {
long long s = que.front().first;
int cnt = que.front().second;
que.pop();
if (cnt == 6) continue;
for (int i = 0; i < 25; i++) {
long long tem = turn(s, i);
if (ans.find(tem) != ans.end()) continue;
que.push(make_pair(tem, cnt + 1));
ans[tem] = cnt + 1;
}
}
}
int main () {
int n;
cin >> n;
bfs();
while(n--) {
long long s = scan();
if (ans.find(s) == ans.end()) cout << -1 << "\n";
else cout << ans[s] << "\n";
}
return 0;
}
第二个方法是利用了递推的思想,假设前面一行已经完全为1,如果在当前行出现0,就只能依靠下一行对应位置来改变。因为无论改变当前位置,还是改变左右的位置,都会打乱前一行的序列。
那问题的关键就是如何决策第一行。如果第一行出现了0,决策有3个,依靠改变左右和自己,和依靠改变下面。依靠下面的决策,我们可以放到递推的过程中。剩下的就是直接枚举依赖左右和自己的决策,即可。
#include<bits/stdc++.h>
using namespace std;
char tem[6][6];
char cp[6][6];
int dx[5] = {0, 0, 0 , -1 , 1};
int dy[5] = {0,1, -1, 0 , 0};
void turn(int x, int y) {
for (int i = 0; i <= 4; i++) {
int tx = x + dx[i];
int ty = y + dy[i];
if (tx < 1 || ty > 5 || ty < 1 || ty > 5) continue;
tem[tx][ty] = tem[tx][ty] == '0' ? '1' : '0';
}
}
int dp() {
int ans = 10;
for (int choice = 0; choice < (1 << 5); choice++) {
memcpy(tem, cp, sizeof cp);
int cnt = 0;
for (int j = 0; j < 5; j++) {
if ((choice >> j) & 1) {
cnt++;
turn(1, j + 1);
}
}
for (int i = 1; i < 5; i++) {
for (int j = 1; j <= 5; j++) {
if (tem[i][j] == '0') {
cnt++;
turn(i + 1, j);
}
}
}
bool is_yes = true;
for (int i = 1; i <= 5; i++) {
if (tem[5][i] == '0') {
is_yes = false;
}
}
if (is_yes == true) ans = min(ans, cnt);
}
if (ans > 6) ans = -1;
return ans;
}
int main () {
int t;
cin >> t;
while (t--) {
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= 5; j++) {
cin >> cp[i][j];
}
}
cout << dp() << "\n";
}
return 0;
}