题目 飞行员兄弟
一、有关题目(涉及算法:枚举,位运算)
1.题目来源:《算法竞赛进阶指南》
2.题目链接 https://www.acwing.com/problem/content/118/
3.题目描述
“飞行员兄弟”这个游戏,需要玩家顺利的打开一个拥有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
二、抽象模型
这个题目是一个“开关问题”,与题目“费解的开关”很像。
(题目“费解的开关”具见文章 https://blog.csdn.net/m0_72955669/article/details/135901175)
但仔细考虑会发现,这个题目每个“把手”的状态不会只受一个“开关”的影响,因此无法用递推来解决。
除此之外,本题与题目“费解的开关”最大的区别是,此题是4*4的矩阵而,题目“费解的开关”是5*5的矩阵。本题矩阵更小,也就意味着采用完全枚举的方法可能时间复杂度也是满足题目的。
因此,本题应采用枚举的方法来做。
本题属于指数型枚举,可以利用二进制与十进制的关系采用普通枚举法(可以利用位运算来对算法进行优化),也可以利用DFS(深度优先遍历)进行枚举。
三、相关代码
方法1:普通枚举法
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
typedef pair<int, int> PII;
char g[5][5], backup[5][5];
vector<PII>ans, ans_b;
void turn_one(int x, int y){
if(g[x][y] == '+') g[x][y] = '-';
else g[x][y] = '+';
}
void turn_all(int x, int y){
for(int i = 0; i < 4; ++ i){
turn_one(x, i);
turn_one(i, y);
}
turn_one(x, y);
}
int main(){
for(int i = 0; i < 4; ++ i){
scanf("%s", g[i]);
}
memcpy(backup, g, sizeof g);
for(int op = 0; op < 1<<16; ++ op){
memcpy(g, backup, sizeof backup);
ans_b.clear();
for(int i = 0; i < 16; ++ i){
if(op >> i & 1){
int x = i / 4;
int y = i % 4;
ans_b.push_back({x, y});
turn_all(x, y);
}
}
bool has_closed = false;
for(int i = 0; i < 4; ++ i){
for(int j = 0; j < 4; ++ j){
if(g[i][j] == '+'){
has_closed = true;
break;
}
}
}
if(!has_closed){
if(ans.empty() || ans.size() > ans_b.size()){
ans = ans_b;
}
}
}
printf("%d\n", ans.size());
for(int i = 0; i < ans.size(); ++ i){
printf("%d %d\n", ans[i].first+1, ans[i].second+1);
}
return 0;
}
方法2:位运算优化方法1
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
typedef pair<int, int> PII;
vector<PII> ans, ans_b;
char g[5][5];
int change[5][5];
int now, state;
int get(int x, int y){
return x*4+y;
}
int main(){
for(int i = 0; i < 4; ++ i){
scanf("%s", g[i]);
}
for(int i = 0; i < 4; ++ i){
for(int j = 0; j < 4; ++ j){
for(int k = 0; k < 4; ++ k){
change[i][j] += (1<<get(i, k)) + (1<<get(k, j));
}
change[i][j] -= 1<<get(i, j);
}
}
for(int i = 0; i < 4; ++ i){
for(int j = 0; j < 4; ++ j){
if(g[i][j] == '+'){
state += 1<<get(i, j);
}
}
}
for(int op = 0; op < 1<<16; ++ op){
now = state;
ans_b.clear();
for(int i = 0; i < 4; ++ i){
for(int j = 0; j < 4; ++ j){
if(op >> get(i, j) & 1){
ans_b.push_back({i, j});
now ^= change[i][j];
}
}
}
if(!now){
if(ans.empty() || ans.size() > ans_b.size()){
ans = ans_b;
}
}
}
printf("%d\n", ans.size());
for(int i = 0; i < ans.size(); ++ i){
printf("%d %d\n", ans[i].first+1, ans[i].second+1);
}
return 0;
}
方法3:DFS枚举法
//DFS(大多枚举的问题都可以用DFS来解决)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
typedef pair<int, int>PII;
vector<PII>temp, ans;
char g[5][5];
void turn_one(int x, int y){
if(g[x][y] == '+') g[x][y] = '-';
else g[x][y] = '+';
}
void turn_all(int x, int y){
for(int i = 0; i < 4; ++ i){
turn_one(x, i);
turn_one(i, y);
}
turn_one(x, y);
}
void DFS(int x, int y){
if(x == 3 && y == 4){
for(int i = 0; i < 4; ++ i){
for(int j = 0; j < 4; ++ j){
if(g[i][j] == '+')
return;
}
}
if(ans.empty() || ans.size() > temp.size()){
ans = temp;
}
return;
}
if(y == 4){
++ x;
y = 0;
}
//指数型枚举
// 按下开关(1)
turn_all(x, y);
temp.push_back({x, y});
DFS(x, y+1);
turn_all(x, y); //注意:这两句旨在恢复“现场”,因此不需要整体进行ans_b的清空以及数组g的备份。
temp.pop_back(); //注意:这两句旨在恢复“现场”,因此不需要整体进行ans_b的清空以及数组g的备份。
// 不按 (0)
DFS(x, y+1);
}
int main(){
for(int i = 0; i < 4; ++ i){
scanf("%s", g[i]);
}
DFS(0, 0);
printf("%d\n", ans.size());
for(int i = 0; i < ans.size(); ++ i){
printf("%d %d\n", ans[i].first+1, ans[i].second+1);
}
return 0;
}