POJ 1753 Flip Game(状态压缩+BFS)解题报告

2016-07-25 704 次浏览 解题报告

题面

Flip Game
Time Limit: 1000MS Memory Limit: 65536K

Description

Flip game is played on a rectangular 4x4 field with two-sided pieces placed on each of its 16 squares. One side of each piece is white and the other one is black and each piece is lying either it's black or white side up. Each round you flip 3 to 5 pieces, thus changing the color of their upper side from black to white and vice versa. The pieces to be flipped are chosen every round according to the following rules:

  1. Choose any one of the 16 pieces.
  2. Flip the chosen piece and also all adjacent pieces to the left, to the right, to the top, and to the bottom of the chosen piece (if there are any).

POJ 1753

Consider the following position as an example:

bwbw
wwww
bbwb
bwwb
Here "b" denotes pieces lying their black side up and "w" denotes pieces lying their white side up. If we choose to flip the 1st piece from the 3rd row (this choice is shown at the picture), then the field will become:

bwbw
bwww
wwwb
wwwb
The goal of the game is to flip either all pieces white side up or all pieces black side up. You are to write a program that will search for the minimum number of rounds needed to achieve this goal.

Input

The input consists of 4 lines with 4 characters "w" or "b" each that denote game field position.

Output

Write to the output file a single integer number - the minimum number of rounds needed to achieve the goal of the game from the given position. If the goal is initially achieved, then write 0. If it's impossible to achieve the goal, then write the word "Impossible" (without quotes).

Sample Input

bwwb
bbwb
bwwb
bwww

Sample Output

4

Source

Northeastern Europe 2000

解题思路

本题基本思路并不难,很容易想到用 BFS 来搜索到达目标状态(全黑或全白)的最小步数,不过与传统的地图搜索不同的是,每一次我们都要搜 16 个位置,即要对每一个位置上的棋子做反转来搜索。于是,本题的主要难点就在于如何存储棋盘盘面状态和反转。

状态存储

由于棋盘上每一个棋子都只有黑或白两种状态,所以我们可以用 0(黑)和 1(白)来表示每一个格子上的棋子状态。而对于整个二维棋盘,一共只有16个格子,我们可以按位置顺序将他们编号:
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15

这样,整个棋盘的状态就可以转化为一个有 16 个数的序列,例如对于题目中的样例棋盘:
bwbw
wwww
bbwb
bwwb

首先转换为 0/1 存储:
0101
1111
0010
0110

然后按照编号从大到小的顺序排成一个序列:
0110010011111010

这样,我们可以用这个序列来表示整个棋盘的状态。有没有觉得眼熟呢?很像一个二进制数对吧?没错,我们就是这样将整个棋盘的状态压缩成一个 16 位的二进制数来存储。不要觉得这个数似乎很大,其实 16 位二进制数以 10 进制表示的话最大只有 2^16-1 = 65535,所以用 int 或 unsigned short 就足够存储了。

解决了状态如何存储的问题,你可能会问如何计算出这个二进制数?其实很简单,每一个编号上是 0 还是 1 我们是知道的,令此二进制数初始为全 0,我们只需要把状态为 1 的编号在二进制数中的对应位置设置为 1 即可。由于我们的二进制数是以编号递减的顺序存储的,所以简单地用左移运算即可得到对应的二进制值。例如,编号为 3 的位置是 1,我们就需要把二进制数的倒数第 4 位(别忘了编号从 0 开始)设置为 1,不难想到加上一个二进制的 1000 就可以将倒数第 4 位变为 1,其他位不受影响。而要计算出这个 1000 其实也很容易,将 1 左移 3 位即可得到。所以我们的计算公式即是对于每一个为状态 1 的点,都在二进制数上加 1 << (编号) 。

反转处理

理解了如何存储状态,反转就容易多了。使用二进制存储状态的另一个好处就是,利用位运算即可便捷地改变状态。这里我们要用到「异或」运算,两个数做异或运算的规则是:比较它们对应的每一位,如果不同则此位为 1,否则为 0 。那么如何利用异或处理反转,相信很多同学已经想到了,对于需要反转的位置,与 1 做异或运算,0 ^ 1 = 1,1 ^ 1 = 0,对于不需要反转的位置,与 0 做异或运算,0 ^ 0 = 0,1 ^ 0 = 1,结果不变。下面我们以题目样例来解释一下反转如何处理。

初始状态:
bwbw
wwww
bbwb
bwwb

二进制表示:
0110010011111010

反转编号为 8 的棋子,会使其上下左右的棋子一起反转,即编号为 4、8、9、12 的棋子会被反转,我们将要反转的位置设置为 1,不反转的位置设置为 0,得到一个二进制数:
0001001100010000

然后进行异或运算:
0110010011111010
0001001100010000
——————————
0111011111101010

这样得到的结果就是反转之后的盘面状态。反转某一个位置的棋子,哪些位置会随之反转是确定的,参与异或运算的这个二进制数也就是确定的。这样对于总共的 16 个可反转位置,我们可以先预处理好所有情况,BFS 的时候直接取出来对当前盘面状态做异或运算就可以实现反转了。

参考代码

#include <cstdio>
#include <queue>
using namespace std;

const int size = 4;
const int dir_num = 4;
struct info {
    int value;    // 状态值 
    int step;    // 步数
};
int dir[dir_num][2] = {
        {-1, 0}, {0, 1},
        {1, 0}, {0, -1},
    };
int pos[size*size];    // 16 种翻转状态
bool vis[1<<(size*size)];    //标记某状态是否已出现过

// 判断某个位置是否在棋盘内
bool Judge(int x, int y) {
    if(x>=0 && x<size
        && y>=0 && y<size)
        return true;
    else return false;
}

// 预处理 16 种翻转状态
void Initialize() {
    for(int i=0; i<size; ++i) {
        for(int j=0; j<size; ++j) {
            // i*size + j 即编号
            int value = 1 << (i*size + j);
            for(int k=0; k<dir_num; ++k) {
                int next_x = i+dir[k][0];
                int next_y = j+dir[k][1];
                if(Judge(next_x, next_y))
                    // 加上 1<<(编号) 即可将此位置设置为1
                    value += 1 << (next_x*size + next_y);
            }
            pos[i*size+j] = value;
        }
    }
}

int BFS(int value) {
    queue<info> q;
    info s = {value, 0};
    q.push(s);
    vis[s.value] = true;
    while(!q.empty()) {
        info f = q.front();
        q.pop();
        // 盘面全黑 (0) 或全白 (2^16-1) 时结束,返回步数
        if(f.value==0 || f.value==(1<<(size*size))-1)
            return f.step;
        // 搜索全部 16 个位置
        for(int i=0; i<size*size; ++i) {
            // 通过异或运算得到翻转后的状态
            info next = {f.value^pos[i], f.step+1};
            if(!vis[next.value]) {
                q.push(next);
                vis[next.value] = true;
            }
        }
    }

    return -1;    // 无法到达目标状态,返回 -1
}

int main(int argc, char const *argv[]) {
    Initialize();
    char str[5];
    int value = 0;
    for(int i=0; i<size; ++i) {
        scanf("%s", str);
        // 计算起始状态的值
        for(int j=0; j<size; ++j) {
            if(str[j] == 'w')
                value += 1 << (i*size + j);
        }
    }
    int ans = BFS(value);
    if(ans >= 0) printf("%d\n", ans);
    else printf("Impossible\n");
    
    return 0;
}

bLue 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。
本文地址:https://dreamer.blue/blog/post/2016/07/25/poj-1753.dream

还不快抢沙发

添加新评论