零、(单源)BFS正确性的证明
首先证明BFS过程用到的队列具有以下两个性质:
- 两端性,即同一时刻队列中的元素只有两种数值,且这两种数值相差 1 1 1
- 单调性:即同一时刻队列中的元素是单调增加的
用归纳法证明:
- 初始时,队列中只有一个元素 0 0 0,满足上述性质;
- 假设某一时刻队列满足上述性质,把队头 x x x 取出进行扩展,若扩展到的点之前未被遍历过,则将其距离 x + 1 x+1 x+1 放入队尾,扩展之后,队列仍满足上述性质。
有了上述性质之后,可以证明BFS的正确性,亦有两种方法:
- 已知堆优化的Dijkstra算法求最短路是正确的,我们把BFS中的队列类比Dijkstra中的优先队列,显然BFS是正确的。
- 由于Dijkstra算法与BFS尚存在一定区别 (Dijkstra中第一次出队一定是最小值,BFS中第一次入队即是最小值),我们不借助Dijkstra算法,而是用归纳法和反证法证明:
(1) 初始时显然;
(2) 假设某一时刻,已经出队的点的最小距离已经确定,且队头 x x x 取出时不是最小值,那么它一定会被队列中后面的点经过某条路径后更新成更短的距离,由于出队的点最小距离已经被确定,故路径上一定不会经过已经出队的点,由队列的单调性和边权均为 1 1 1 可推出该路径的距离一定大于 x x x ,矛盾。
一、矩阵距离
本题需要求出任意一点到离它曼哈顿距离最小的 1 1 1 的曼哈顿距离。一个朴素的想法是,分别以每个 1 1 1 为起点进行BFS,每个点取最小值,这种做法的时间复杂度为 O ( N 2 ) O(N^2) O(N2)。如何优化?
在图论中,若求某点到多个起点中最近的那个起点的最短距离,可以将其转化为单源最短路,只要建立一个虚拟源点,向所有起点连一条边权为 0 0 0 的边即可。在BFS中也可应用类似的思想,只不过不需要建立虚拟源点,而是开始时将所有起点同时入队,某点第一次出队 (入队) 即是最小值。
可以通过(单源)BFS正确性的证明方法证明上述“多源BFS”的正确性,仅在初始时稍有不同,“多源BFS”初始时队列中有多个 0 0 0。
代码实现:
#include <cstdio>
#include <cstring>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair <int, int> PII;
const int N = 1010, M = N * N;
int n, m;
char g[N][N];
int dist[N][N];
PII q[M];
void bfs(){
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
memset(dist, -1, sizeof dist);
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
if (g[i][j] == '1'){
dist[i][j] = 0;
q[++ tt] = {i, j};
}
while (hh <= tt){
PII t = q[hh ++];
for (int i = 0; i < 4; i ++){
int a = t.x + dx[i], b = t.y + dy[i];
if (a < 1 || a > n || b < 1 || b > m) continue;
if (dist[a][b] != -1) continue;
dist[a][b] = dist[t.x][t.y] + 1;
q[++ tt] = {a, b};
}
}
}
int main(){
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++)
scanf("%s", g[i] + 1);
bfs();
for (int i = 1; i <= n; i ++){
for (int j = 1; j <= m; j ++)
printf("%d ", dist[i][j]);
puts("");
}
return 0;
}
二、魔板
本题与八数码类似,把棋盘(矩阵)看成一种状态,均属于“最小步数模型”。这种模型的要点是状态的存储,对于全排列,可以运用康托展开将其与自然数映射;对于更一般的状态,可以使用哈希函数。C++STL中的 map
基于红黑树实现,可以很方便的存储状态,C++11中的 unordered_map
基于哈希函数,效率比 map
更高。这里运用 unordered_map
来存储状态。
题目还要求操作序列的字典序最小,只要在扩展时按照 A 、 B 、 C A、B、C A、B、C 的顺序扩展即可。用数学归纳法容易证明。
本题思路不难,但是代码量较大,操作较繁琐,要注意细节。
代码实现:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>
#include <queue>
using namespace std;
char g[2][4];
unordered_map <string, pair <char, string>> pre;
unordered_map <string, int> dist;
queue <string> q;
void set(string state){ //将存储状态的字符串转化为矩阵
for (int i = 0; i < 4; i ++) g[0][i] = state[i];
for (int i = 3, j = 4; i >= 0; i --, j ++) g[1][i] = state[j];
}
string get(){ //将矩阵转化为字符串
string res;
for (int i = 0; i < 4; i ++) res += g[0][i];
for (int i = 3; i >= 0; i --) res += g[1][i];
return res;
}
string move0(string state){ //操作A
set(state);
for (int i = 0; i < 4; i ++) swap(g[0][i], g[1][i]);
return get();
}
string move1(string state){ //操作B
set(state);
char v0 = g[0][3], v1 = g[1][3];
for (int i = 3; i; i --)
for (int j = 0; j < 2; j ++)
g[j][i] = g[j][i - 1];
g[0][0] = v0, g[1][0] = v1;
return get();
}
string move2(string state){ //操作C
set(state);
char v = g[0][1];
g[0][1] = g[1][1];
g[1][1] = g[1][2];
g[1][2] = g[0][2];
g[0][2] = v;
return get();
}
void bfs(string start, string end){
if (start == end) return;
q.push(start);
dist[start] = 0;
while (!q.empty()){
string t = q.front();
q.pop();
string m[3];
m[0] = move0(t);
m[1] = move1(t);
m[2] = move2(t);
for (int i = 0; i < 3; i ++){
string str = m[i];
if (!dist.count(str)){
dist[str] = dist[t] + 1;
pre[str] = {char('A' + i), t};
if (str == end) return;
q.push(str);
}
}
}
}
int main(){
int x;
string start, end;
for (int i = 0; i < 8; i ++){
cin >> x;
end += char(x + '0');
}
for (int i = 1; i <= 8; i ++)
start += char(i + '0');
bfs(start, end);
cout << dist[end] << endl;
string res;
while (end != start){
res += pre[end].first;
end = pre[end].second;
}
reverse(res.begin(), res.end());
if (res.size()) cout << res << endl;
return 0;
}
三、电路维修
本题可以看成求左上角到右下角的最短路,其中边权有两种:电缆不需旋转时,边权为 0 0 0;电缆需要旋转时,边权为 1 1 1。
在这网格中有这样一个性质:有一半的点 (下图中标红的点) 无法从左上角到达,容易发现,这些点的横纵坐标之和为奇数。
由于电缆是连接对角线的两个端点,每次移动时,所在点的横、纵坐标均会变化,且前后差的绝对值为
1
1
1,故横纵坐标之和的奇偶性不变。设初始点为
(
0
,
0
)
(0,0)
(0,0),故只能到达横纵坐标之和为偶数的点。因此,当
R
+
C
R+C
R+C 为奇数时,右下角无法到达。
对于边权只有 0 0 0 和 1 1 1 的最短路问题,有一种经典的做法:双端队列BFS。对于边权为 0 0 0 的扩展,将扩展后的点从队头插入;边权为 1 1 1 的扩展,将扩展后的点从队尾插入。这样可以保证队列的“两段性”、“单调性”,证明方法类似前面BFS的证明。
需要注意使用双端队列BFS时,由于边权并不相同,终点第一次入队时不一定是最小值,终点第一次出队才是最小值。
代码实现:
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <deque>
#define x first
#define y second
using namespace std;
typedef pair <int, int> PII;
const int N = 510;
int n, m;
char g[N][N];
int dist[N][N];
bool st[N][N];
int bfs(){
deque <PII> q;
memset(dist, 0x3f, sizeof dist);
memset(st, 0, sizeof st);
char cs[5] = "\\/\\/";
int dx[4] = {-1, -1, 1, 1}, dy[4] = {-1, 1, 1, -1};
int ix[4] = {-1, -1, 0, 0}, iy[4] = {-1, 0, 0, -1}; //坐标在g中的偏移量
dist[0][0] = 0;
q.push_back({0, 0});
while (!q.empty()){
PII t = q.front();
q.pop_front();
int x = t.x, y = t.y;
if (x == n && y == m) return dist[x][y];
if (st[x][y]) continue;
st[x][y] = 1;
for (int i = 0; i < 4; i ++){
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a > n || b < 0 || b > m) continue;
int ga = x + ix[i], gb = y + iy[i];
int w = g[ga][gb] != cs[i];
int d = dist[x][y] + w;
if (d < dist[a][b]){
dist[a][b] = d;
if (w) q.push_back({a, b});
else q.push_front({a, b});
}
}
}
return -1;
}
int main(){
int T;
scanf("%d", &T);
while (T --){
scanf("%d %d", &n, &m);
for (int i = 0; i < n; i ++)
scanf("%s", g[i]);
if (n + m & 1) puts("NO SOLUTION");
else printf("%d\n", bfs());
}
return 0;
}