POJ1753 Flip Game
Time Limit: 1000MS | Memory Limit: 65536K | |
Total Submissions: 54415 | Accepted: 22787 |
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:
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.
- Choose any one of the 16 pieces.
- 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).
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
【题意分析】 有一4x4棋盘,上面有16枚双面棋子(一面为黑,一面为白), 当翻动一只棋子时,该棋子上下左右相邻的棋子也会同时翻面。 以b代表黑面,w代表白面,给出一个棋盘状态, 问从该棋盘状态开始,使棋盘变成全黑或全白,至少需要翻转多少步 【解题思路】 此题有多种解法,枚举、搜索(BFS、DFS)均可。 搜索的方法网上已有很多人给出(我本人几年前也给过2种搜索代码), 这里补充一下枚举的思路。 ======================================== 首先需要等价变换一下问题,题目需要求的是棋盘从某状态到达全黑/全白的最小翻转步数, 可等价变成棋盘从全黑/全白状态开始,到达指定状态的最小翻转步数,更便于处理 为了简化问题,这里进一步把问题简化为【棋盘从全黑状态开始,到达指定状态的最小翻转步数】 只要求到全黑的答案,全白的答案是可以直接推导的,这个后面再说。 那么枚举的思路就很简单了: ① 预先打表计算出【棋盘从全黑状态开始,到达所有不重复状态的最小翻转步数】 ② 任意给定一个状态,查表有这个状态,则直接输出步数;否则输出不可达 ======================================== 为了便于记录棋盘状态,这里使用二进制数对棋盘状态进行压缩,具体方法如下: 对棋盘矩阵进行编码: * * * * 从左到右分别为第 0, 1, 2, 3位 % % % % 从左到右分别为第 4, 5, 6, 7位 # # # # 从左到右分别为第 8, 9,10,11位 @ @ @ @ 从左到右分别为第12,13,14,15位 由于棋盘每一格只有黑白两种状态,可用0、1进行表示,由此可转换成二进制数: @@@@ #### %%%% **** 15 ← 0 最终可以用一个 unsigned int 数(共32位,这里使用低16位)存储整个棋盘状态, 令 黑=0,白=1: ① 当翻转某一位上的棋子时,相当于使该位与 二进制1 做异或运算. ② 棋盘处于全黑状态时,unsigned int 的低16位值为 0x0000 ③ 棋盘处于全白状态时,unsigned int 的低16位值为 0xFFFF ======================================== 另一方面,不难可以看出每格棋子最多只可以翻转1次, 这是因为只要其中一格重复翻了2次(不论是连续翻动还是不连翻动), 那么它以及周边的棋子和没翻动时的状态是一致的, 由此就可以确定这个4x4棋盘【理论上最多只能翻16次,共2^16 = 65536 种状态】 为了记录翻转过的棋子,可以把 unsigned int 数的高16位也利用起来, 令高16位为棋盘操作位,从全0(全黑)状态开始,翻动过的棋子位置标记为1,没翻动过的标记为0 这样我们就可以通过充分利用一个 unsigned int 数的32位, 记录棋盘在处于某个状态时的编码,包括翻转过的棋子位置、以及当前棋子的翻转情况。 ======================================== 确定了棋盘状态的记录方式,就可以开始枚举所有棋盘状态了。 首先理解几个前提条件: ① 棋盘初始状态为全0(暂不考虑全1) ② 一次只能翻转一个棋子,不能重复翻转同一个棋子(因为会还原状态,无意义) ③ 最多只能翻转 4x4 = 16 次 ④ 第 n 次的翻转操作是基于 第 n-1 次的所有可能的棋盘状态 ⑤ 可能存在不同的翻转次数,会得到相同的棋盘状态 根据①②③④,这里可以使用BFS的思想: 从初始状态开始,分别记录16步(类比16层的树)翻转操作中每一步的所有棋盘状态。 而④⑤可以用来指导BFS过程的剪枝,用于加速搜索: 若第 i、j 步的(i<=j)翻转操作均出现了状态A,那么第j步的状态A舍弃, 这是因为从第j步的状态A又会引申出大量重复状态,且步长必定比第i步长。 ======================================== 上述解题方法是针对初始状态为全0(全黑)情况下的, 那么初始状态为全1(全白)需要重新计算一次所有状态么? 不需要,其实很简单。 当已经计算出初始状态为全0时、在16步内可到达的所有棋盘状态后, 对于给定某个棋盘状态,只要对该棋盘状态取反(黑变白、白变黑), 那么【取反后的棋盘状态到达全0需要的最小步数】等价于【原棋盘状态到达全1需要的最小步数】 注意这里有个误区: 【不能】先计算原棋盘状态到达全0需要的最小步数,再用16减去之 全0与全1导向的结果集并不是减法意义上的互补 事实上,把BFS搜索到的全状态列印出来可以发现: ① 重复状态多达61440种,有效状态只有4096种 ② 有效状态在第8步之前就可全部找到(准确而言是0-7步),且不是呈对称分布 ③ 从全0到全1只需经过4步翻转 ④ 这种结果分布主要是受翻棋时所影响的棋子范围所决定的
#include <set> #include <iostream> using namespace std; // 无符号整型(32位),用于记录棋盘编码,主要为了避免int32的负数影响 // 初始棋盘状态为全0(全黑) // 高16位为棋盘操作位,翻动过的棋子位置标记为1 // 低16位为棋盘状态位, 记录当前棋盘状态(白朝上为1,黑朝上为0) typedef unsigned int _UINT; const static int MAX_STEP = 16; // 可翻棋的最大步数 const static int MAX_STATUS = 65536; // 总状态数 = 2^16(含重复数) // 棋盘状态掩码:当翻转位置i的棋子时,STATUS_MASKS[i]为所有受影响的相邻位置 // 位置i:在4x4棋盘内,从左到右、从上到下按0-15依次编码 const static _UINT STATUS_MASKS[MAX_STEP] = { 0x00000013, 0x00000027, 0x0000004E, 0x0000008C, 0x00000131, 0x00000272, 0x000004E4, 0x000008C8, 0x00001310, 0x00002720, 0x00004E40, 0x00008C80, 0x00003100, 0x00007200, 0x0000E400, 0x0000C800 }; /* * 棋盘对象 */ class Chess { public: Chess(); ~Chess(); int getStep(int status); // 计算从全0或全1状态到达指定棋盘状态的最小步数 private: void bfsAllStatus(void); // 记录不重复地翻动1-16步可得到的所有棋盘状态 _UINT filp(_UINT chess, int bitPos);// 翻动棋盘上某个指定位位置的棋子 int toStatus(_UINT chess); // 从棋盘编码提取棋盘状态信息 int getMaxBitPos(_UINT chess); // 从棋盘编码提取棋盘操作信息,获得其中最大翻转编号的位置 int getFilpCount(_UINT chess); // 从棋盘编码提取棋盘操作信息,获取棋盘从全0状态开始共被翻动棋子的次数 int min(int a, int b); // 返回最小值 private: set<_UINT>* chesses; // 从棋盘全0开始,分别记录不重复地翻动1-16步可得到的所有棋盘编码 int* steps; // 从棋盘全0开始,到达指定棋盘状态需要翻动棋子的最小步数 }; int main(void) { // 一次计算把所有棋盘状态打表 Chess* chess = new Chess(); // 迭代输入棋盘状态 查表 int chessStatus = 0; int byteCnt = 0; char byteBuff[5] = { '