UVa 1604 Eight Cubic-Puzzle 题解

Posted alrond

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UVa 1604 Eight Cubic-Puzzle 题解相关的知识,希望对你有一定的参考价值。

建议用时:50 min

实际用时:0x3f3f3f3f

题目:?? ,我的 AC 代码:??

 

这题让我做得很尴尬啊。

用时就不说了。搞了半天还是花了 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

以上是关于UVa 1604 Eight Cubic-Puzzle 题解的主要内容,如果未能解决你的问题,请参考以下文章

UVa 1604 Eight Cubic-Puzzle 优化方案

习题 7-9 UVA-1604Cubic Eight-Puzzle

HDU 1043 Eight BFS

HDU - 3567 Eight II IDA*

HDU 2514 Another Eight Puzzle(DFS)

CPP-week eight