D - Fliptile
Time Limit:2000MS Memory Limit:65536KB 64bit IO Format:%I64d & %I64u
Description
Farmer John knows that an intellectually satisfied cow is a happy cow who will give more milk. He has arranged a brainy activity for cows in which they manipulate an M × N grid (1 ≤ M ≤ 15; 1 ≤ N ≤ 15) of square tiles, each of which is colored black on one side and white on the other side.
As one would guess, when a single white tile is flipped, it changes to black; when a single black tile is flipped, it changes to white. The cows are rewarded when they flip the tiles so that each tile has the white side face up. However, the cows have rather large hooves and when they try to flip a certain tile, they also flip all the adjacent tiles (tiles that share a full edge with the flipped tile). Since the flips are tiring, the cows want to minimize the number of flips they have to make.
Help the cows determine the minimum number of flips required, and the locations to flip to achieve that minimum. If there are multiple ways to achieve the task with the minimum amount of flips, return the one with the least lexicographical ordering in the output when considered as a string. If the task is impossible, print one line with the word "IMPOSSIBLE".
Input
Line 1: Two space-separated integers: M and N
Lines 2.. M+1: Line i+1 describes the colors (left to right) of row i of the grid with N space-separated integers which are 1 for black and 0 for white
Output
Lines 1.. M: Each line contains N space-separated integers, each specifying how many times to flip that particular location.
Sample Input
4 4
1 0 0 1
0 1 1 0
0 1 1 0
1 0 0 1
Sample Output
0 0 0 0
1 0 0 1
1 0 0 1
0 0 0 0
题意:有一个n*m的棋盘,0表示白色,1表示黑色。每次可以翻转当前位置,它的上下左右四个位置也会被相应翻转。问最少翻转多少次会使所有棋面显示为白色,并给出需要翻转的位置,0表示不翻转,1表示翻转。
思路:可以利用第一层的 2^n 种状态来进行枚举。依次翻转到最后一行,如果最后一行均为白色,那么这种第一层状态可以达到效果。记录下来,最后取最少翻转次数的结果输出。具体实现细节和相关解释在代码注释中给出。
ps:(学到了利用二进制进行枚举的方便之处)QQA~
i的二进制中,从右往左数,取第j+1位上的值
补充:i>>(j-1) & 1;
如果写成能理解的应该是这样的
1 2 3 4 |
|
<< 是左位移的符号。
m的值是2,用2进制表示就是10。
m<<=1相当于m=m<<1;
m<<1向左移一位,也就是二进制数0010变成了0100.
然后再将0100也就是4赋值给m。
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
int Map[20][20],cal[20][20],out[20][20];
int n,m;
int dir[5][2] = {{0,0},{0,1},{0,-1},{1,0},{-1,0}};
int fuc(int x,int y){ //(x,y)的状态由本身的黑白 + 周围五个的翻转状态决定
int temp = Map[x][y];
for(int i = 0;i < 5;i ++){
int xi = x+dir[i][0];
int yi = y+dir[i][1];
if(xi < 1 || xi > n || yi < 1 || yi > m) continue;
temp += cal[xi][yi];
}
return temp%2;
}
int dfs(){
for(int i = 2;i <= n;i ++)
for(int j = 1;j <= m;j ++)
if(fuc(i-1,j)) //如果上方为黑色,必须要翻转
cal[i][j] = 1;
for(int i = 1;i <= m;i ++) //最后一行全白
if(fuc(n,i))
return -1;
int res = 0;
for(int i = 1;i <= n;i ++)
for(int j = 1;j <= m;j ++)
res += cal[i][j];
return res;
}
int main()
{
while(cin>>n>>m){
for(int i = 1;i <= n;i ++)
for(int j = 1;j <= m;j ++)
cin>>Map[i][j];
int flag = 0;
int ans = 0x3f3f3f3f;
for(int i = 0;i < 1<<m;i ++){ //第一行 1<<m种状态,二进制从0开始,字典序从小到大
memset(cal,0,sizeof(cal));
for(int j = 1;j <= m;j ++) //利用二进制枚举第一行所有的情况
cal[1][m-j+1] = i>>(j-1) & 1;
int cont = dfs();
if(cont >= 0 && cont < ans){ //翻转次数最少
flag = 1;
ans = cont;
memcpy(out,cal,sizeof(cal));
}
}
if(!flag) cout<<"IMPOSSIBLE"<<endl;
else{
for(int i = 1;i <= n;i ++){
for(int j = 1;j <= m;j ++){
if(j != 1) cout<<" ";
cout<<out[i][j];
}
cout<<endl;
}
}
}
return 0;
}
做这题之前,我们首先需要理解一个现象:除了最后一行,其他任何一行上的1都可以通过下一行的翻转转换成0。
也就是说,除了最后一行外,我们总是可以通过翻转,将前n-1行翻成全0。
只要按照这样的原则:对于某一个位置x,如果它的上一个是1,就翻转它,如果是0,就不翻转。
知道了这个,我们应该就能想到,第一行的翻法直接就决定了后面的所有的翻法,这就是我们解决这道题目的思路,即枚举第一行所有可能的翻法,这里,如果用循来枚举,未免太浪费时间了,所以,这里采取了一种二进制压缩的方法。
首先让我们来学习一下左移<<的概念,其实很好理解,就是将一个数转换为二进制,然后向左移动若干位,然后在多出来的位置上补零,比如,对于5<<2,5的二进制为101,左移两位就是10100,那么最后的结果就是20,。
利用这个特点,我们可以通过使用一个特殊的数字1,来解决这个枚举问题。枚举的第一步就是确定到底要改变哪几位数,联想到二进制,我们可以这样处理,就题目的测试数据而言,一行有四个数,用一个二进制数xxxx表示,易知,xxxx一共有2^4种排列,其实也是1<<4,之所以这样写,是因为这比pow要快,所以,我们让k从0开始枚举到15,也就是0000到1111,然后规定,只要是带1的位置,就要翻转这个位置的数字,问题又来了,怎么知道哪一位是1,呢,这里还是用到了二进制,即与运算,我们让k分别与1000,0100,0010,0001进行与运算,分别对应不同的位,如果结果不是1,说明这一位上不是0,是不是灰常巧妙,当然,那四个值依然是通过1的左移来计算出来的,核心的东西讲完了,下面看看代码吧
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 16;
int g[N][N], t[N][N], f[N][N];
int cnt, n, m;
int x[4] = {0, 0, -1, 1};
int y[4] = { -1, 1, 0, 0};
void flip(int i, int j)//翻转
{
++cnt, f[i][j] = 1;//步数加1,记录翻转了哪个瓷砖
t[i][j] = !t[i][j];//首先翻转自己
for(int k = 0; k < 4; ++k)//向四个方向寻找,找到就翻转,这里使用了异或
if(i + x[k] > -1 && j + y[k] > -1)
t[i + x[k]][j + y[k]] ^= 1;
}
bool ok(int k)//对于第一行的每一种情况,判断是否能够产生最终的结果
{
cnt = 0;//初始化步数
memcpy(t, g, sizeof(t));//初始化临时数组,作为原始数组的副本
for(int j = 0; j < m; ++j)//这里采用了二进制压缩,例如,j从0到3,那么1 << (m - 1 - j)的二进制就是1000,0100,0010,0001
if(k & (1 << (m - 1 - j)))//对于k的每一个取值,如1010,找到不为0的列,因为只需要翻转1就可以了,用到了与运算
flip(0, j);//如果某一列不为0,就翻转第一行的这个位置
for(int i = 1; i < n; ++i)//当第一行全部翻转完了,原来为1的位置肯定是0,原来是0的位置肯定是1,这就需要第二行来解决这些为1位置,以此类推
for(int j = 0; j < m; ++j)
if(t[i - 1][j]) flip(i, j);//如果该列上一个位置是1,那么这个位置需要翻,否则不需要翻
for(int j = 0; j < m; ++j)//因为每一行的1都可以由下一行搞定,但是最后一行没有下一行,所以只需要考察最后一行最后是不是全0就可以了
if(t[n - 1][j]) return false;
return true;
}
int main()
{
int ans, p;
while(~scanf("%d%d", &n, &m))
{
for(int i = 0; i < n; ++i)//数据输入
for(int j = 0; j < m; ++j)
scanf("%d", &g[i][j]);
ans = n * m + 1, p = -1;//初始化
for(int i = 0; i < (1 << m); ++i)//i表示一个二进制数,用来枚举第一行的各种不同翻法,如0001就是只翻最后一个
if(ok(i) && cnt < ans) //如果找到一种可能并且所用的步数更少的话,记下这种翻法
ans = cnt, p = i;
memset(f, 0, sizeof(f));
if(p >= 0)//最后找到的就是最少的翻法,模拟一遍,然后输出
{
ok(p);
for(int i = 0; i < n; ++i)
for(int j = 0; j < m; ++j)
printf("%d%c", f[i][j], j < m - 1 ? ' ' : '\n');
}
else puts("IMPOSSIBLE");
}
return 0;
}