建议用时:50 min
实际用时:0x3f3f3f3f
这题让我做得很尴尬啊。
用时就不说了。搞了半天还是花了 1500 ms AC。我有时候真的想不明白那些大神是怎样在 300 ms 内 AC 过的。
先分析一下题目吧。
题目首先给出一种染色过的方块。方块相对的面的颜色是一样的。分别记为 “W” “B” “R”。
题目接着说,现在有一个 3 * 3 的格子。其中八个格子有染色的方块,另外一个是空格。
我们要完成的任务是:每次给定一个一种俯视图,一个初始空格位置。问最小要多少步可以将初始俯视图通过滚动方块转换为目标图。
读完题目,不难发现,每个方块,先不管它在方格里的位置,一共有 6 种状态。
这好说。想象从右上角往左下角看一个方块。可以看到正面,上面和右面。现在要把 3 种颜色涂在三个面上,自然有 6 种涂法,每种涂色方法构造的方块的状态是不一样的。
这里我们定义一个方块的状态,就是定义为它的上右前三个面的颜色的排列。可以验证对于不同的涂色的排列这个方块是不同于其他排列的。
对于两个方块,如果他们纵向或横向滚动后在俯视方向的颜色不同,就说这两个方块的状态不同,
分析完状态的定义,先看看算法,待会再说状态的实现。
算法很简单粗暴了。先建立好恰当的状态表示方法,然后用 BFS 横向搜索,一层一层直到搜到正解为止。
但是如果这个算法可以过,我就没有必要做这道题了,对吧?(~~)
为什么呢?因为出题人存心要卡常数。
如果用单纯的 bfs,会 TLE。
然后怎么办呢?还有比 BFS 更高效的算法吗?
就我所知,还不知道这类题又没有更高效的解法。
BUT,要记住,做题时一定要多个心眼。
如果算法已经不能进一步简化了,可以考虑缩减常数。
下面隆重介绍 BFS 的好兄弟:
“双向 BFS”。
这两位出身同门,长相也几乎一样。
然而,”双向 bfs“ 深谙常数的哲学,对于 “单向 BFS” 解决不了的题,他有办法。
回到正题,别扯远了。
严肃,严肃。??
下面来看双向 BFS。
双向 BFS,顾名思义,是从一头一尾两个方向往中间搜索。可以想像一个三角形。如果我们要找出发点对边的中点(目标点事实上在 BFS 的解答数的最后一层的某个位置),
并且从上往下搜的话,BFS 将会扫过整个三角形的面积。然而,如果用双向 BFS,一头从起点开始,一头从终点开始,各自记录沿途点到最上面和最下面的距离。当两支线遇到了同一个
点时,就可以把这个点当作连接起点和终点的桥,即最短路通过这个点。并且我们的目标最短路就是这个点到起点和终点的最短路之和。那么最短距离自然就是两个支线上的最短距离之和了。
(下面模拟了一下搜索范围)(表述不清请见谅~)
S
/\
/ \
/ \
/ \
/ \ / \
/ \ / \
/ \ / \
/________ \ /________ \
T
好吧,算法确定了。现在最麻烦的时候到了。
上面说到,方块总共有 6 种状态,加上空的,一共 7 种。那么整个网格就有 9 *(6 ^ 8)= 15116544 种状态。然而我为了待会儿做哈希方便,干脆取 7 ^ 9 = 40353607 种。常数区别。
好了。确定了规模,再来看对单个状态的表示方法。
状态有两个:单个方块的状态和整个网格(方块集合)的状态。
对于单个方块的状态,可以用 0 ... 6 的数字表示。
我在草稿上画出了 6 种状态的图。
对于方块集合的状态,用一个 3 * 3 的数组来存。
一个集合有两个状态:1)俯视图案 2)每个方块的状态(决定了 7 ^ 9 的规模)。
了解了上述状态,代码才好实现。
?? BFS 时要用状态 2),不能用状态 1)!
现在把具体算法说一下。
整体上:
1)读入数据,把处理成数字。
2)既然有目标状态,是状态 1),那么就要找到关联的状态 2),全部装进 2 号队列里。(所谓关联,就是状态 2)的某一实例的俯视图是状态 1),每个状态 1)一共有 256 种对应的状态 2))
3)1 号队列装入初始状态,与 2 号队列交错搜索。当搜到对方处理过的点,就输出。
对于步骤 1),需要一个 map。
对于步骤 2),需要 DFS 构造。有一点要明白,因为每一种颜色(特指俯视的那个)可以对应 2 种不同的方块状态。
下面给出算法:
1)从 (0,0)开始调用
2)如果到了(3,0),那么就够造了一个完整的状态。可以压缩为整数,装进队列 2 里。返回上一层。
2)对于(r,c),先把这个格子赋值为颜色对应的方格状态。调用 2),参数为下一个格子的坐标。
3)如果(r,c)上的颜色不为 ‘E‘,那么就赋上另一个值,接着调用一次 2),参数同样为下一个格子的坐标。
对于 BFS 的搜索算法,要注意的有:
1)控制好每次搜索在且只在一层内。
2)处理好最短路的相加的问题。最后的答案应该是 dist[0] + dist[1] + 1,稍微调试就可以把后面的 1 整出来。
3)不要忘了处理 30 的限制。
算法弄清楚后,可以写代码了。
到目前为止还没有说怎样表示状态。
用通常的思路就是,1,2,3,4,5,6 分别表示一种方块朝向的状态,1,2,3 表示颜色,用数组构造一个从朝向到颜色的映射。
然而出于强迫症的考虑,我把状态弄成这样:
0 1 2 3 4 5 6 0 1 2 3
E W B R W B R E W B R
左边是朝向状态对应的俯视颜色,右边则是颜色的标号。
不难发现,其中有一些奇妙的规律。
这对下面的操作是有好处的。(虽然似乎没有必要)
说不如看,对吧?下面是数据结构。
1 namespace data_structure { 2 3 // 0 1 2 3 4 5 6 4 // E W B R W B R 5 6 // 0 1 2 3 7 // E W B R 8 9 const int dc[4] = { 0, 1, -1, 0 }; // up right left down 10 const int dr[4] = { -1, 0, 0, 1 }; 11 12 const int rota[7][2] = { // 翻转对应状态的变化 13 {0, 0}, 14 {6, 2}, 15 {3, 1}, 16 {2, 4}, 17 {5, 3}, 18 {4, 6}, 19 {1, 5} 20 }; 21 22 const int cubics[] = { 0, 1, 2, 3, 1, 2, 3 }; // 状态对应的颜色 23 24 struct Status { 25 int p[3][3], r, c, h; 26 Status(const int q[3][3], int r, int c, int h) : r(r), c(c), h(h) { 27 memcpy(p, q, sizeof(p)); 28 } 29 }; 30 31 } using namespace data_structure;
下面是主算法前的准备代码。包括翻转,判断 0 ,哈希,找末尾 256 种状态(和一大堆变量声明)。
1 namespace data_structure { 2 3 // 0 1 2 3 4 5 6 4 // E W B R W B R 5 6 // 0 1 2 3 7 // E W B R 8 9 const int dc[4] = { 0, 1, -1, 0 }; // up right left down 10 const int dr[4] = { -1, 0, 0, 1 }; 11 12 const int rota[7][2] = { // first up & down, then left & right 13 {0, 0}, 14 {6, 2}, 15 {3, 1}, 16 {2, 4}, 17 {5, 3}, 18 {4, 6}, 19 {1, 5} 20 }; 21 22 const int cubics[] = { 0, 1, 2, 3, 1, 2, 3 }; 23 24 struct Status { 25 int p[3][3], r, c, h; 26 Status(const int q[3][3], int r, int c, int h) : r(r), c(c), h(h) { 27 memcpy(p, q, sizeof(p)); 28 } 29 }; 30 31 } using namespace data_structure; 32 33 namespace declarations { 34 35 //const int max_color = 262144; 36 const int max_number = 40353607; 37 int ec, er; // 初始空格 38 int tc, tr; // 目标空格 39 int tar[3][3]; // 4进制 40 int src[3][3]; // 7进制 41 int source; // 源点哈希 42 map<char, int> c2i; 43 bool vis_by[2][max_number]; 44 int dist[max_number]; 45 int temp[3][3]; // 构造末尾状态时临时存数据 46 queue<Status> q[2]; 47 48 } using namespace declarations; 49 50 namespace processors { 51 52 void rot(int p[3][3], int pr, int pc, int r, int c, int dir) { 53 swap(p[r][c], p[pr][pc]); 54 int id = p[r][c]; dir = min(dir, 3 - dir); 55 p[r][c] = rota[id][dir]; 56 } 57 58 bool are_same(int p[3][3], int q[3][3]) { 59 for (int i = 0; i <= 2; i++) 60 for (int j = 0; j <= 2; j++) 61 if (cubics[p[i][j]] != cubics[q[i][j]]) // 这里是亮点(~~) 62 return false; 63 return true; 64 } 65 66 int num_hash(int p[3][3]) { 67 int res = 0; 68 for (int i = 0; i <= 2; i++) 69 for (int j = 0; j <= 2; j++) 70 res = res * 7 + p[i][j]; // 七进制转为十进制 71 return res; 72 } 73 74 void iterate(int r, int c) { 75 if (r == 3) { 76 int end = num_hash(temp); 77 q[1].push(Status(temp, tr, tc, end)); 78 vis_by[1][end] = 1; 79 dist[end] = 0; 80 return; 81 } 82 83 int nr = (r * 3 + c + 1) / 3; // 小细节 84 int nc = (r * 3 + c + 1) % 3; 85 86 temp[r][c] = tar[r][c]; 87 88 iterate(nr, nc); 89 90 if (tar[r][c] != 0) { // 注意 91 temp[r][c] = tar[r][c] + 3; 92 iterate(nr, nc); 93 } 94 } 95 96 } using namespace processors;
好了,下面是重头戏。
1 namespace main_functions { 2 3 int bfs() { 4 if (are_same(tar, src)) return 0; // 特判 5 while (!q[0].empty()) q[0].pop(); 6 while (!q[1].empty()) q[1].pop(); 7 source = num_hash(src); 8 vis_by[0][source] = 1; dist[source] = 0; 9 q[0].push(Status(src, er, ec, source)); // 这里把哈希值也并在结构体里面,省去每次都要算哈希的麻烦 10 iterate(0, 0); 11 int step = 0; // 记录到现在为止两个队列共行进了几层 12 while (!q[0].empty() || !q[1].empty()) { // 标准的双向 DFS 13 for (int i = 0; i <= 1; i++) { 14 int lim = (int)q[i].size(); // 这里是重点,这样限制搜且仅搜一层 15 if (step > 30) return -1; 16 bool update = false; 17 while (lim--) { // 限制层数 18 const Status cur = q[i].front(); q[i].pop(); 19 const int r = cur.r, c = cur.c, cur_id = cur.h; 20 int cpy[3][3]; memcpy(cpy, cur.p, sizeof(cpy)); 21 for (int dir = 0; dir <= 3; dir++) { 22 const int nr = r + dr[dir]; 23 const int nc = c + dc[dir]; 24 if (nr > 2 || nc > 2 || nr < 0 || nc < 0) continue; 25 rot(cpy, nr, nc, r, c, dir); // 转一下 26 const int int_id = num_hash(cpy); 27 if (!vis_by[i][int_id]) { // 必要的 28 if (vis_by[i^1][int_id]) { // 重点 29 int res = dist[int_id] + dist[cur_id] + 1; // “+ 1” 30 return res <= 30 ? res : -1; // 判断是否过线(貌似没必要) 31 } 32 vis_by[i][int_id] = 1; 33 dist[int_id] = dist[cur_id] + 1; 34 q[i].push(Status(cpy, nr, nc, int_id)); 35 update = true; 36 } 37 rot(cpy, r, c, nr, nc, dir); // 别忘了转回去 38 } 39 } 40 if (update) step++; 41 } 42 } 43 return -1; 44 } 45 46 void solve() { 47 for (int i = 0; i <= 2; i++) 48 for (int j = 0; j <= 2; j++) src[i][j] = 1; // 不能用 memset,memset 只适用于 -1 和 0, 1 代表规定的初始状态 49 src[er][ec] = 0; // 0 是空格50 fill(dist, dist + max_number, 31); // 这个可以随便赋值的,只要保证起点的 dist 设为 0 就可以了 51 memset(vis_by, 0, sizeof(vis_by));52 cout << bfs() << endl; 53 } 54 55 } using namespace main_functions;
这题的细节还是很麻烦的。(我太水的认为)
不容易啊,花了很长时间才搞定。
2018-01-27