奶牛翻盖问题 二进制压缩枚举

原题:
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

问题简述:
有人从农场里养了许多奶牛,然后在一个边长M,N都不超过15的方格内有着板块,正面是白色,反面是黑色,目标是找到最少的翻转地方的次数。当翻一次时候,和它共享边的板块也能翻转。
输入:
长和宽。
黑白色不知情况。

输出:
哪里需要翻转,以及翻转几次。

解题思路:
做这题之前,我们首先需要理解一个现象:除了最后一行,其他任何一行上的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;  
}  

猜你喜欢

转载自blog.csdn.net/zhangzhiyuan88/article/details/80487710