一、题意
这是一个八数码问题的变种。
输入九宫格的初始状态,九宫格的数字为0...8,其中0代表为空位,其余数字代表由相应数字的板块在那个位置上。
然后对九宫格里的板块进行移动,直到变成一个最终状态。
要求输出变成的最远的状态,即操作次数最多的状态。
打印出这个最终状态,同时用“UDLR”输出空位的移动路径。
二、解析
最短路径可以用bfs,最长路径当然也可以用bfs。
只不过这道题目的状态点比较大,需要记录所有9个数字。
这样就有个问题,vis数组应该如何处理?我们不可能开一个9维的数组,也开不起10^9的数组。
这里有三种方案:
- 为这9个数字的数组进行简单编码,然后vis用一个set<int> 进行判定。
这种方法可以作为一个“跳板”方法,来确保主算法正确,然后再修改为下面两种方法之一(当然如果没超时也可以用)。</int> - 哈希法。为这9个数字的数组进行编码后,放到哈希表中进行维护。这里的哈希表需要解决冲突问题,因此实际上是用一个list<> vis[maxn]实现。然后实现相应的insert、query、clear操作即可。
- 康托法。也即是康托展开,它是一种可以完美实现数组和编码相互转换的方法,实际上就是吧数组在全排列中排第几个作为编码值。好处是效率高,不好就是代码量大。
所以还是推荐用哈希法。
三、代码
- 哈希法的实现
#include <iostream> #include <queue> #include <string> #include <list> using namespace std; const int Fx[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; const string Ord = "ULDR"; const int HASH = 100003; class node { public: int val[3][3]; int x, y; string path; node() {} int getCode() const { int res = 0; for(int i = 0; i < 3; i ++) for(int j = 0; j < 3; j ++) res = res * 10 + val[i][j]; return res; } void output() { for(int i = 0; i < 3; i ++) { for(int j = 0; j < 3; j ++) cout << (j ? " " : "") << val[i][j]; cout << "\n"; } } }; int kase; node u0; // 哈希表写法 class HashTable { public: list<int> vis[HASH]; void insert(int val) { vis[val % HASH].push_back(val); } bool query(int val) { for(auto num : vis[val % HASH]) if(num == val) return 1; return 0; } void clear() { for(int i = 0; i < HASH; i ++) vis[i].clear(); } }; HashTable hashTable; void bfs() { queue<node> que; que.push(u0), hashTable.insert(u0.getCode()); while(!que.empty()) { node u = que.front(); que.pop(); for(int k = 0; k < 4; k ++) { node v = u; int nx = v.x + Fx[k][0], ny = v.y + Fx[k][1]; if(nx < 0 || nx >= 3 || ny < 0 || ny >= 3) continue; swap(v.val[v.x][v.y], v.val[nx][ny]), v.x = nx, v.y = ny, v.path += Ord[k]; int hash = v.getCode(); if(hashTable.query(hash)) continue; que.push(v), hashTable.insert(hash); } if(que.empty()) { u.output(); cout << u.path << endl; } } } int main() { cin >> kase; for(int k = 1; k <= kase; k ++) { for(int i = 0; i < 3; i ++) for(int j = 0; j < 3; j ++) { cin >> u0.val[i][j]; if(u0.val[i][j] == 0) u0.x = i, u0.y = j; } u0.path.clear(); hashTable.clear(); cout << "Puzzle #" << k << "\n"; bfs(); cout << endl; } }
- 康托编码实现
#include <iostream> #include <queue> #include <string> #include <cstring> using namespace std; const int Fx[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; const int Fac[9] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320}; const string Ord = "ULDR"; class node { public: int val[3][3]; int x, y; string path; node() {} int getCantor() const { int res = 0; for(int i = 0; i < 9; i ++) { int small = 0; for(int j = i + 1; j < 9; j ++) if(val[j / 3][j % 3] < val[i / 3][i % 3]) small ++; res += small * Fac[8 - i]; } return res; } void output() { for(int i = 0; i < 3; i ++) { for(int j = 0; j < 3; j ++) cout << (j ? " " : "") << val[i][j]; cout << "\n"; } } }; int kase; node u0; bool vis[4000000]; void bfs() { queue<node> que; que.push(u0), vis[u0.getCantor()] = 1; while(!que.empty()) { node u = que.front(); que.pop(); for(int k = 0; k < 4; k ++) { node v = u; int nx = v.x + Fx[k][0], ny = v.y + Fx[k][1]; if(nx < 0 || nx >= 3 || ny < 0 || ny >= 3) continue; swap(v.val[v.x][v.y], v.val[nx][ny]), v.x = nx, v.y = ny, v.path += Ord[k]; int hash = v.getCantor(); if(vis[hash]) continue; que.push(v), vis[hash] = 1; } if(que.empty()) { u.output(); cout << u.path << endl; } } } int main() { cin >> kase; for(int k = 1; k <= kase; k ++) { for(int i = 0; i < 3; i ++) for(int j = 0; j < 3; j ++) { cin >> u0.val[i][j]; if(u0.val[i][j] == 0) u0.x = i, u0.y = j; } u0.path.clear(); memset(vis, 0, sizeof(vis)); cout << "Puzzle #" << k << "\n"; bfs(); cout << endl; } }
四、归纳
- 最终测试了一下,3种方法都能在vjudge中通过,其中set方法用时1660ms,哈希法和康托法差不多,用时分别为1060ms和1100ms.
- 这道题还让我学会了一点,就是C++课上学的class写法也可以大胆地用起来呀!一直以来都是只是用struct存放一些值而已,不过在复杂的题目中,class能意外地让代码变得清晰起来。使用class的话,简单的将struct改成class,然后加一个public将所有变量、方法暴露出来即可,然后还是实例化一个对象出来使用比较不容易出错。