《算法竞赛进阶指南》0x00习题---飞行员兄弟题解
题目描述
“飞行员兄弟”这个游戏,需要玩家顺利的打开一个拥有 16 个把手的冰箱。
已知每个把手可以处于以下两种状态之一:打开或关闭。
只有当所有把手都打开时,冰箱才会打开。
把手可以表示为一个 \(4×4\) 的矩阵,您可以改变任何一个位置 \([i,j]\) 上把手的状态。
但是,这也会使得第 \(i\) 行和第 \(j\) 列上的所有把手的状态也随着改变。
请你求出打开冰箱所需的切换把手的次数最小值是多少。
输入格式
输入一共包含四行,每行包含四个把手的初始状态。
符号 + 表示把手处于闭合状态,而符号 - 表示把手处于打开状态。
至少一个手柄的初始状态是关闭的。
输出格式
第一行输出一个整数 \(N\),表示所需的最小切换把手次数。
接下来 \(N\) 行描述切换顺序,每行输出两个整数,代表被切换状态的把手的行号和列号,数字之间用空格隔开。
注意:如果存在多种打开冰箱的方式,则按照优先级整体从上到下,同行从左到右打开。
数据范围
\(1≤i,j≤4\)
样例
输入样例:
-+--
----
----
-+--
输出样例:
6
1 1
1 3
1 4
4 1
4 3
4 4
思路
- 二进制枚举对 \(16\) 个位置的操作,低位 从上到下,从左到右 到高位。
- 同样将初始状态
init
映射成二进制形式如样例0010000000000010
右边是最低位。 - 将行列操作映射成对一个数进行异或。比如第一行便是
^= 0000000000001111
,第二行便是将低位 \(4\) 个 \(1\) 左 \(4\) 位 - 再比如对第一列相当于
^=0001000100010001
,对第二列操作就是将第一列操作左移 \(1\) 位 - 注意同时对行列操作需要将交叉的部分再异或一下即
^=(row[x] & col[y])
- 至此记录更新的答案和操作,再将数还原为行列坐标即可
时间复杂度
\(O(2^{16} * 16)\)
C++ 代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 5, M = 16;
int ans, init, op;
// + 代表 1, - 代表 0, 每一行下移要 << 4, 每一列右移就 << 1
int row[4] = {15, 15 << 4, 15 << 8, 15 << 12}, col[4] = {4369, 4369 << 1, 4369 << 2, 4369 << 3};
int gen(int x, int y){ // 对第 x 行、第 y 列进行操作
return row[x] ^ col[y] ^ (row[x] & col[y]);
}
void change(int st){
int t = init, tmp = st, res = 0, cnt = 0;
while(tmp){
if(tmp & 1){
int x = cnt / 4, y = cnt % 4;
t = t ^ gen(x, y);
res ++;
}
tmp >>= 1;
cnt ++;
}
if(!t && res < ans){ // 更新答案
op = st;
ans = res;
}
}
int main(){
int cnt = 0;
ans = 1000;
for(int i = 0; i < 4; i++){
string s;
cin >> s;
for(int j = 0 ; j < s.size(); j++)
if(s[j] == '+')
init += 1 << (i * 4 + j);
}
for(int i = 0; i < 1 << M; i++) // 二进制枚举操作方案
change(i);
cnt = 0;
cout << ans << endl;
while(op){
if(op & 1)
cout << cnt / 4 + 1 << " " << cnt % 4 + 1 << endl;
op >>= 1;
cnt ++;
}
return 0;
}