飞行员兄弟
题目描述
核心思路
每个把手都有两种选择,按/不按,总共有16个把手,因此有 2 16 = 65536 2^{16}=65536 216=65536种操作。那么我们可以用16位二进制数来表示,即从0000000000000000到1111111111111111。然后用1来表示按下,用0表示不按。
主要步骤:
- 枚举从0000000000000000到1111111111111111所有操作
- 按照当前操作,对所有把手进行操作
- 判断方案是否合法,并记录合法方案
- 当 2 16 2^{16} 216种操作结束后,就可以得到最优解了
代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 5;
char g[N][N], backup[N][N];
//棋盘是二维的,用位运算枚举是一维的,存在一个映射关系
//get函数是求得某盏灯的编号(0~15) 也就是求出二维矩阵中的某个点所对应的一维坐标
//16位二进制有16位,从最低位到最高位是第0位~第15位
//求出二维矩阵中的某个点所对应的一维坐标目的也就是想求出这个点在16位二进制中是第几个的二进制位
//也就说这里是想求出二维矩阵中(i,j)这盏灯它在16位二进制中是在第几个二进制位
int get(int x, int y)
{
return x * 4 + y;
}
//翻转当前这盏灯的状态
void turn_one(int x, int y)
{
//如果当前这盏灯是灭的,则要变为亮的
if (g[x][y] == '+')
g[x][y] = '-';
//如果当前这盏灯是亮的,则要变成灭的
else
g[x][y] = '+';
}
//翻转(x,y)这盏灯它所在行所在列的所有灯的状态(包括它自己也要翻转)
void turn_all(int x, int y)
{
//枚举四个点
for (int i = 0; i < 4; i++)
{
turn_one(x, i);//翻转(x,y)这盏灯所在行的所有灯
turn_one(i, y);//翻转(x,y)这盏灯所在列的所有灯
}
//因为翻转(x,y)这盏灯所在行、所在列的所有灯时,(x,y)这盏灯被翻转了两次,就相当于没有翻转
//按道理它也是需要翻转的,所以要在这里给它翻转一次
turn_one(x, y);
}
int main()
{
for (int i = 0; i < 4; i++) //输入地图
cin >> g[i];
vector<PII> res;//最终要输出的结果,存储合理的方案
//有16盏灯,每盏灯都有按/不按两种选择,所以从0000000000000000~1111111111111111共2^16种方案
//用1来表示按,0表示不按 比如序列00000000001111表示ABCD这四盏灯需要被按下
//每一种方案都唯一确定一个序列 这16个01序列分别对应16盏灯
for (int op = 0; op < 1 << 16; op++)
{
vector<PII> temp;//临时数组,用于存当前枚举方案的情况
memcpy(backup, g, sizeof g); // 备份 然后用g数组去操作
//暴力枚举这16盏灯。假设初始时输入的这16盏灯的原始序列为0100000000000100
//那么我们就把这个原始序列转变为当前op这种方案下所应该的序列,假设为00000000000101
//然后根据00000000000101这个序列,去对16盏灯进行全面的调整
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
//有16盏灯,每盏灯都有一个编号,从0~15 这里求出二维矩阵中坐标为(i,j)这盏灯它在一维中的编号
//假设有ABCDEFGHIJKLMNOP共16盏灯,对应的编号分别为0~15
//当前某个op方案下所得到的序列已经是确定了的,那么接下来就是要确定这个序列中的哪些等应该被按下
//哪些等不需要被按下。 求出二维矩阵中(i,j)这盏灯所对应的一维编号,其实也就是
//求出二维矩阵中(i,j)这盏灯在00000000000101这16位二进制中的第几个二进制位
//如果op >> num & 1表示某盏灯是1,那么就说明这盏灯应该被按下
//比如某个op方案的序列为00000000000101,则表示A、C灯是要被按下的
//A灯在矩阵中坐标为(0,0)通过get函数后,求得num=0,说明A灯在00000000000101序列中是第0个二进制位
//然后就取出这个二进制位看看是不是1,如果是1,说明A这盏灯需要被按下,如果是0,说明不需要被按下
//B灯在矩阵中坐标为(0,1)通过get函数后,求得num=1,说明B灯在00000000000101序列中是第1个二进制位
//然后就取出这个二进制位看看是不是1,如果是1,说明A这盏灯需要被按下,如果是0,说明不需要被按下
//C灯在矩阵中坐标为(0,2)通过get函数后,求得num=2,说明C灯在00000000000101序列中是第2个二进制位
//然后就取出这个二进制位看看是不是1,如果是1,说明A这盏灯需要被按下,如果是0,说明不需要被按下
int num = get(i, j);
if (op >> num & 1)//判断当前这个位置的这盏灯是否要进行操作
{
temp.push_back({ i, j });//当前(i,j)这盏灯被按下,则加入存储枚举方案的数组
turn_all(i, j);//转变(i,j)这盏灯所在行、所在列的所以灯
}
}
}
// 判断所有灯泡是否全亮
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;//这16盏灯中还有灭灯
break;
}
}
}
//如果灯都是亮的,看看这个答案和当前存储的答案哪一个需要的操作步骤最少
if (has_closed == false)
{
if (res.empty() || res.size() > temp.size())
res = temp;
}
memcpy(g, backup, sizeof g); // 还原 让g数组还原成最初输入的地图
}
cout << res.size() << endl; //输出最小切换把手次数
//输出切换状态的把手的行号和列号 这里要注意我们搞的二维坐标是从0开始 题目输出的是要从1开始 因此需要+1
for (auto op : res)
cout << op.first + 1 << ' ' << op.second + 1 << endl;
return 0;
}