题目链接:The Morning after Halloween
题目描述:
给定一个二维矩阵,图中有障碍物和字母,你需要把小写字母移动到对应的大写字母位置,不同的小写字母可以同时移动(上下左右四个方向或者保持不动 ),但是移动之后两个字母不能重合,同时移动后不能是两个字母发生了位置的交换。
上图给出了一种情况即该情况下的四种合法移动。
同时题目保证任意一个 2 × 2 2\times2 2×2的子区域中一定至少含有一个障碍物,同时边界一定是障碍物。
题解:
我们可以把每个矩阵的看成一个状态进行 B F S BFS BFS但是这样状态的个数比较多。
由于题目保证了很小的区域有障碍这也就变相说明了题目可以到达的点并不会很多,所以我们可以把每个可以经过的点进行编号,然后记录字母落在点的编号的三元组(缺少的字母用 0 0 0来表示,这意味着点的编号需要从 1 1 1开始),然后将三元组看成是状态,而每一次移动看成是一条边,这样我们只需要进行 B F S BFS BFS即可找到目标状态的最少移动次数(在移动的时候,我们需要判断是否违反规则,例如移动后两个字母在同一个位置,或者移动导致两个相邻的字母发生的交换)。由于本题的目标状态也是已知的,那么我们可以使用双向 B F S BFS BFS也就是从目标状态和初始状态同时进行 B F S BFS BFS当在中间的某个状态相遇的时候,就意味着找到了最少花费,在代码中我们只需要两个队列就可以实现双向 B F S BFS BFS一个放入初始状态进行搜索,一个放入目标状态进行搜索。
为什么需要双向 B F S BFS BFS?双向 B F S BFS BFS通常情况下能够让时间复杂度下降。原理实际上是因为每一次都选择含有较少的节点进行扩展,这样能一定程度上节省时间。
双向 B F S BFS BFS的代码应该是每次选择较小的队列进行扩展,而不是轮流进行扩展(如果是轮流进行扩展那么实际上和单向 B F S BFS BFS没有太大的差别)。
代码:
#include <bits/stdc++.h>
const int DIRECTION_NUM = 5;
const int MAXH = 16 + 1;
const int MAXW = 16 + 1;
const int MAX_NODE = MAXH * MAXW;
using namespace std;
int w, h, n, nodeNum;
char g[MAXH][MAXW];
int dx[] = {0, 1, -1, 0, 0};
int dy[] = {0, 0, 0, 1, -1};
vector<int> es[MAX_NODE];
int nodeId[MAXH][MAXW];
map<char, int> s, t;
int dis[MAX_NODE][MAX_NODE][MAX_NODE];
int inQ[MAX_NODE][MAX_NODE][MAX_NODE];
struct State
{
int a, b, c; //最多三个字母位置的结点编号
State() {}
State(int a, int b, int c) : a(a), b(b), c(c) {}
State& operator=(map<char, int> &rhs)
{
a = b = c = 0;
auto iter = rhs.begin();
for (size_t i = 0; i < n; i++) {
if (i == 0) { a = iter->second; }
else if (i == 1) { b = iter->second; }
else if (i == 2) { c = iter->second; }
iter++;
}
return *this;
}
}st, ed;
void buildGraph()
{
nodeNum = 1;
s.clear();
t.clear();
for (int i = 0; i < MAX_NODE; i++) { es[i].resize(0); }
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
if (g[i][j] == '#') { continue; }
nodeId[i][j] = nodeNum++;
if (islower(g[i][j])) {
s[g[i][j]] = nodeId[i][j];
} else if (isupper(g[i][j])) {
t[g[i][j]] = nodeId[i][j];
}
}
}
st = s;
ed = t;
es[0].push_back(0);
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
if (g[i][j] == '#') { continue; }
for (int k = 0; k < DIRECTION_NUM; k++) {
int nx = i + dx[k];
int ny = j + dy[k];
if (g[nx][ny] == '#') { continue; }
es[nodeId[i][j]].push_back(nodeId[nx][ny]);
}
}
}
}
bool conflict(int newA, int newB, int a, int b) { return (newA == newB && newA != 0) || (newA == b && newB == a && newA != 0 && newB != 0); }
int bfs(queue<State> &q, int nowQ)
{
int size = q.size();
int a, b, c;
while(size--) {
State now = q.front();
q.pop();
a = now.a, b = now.b, c = now.c;
for (auto newA : es[a]) {
for (auto newB : es[b]) {
if (conflict(newA, newB, a, b)) { continue; }
for (auto newC : es[c]) {
if (conflict(newA, newC, a, c) || conflict(newB, newC, b, c)) { continue; }
if (inQ[newA][newB][newC] == nowQ) { continue; }
if (inQ[newA][newB][newC] == 3 - nowQ) { // 在另一个队列中
cout << dis[newA][newB][newC] + dis[a][b][c] + 1 << endl;
return 0;
}
dis[newA][newB][newC] = dis[a][b][c] + 1;
inQ[newA][newB][newC] = nowQ;
q.push({newA, newB, newC});
}
}
}
}
return -1;
}
void dBfs()
{
queue<State> qs;
queue<State> qt;
memset(inQ, 0, sizeof(inQ));
qs.push(st);
qt.push(ed);
dis[st.a][st.b][st.c] = 0;
inQ[st.a][st.b][st.c] = 1;
dis[ed.a][ed.b][ed.c] = 0;
inQ[ed.a][ed.b][ed.c] = 2;
while (!qs.empty() && !qt.empty()) {
if (qs.size() < qt.size()) { // 优先选择队列短的队列进行bfs
if(bfs(qs, 1) != -1) { return; }
}
else {
if(bfs(qt, 2) != -1) { return; };
}
}
}
int main()
{
ios::sync_with_stdio(false);
while(cin >> w >> h >> n) {
if (w == 0 && h == 0 && n == 0) { break; }
cin.get(); //读取末尾的换行
for (int i = 0; i < h; i++) { cin.getline(g[i], MAXW); } // cin.getline读取的数据末尾没有'\n', cin.getline会保证末尾有结束符0,所以最多读取MAXW-1个有效字符
buildGraph();
dBfs();
}
return 0;
}