广度优先搜索(BFS)
BFS介绍
树和图的广度优先遍历都需要使用一个队列来实现。
在过程中,我们不断从队头取出一个节点x,对于x面对的多条分支,把沿着每条分支到达的下一个尚未访问过的节点插入队尾。
BFS有两个重要性质:
- 在访问完所有的第 i i i 层节点后,才会开始访问第 i + 1 i+1 i+1 层节点。
- 任意时刻,队列中至多有两个层次的节点。若其中一部分节点属于第 i i i 层,那么另一部分节点就属于第 i + 1 i+1 i+1 层,并且所有第 i i i 层节点排在第 i + 1 i+1 i+1 层节点之前。也就是说,广度优先遍历队列中的元素关于层次满足“两段性”和“单调性”。
拓扑排序
参考代码如下:
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++, d[b]++;
}
void topsort()
{
queue<int> q;
for(int i = 1; i <= n; i++)
if(!d[i]) q.push(i);
while(q.size()) {
int u = q.front(); q.pop();
top_q[++cnt] = u;
for(int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if(--d[v] == 0) q.push(v);
}
}
}
例题
可达性统计 (CH2001)
代码实现
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <bitset>
using namespace std;
const int N = 3e4 + 10;
int n, m;
int h[N], e[N], ne[N], d[N], idx;
int q[N];
int a[N], cnt;
bitset<N> f[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++, d[b] ++;
}
void topsort()
{
int hh = 0, tt = -1;
for(int i = 1; i <= n; i++)
if(d[i] == 0) q[++tt] = i;
while(hh <= tt)
{
int u = q[hh ++];
a[cnt++] = u;
for(int i = h[u]; ~i; i = ne[i])
if(--d[e[i]] == 0) q[++ tt] = e[i];
}
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while(m--)
{
int a, b;
cin >> a >> b;
add(a, b);
}
topsort();
for(int i = cnt - 1; i >= 0; i--)
{
int u = a[i];
f[u][u] = 1;
for(int j = h[u]; ~j; j = ne[j])
f[u] |= f[e[j]];
}
for(int i = 1; i <= n; i++)
cout << f[i].count() << endl;
return 0;
}
Bloxorz (POJ3322)
详情见蓝书。
代码实现
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N = 510;
struct rec { int x, y, lie; };
int n, m;
char s[N][N];
int d[N][N][3];
rec st, ed;
const int next_x[3][4] = { {0, 0, -2, 1}, {0, 0, -1, 1}, {0, 0, -1, 2} };
const int next_y[3][4] = { {-2, 1, 0, 0}, {-1, 2, 0, 0}, {-1, 1, 0, 0} };
const int next_lie[3][4] = { {1, 1, 2, 2}, {0, 0, 1, 1}, {2, 2, 0, 0} };
inline bool valid(int x, int y)
{
return x >= 0 && x < n && y >= 0 && y < m;
}
inline bool valid(rec t)
{
int x = t.x, y = t.y, lie = t.lie;
if(!valid(x, y)) return false;
if(s[x][y] == '#') return false;
if(lie == 0 && s[x][y] != '.') return false;
if(lie == 1 && s[x][y + 1] == '#') return false;
if(lie == 2 && s[x + 1][y] == '#') return false;
return true;
}
void parse_st_ed()
{
int dx[4] = {0, 0, -1, 1}, dy[4] = {-1, 1, 0, 0};
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
if(s[i][j] == 'O') {
ed.x = i, ed.y = j, ed.lie = 0, s[i][j] = '.';
}
else if(s[i][j] == 'X') {
for(int k = 0; k < 4; k++) {
int x = i + dx[k], y = j + dy[k];
if(valid(x, y) && s[x][y] == 'X') {
st.x = min(i, x), st.y = min(j, y);
st.lie = k < 2 ? 1 : 2;
s[i][j] = s[x][y] = '.';
break;
}
}
if(s[i][j] == 'X') st.x = i, st.y = j, st.lie = 0;
}
}
int bfs()
{
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
for(int k = 0; k < 3; k++)
d[i][j][k] = -1;
queue<rec> q;
q.push(st);
d[st.x][st.y][st.lie] = 0;
while(q.size()) {
rec now = q.front(); q.pop();
if(now.x == ed.x && now.y == ed.y && now.lie == ed.lie) return d[now.x][now.y][now.lie];
for(int i = 0; i < 4; i++) {
rec next = {now.x + next_x[now.lie][i], now.y + next_y[now.lie][i], next_lie[now.lie][i]};
if(!valid(next)) continue;
int x = next.x, y = next.y, lie = next.lie;
if(d[x][y][lie] == -1) {
d[x][y][lie] = d[now.x][now.y][now.lie] + 1;
q.push(next);
}
}
}
return -1;
}
int main()
{
while(scanf("%d %d", &n, &m) != EOF && n)
{
for(int i = 0; i < n; i++) scanf("%s", s[i]);
parse_st_ed();
int ans = bfs();
if(ans == -1) puts("Impossible");
else printf("%d\n", ans);
}
return 0;
}
矩阵距离 (CH2501)
题目描述:
给定一个 N N N 行 M M M 列的 01 01 01 矩阵 A A A, A [ i ] [ j ] A[i][j] A[i][j] 与 A [ k ] [ l ] A[k][l] A[k][l] 之间的曼哈顿距离定义为:
d i s t ( A [ i ] [ j ] , A [ k ] [ l ] ) = ∣ i − k ∣ + ∣ j − l ∣ d i s t ( A [ i ] [ j ] , A [ k ] [ l ] ) = ∣ i − k ∣ + ∣ j − l ∣ dist(A[i][j],A[k][l])=|i−k|+|j−l|dist(A[i][j],A[k][l])=|i−k|+|j−l| dist(A[i][j],A[k][l])=∣i−k∣+∣j−l∣dist(A[i][j],A[k][l])=∣i−k∣+∣j−l∣
输出一个 N N N 行 M M M 列的整数矩阵 B B B,其中:
B [ i ] [ j ] = m i n 1 ≤ x ≤ N , 1 ≤ y ≤ M , A [ x ] [ y ] = 1 d i s t ( A [ i ] [ j ] , A [ x ] [ y ] ) B[i][j]=min1≤x≤N,1≤y≤M,A[x][y]=1dist(A[i][j],A[x][y]) B[i][j]=min1≤x≤N,1≤y≤M,A[x][y]=1dist(A[i][j],A[x][y])
代码实现:
/*
主要是运用了bfs队列的两段性和单调性。
在把矩阵中每一个1看作起点的情况下,把这些初始起点插入到队列中
根据bfs逐层搜索(队列的单调性),能保证所有的0都是被离它最近的1遍历到的。
*/
#include <iostream>
#include <algorithm>
#include <cstring>
#define endl "\n"
using namespace std;
typedef pair<int, int> PII;
const int N = 1010, M = N * N;
int n, m;
char g[N][N];
int dis[N][N];
PII q[M];
void bfs()
{
memset(dis, -1, sizeof dis);
int hh = 0, tt = -1;
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
if(g[i][j] == '1') {
dis[i][j] = 0;
q[++ tt] = {i, j};
}
int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, 1, -1};
while(hh <= tt) {
PII t = q[hh ++];
int tx = t.first, ty = t.second;
for(int i = 0; i < 4; i++) {
int x = tx + dx[i], y = ty + dy[i];
if(x < 0 || x >= n || y < 0 || y >= m) continue;
if(dis[x][y] > -1) continue;
dis[x][y] = dis[tx][ty] + 1;
q[++ tt] = {x, y};
}
}
}
int main()
{
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 0; i < n; i++) cin >> g[i];
bfs();
for(int i = 0; i < n; i++) {
for(int j = 0; j < m; j++)
cout << dis[i][j] << " ";
cout << endl;
}
return 0;
}
Pushing Boxes (POJ1475)
思路见蓝书。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
#define F first
#define S second
using namespace std;
typedef pair<int, int> PII;
const int N = 25;
struct Node { int x, y, dir; };
int n, m;
char g[N][N];
bool st[N][N][4], used[N][N];
Node pre[N][N][4];
PII dist[N][N][4];
vector<int> path[N][N][4];
int pre_man[N][N];
const int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};
bool valid(int x, int y)
{
return x >= 0 && x < n && y >= 0 && y < m && g[x][y] != '#';
}
int bfs_man(PII start, PII end, PII box, vector<int> &seq)
{
memset(used, false, sizeof used);
memset(pre_man, -1, sizeof pre_man);
queue<PII> q;
q.push(start);
used[start.F][start.S] = true;
used[box.F][box.S] = true;
while(q.size())
{
auto t = q.front(); q.pop();
int x = t.F, y = t.S;
if(t == end)
{
seq.clear();
while(pre_man[x][y] != -1)
{
int dir = pre_man[x][y];
seq.push_back(dir);
x -= dx[dir], y -= dy[dir];
}
return seq.size();
}
for(int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if(valid(a, b) && !used[a][b])
{
used[a][b] = true;
pre_man[a][b] = i;
q.push({a, b});
}
}
}
return -1;
}
bool bfs_box(PII man, PII box, Node &ed)
{
memset(st, false, sizeof st);
queue<Node> q;
int x = box.F, y = box.S;
for(int i = 0; i < 4; i++)
{
int a = x - dx[i], b = y - dy[i];
int j = x + dx[i], k = y + dy[i];
vector<int> seq;
if(valid(a, b) && valid(j, k) && bfs_man(man, {a, b}, box, seq) != -1)
{
st[j][k][i] = true;
pre[j][k][i] = {x, y, -1};
path[j][k][i] = seq;
dist[j][k][i] = {1, seq.size()};
q.push({j, k, i});
}
}
bool flag = false;
PII man_d = {1e9, 1e9};
while(q.size())
{
auto t = q.front(); q.pop();
if(g[t.x][t.y] == 'T')
{
flag = true;
if(dist[t.x][t.y][t.dir] < man_d)
{
man_d = dist[t.x][t.y][t.dir];
ed = t;
}
}
for(int i = 0; i < 4; i++)
{
int a = t.x - dx[i], b = t.y - dy[i];
int j = t.x + dx[i], k = t.y + dy[i];
if(valid(a, b) && valid(j, k))
{
PII &p = dist[j][k][i];
vector<int> seq;
int distance = bfs_man({t.x - dx[t.dir], t.y - dy[t.dir]}, {a, b}, {t.x, t.y}, seq);
if(distance != -1)
{
PII td = {dist[t.x][t.y][t.dir].F + 1, dist[t.x][t.y][t.dir].S + distance};
if(!st[j][k][i])
{
st[j][k][i] = true;
pre[j][k][i] = t;
path[j][k][i] = seq;
p = td;
q.push({j, k, i});
}
else if(p > td)
{
p = td;
pre[j][k][i] = t;
path[j][k][i] = seq;
}
}
}
}
}
return flag;
}
int main()
{
int T = 1;
while(cin >> n >> m, n)
{
printf("Maze #%d\n", T ++);
for(int i = 0; i < n; i++) cin >> g[i];
PII man, box;
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
if(g[i][j] == 'S') man = {i, j};
else if(g[i][j] == 'B') box = {i, j};
Node ed;
if(!bfs_box(man, box, ed)) puts("Impossible.");
else {
char ops[] = "nswe";
string res = "";
while(ed.dir != -1)
{
res += ops[ed.dir] - 32;
for(auto dir : path[ed.x][ed.y][ed.dir]) res += ops[dir];
ed = pre[ed.x][ed.y][ed.dir];
}
reverse(res.begin(), res.end());
cout << res << endl;
}
puts("");
}
return 0;
}
Flood Fill
Flood-Fill算法是一个非常常见的算法,具体就类似于对一片低洼地区一直倒水直到水面与其他平面持平。
每次遍历到一个“低洼”位置的时候,我们就可以进行一次广搜,把所有同“高度”的“低洼”位置都覆盖过去。具体可以看例题。
例题
池塘计数
题目描述:
农夫约翰有一片 N ∗ M N∗M N∗M 的矩形土地。
最近,由于降雨的原因,部分土地被水淹没了。
现在用一个字符矩阵来表示他的土地。
每个单元格内,如果包含雨水,则用“W”表示,如果不含雨水,则用”.”表示。
现在,约翰想知道他的土地中形成了多少片池塘。
每组相连的积水单元格集合可以看作是一片池塘。
每个单元格视为与其上、下、左、右、左上、右上、左下、右下八个邻近单元格相连。
请你输出共有多少片池塘,即矩阵中共有多少片相连的“W”块。
代码实现:
#include <cstdio>
#include <algorithm>
#define sc scanf
#define pf printf
using namespace std;
typedef pair<int, int> PII;
const int N = 1010, M = N * N;
int n, m;
char mp[N][N];
bool vis[N][N];
PII q[M];
void bfs(int sx, int sy)
{
int hh = 0, tt = -1;
q[++tt] = {sx, sy};
vis[sx][sy] = true;
while(hh <= tt) {
PII t = q[hh++];
int dx = t.first, dy = t.second;
for(int tx = -1; tx <= 1; tx++)
for(int ty = -1; ty <= 1; ty++) {
int x = dx + tx, y = dy + ty;
if(x == sx && y == sy) continue;
if(x < 0 || x >= n || y < 0 || y >= m) continue;
if(vis[x][y] || mp[x][y] == '.') continue;
q[++tt] = {x, y};
vis[x][y] = true;
}
}
}
int main()
{
sc("%d %d", &n, &m);
for(int i = 0; i < n; i++) sc("%s", &mp[i]);
int cnt = 0;
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
if(mp[i][j] == 'W' && !vis[i][j]) {
bfs(i, j);
++cnt;
}
pf("%d\n", cnt);
return 0;
}
城堡问题
题目描述:
1 2 3 4 5 6 7
#############################
1 # | # | # | | #
#####---#####---#---#####---#
2 # # | # # # # #
#---#####---#####---#####---#
3 # | | # # # # #
#---#########---#####---#---#
4 # # | | | | # #
#############################
(图 1)
# = Wall
| = No wall
- = No wall
方向:上北下南左西右东。
图1是一个城堡的地形图。
请你编写一个程序,计算城堡一共有多少房间,最大的房间有多大。
城堡被分割成 m ∗ n m∗n m∗n 个方格区域,每个方格区域可以有 0~4 面墙。
注意:墙体厚度忽略不计。
代码实现:
/*
传统Flood Fill算法,没什么好说的
要注意的是墙是由数字来描述的,总共四面墙,二进制1111. 要注意处理判断条件。
*/
#include <iostream>
#include <algorithm>
#define S second
#define F first
#define endl "\n"
using namespace std;
typedef pair<int, int> PII;
const int N = 55, M = N * N;
int n, m;
int mp[N][N];
bool vis[N][N];
PII q[M];
int bfs(int sx, int sy)
{
int hh = 0, tt = 0;
q[0] = {sx, sy};
vis[sx][sy] = true;
int dx[4] = {0, -1, 0, 1}, dy[4] = {-1, 0, 1, 0};
int area = 0;
while(hh <= tt) {
PII t = q[hh ++];
area ++;
int tx = t.F, ty = t.S;
//该循环是判断四个方向是否能走的
//判断是否有墙的基础还是基于 {tx, ty}
for(int i = 0; i < 4; i++) {
int x = tx + dx[i], y = ty + dy[i];
if(x < 0 || x >= n || y < 0 || y >= m) continue;
if(vis[x][y] || (mp[tx][ty] >> i & 1)) continue;
q[++tt] = {x, y};
vis[x][y] = true;
}
}
return area;
}
int main()
{
cin >> n >> m;
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
cin >> mp[i][j];
int cnt = 0, area = 0;
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
if(!vis[i][j]) {
area = max(area, bfs(i, j));
++cnt;
}
cout << cnt << endl << area << endl;
return 0;
}
山峰与山谷
题目描述:
FGD小朋友特别喜欢爬山,在爬山的时候他就在研究山峰和山谷。
为了能够对旅程有一个安排,他想知道山峰和山谷的数量。
给定一个地图,为FGD想要旅行的区域,地图被分为 n × n n×n n×n 的网格,每个格子 ( i , j ) (i,j) (i,j) 的高度 w ( i , j ) w(i,j) w(i,j) 是给定的。
若两个格子有公共顶点,那么它们就是相邻的格子,如与 ( i , j ) (i,j) (i,j) 相邻的格子有 ( i − 1 , j − 1 ) , ( i − 1 , j ) , ( i − 1 , j + 1 ) , ( i , j − 1 ) , ( i , j + 1 ) , ( i + 1 , j − 1 ) , ( i + 1 , j ) , ( i + 1 , j + 1 ) (i−1,j−1),(i−1,j),(i−1,j+1),(i,j−1),(i,j+1),(i+1,j−1),(i+1,j),(i+1,j+1) (i−1,j−1),(i−1,j),(i−1,j+1),(i,j−1),(i,j+1),(i+1,j−1),(i+1,j),(i+1,j+1)
我们定义一个格子的集合 S S S 为山峰(山谷)当且仅当:
- S S S 的所有格子都有相同的高度。
- S S S 的所有格子都连通。
- 对于 s s s 属于 S S S,与 s s s 相邻的 s ′ s′ s′ 不属于 S S S,都有 w s > w s ′ ws>ws′ ws>ws′(山峰),或者 w s < w s ′ ws<ws′ ws<ws′(山谷)。
- 如果周围不存在相邻区域,则同时将其视为山峰和山谷。
你的任务是,对于给定的地图,求出山峰和山谷的数量,如果所有格子都有相同的高度,那么整个地图即是山峰,又是山谷。
代码实现:
#include <cstdio>
#include <algorithm>
#define sc scanf
#define pf printf
#define F first
#define S second
using namespace std;
typedef pair<int, int> PII;
const int N = 1010, M = N * N;
int n;
int h[N][N];
bool vis[N][N];
PII q[M];
void bfs(int sx, int sy, bool &has_higher, bool &has_lower)
{
int hh = 0, tt = 0;
q[0] = {sx, sy};
vis[sx][sy] = true;
while(hh <= tt) {
PII t = q[hh ++];
int tx = t.F, ty = t.S;
for(int i = -1; i <= 1; i++)
for(int j = -1; j <= 1; j++) {
int x = tx + i, y = ty + j;
if(x < 0 || x >= n || y < 0 || y >= n) continue;
if(h[x][y] != h[tx][ty]) {
if(h[x][y] > h[tx][ty]) has_higher = true;
else has_lower = true;
continue;
}
if(vis[x][y]) continue;
q[++tt] = {x, y};
vis[x][y] = true;
}
}
}
int main()
{
sc("%d", &n);
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
sc("%d", &h[i][j]);
int peak = 0, valley = 0;
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
if(!vis[i][j]) {
bool has_higher = false, has_lower = false;
bfs(i, j, has_higher, has_lower);
if(!has_higher) peak++;
if(!has_lower) valley++;
}
pf("%d %d\n", peak, valley);
return 0;
}
最短路模型
普通广搜主要能够解决的是最短路模型,是权值都相同的模型。
例题
迷宫问题
代码实现:
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#define sc scanf
#define pf printf
#define S second
#define F first
using namespace std;
typedef pair<int, int> PII;
const int N = 1010;
int n;
int g[N][N];
PII q[N * N];
bool vis[N][N];
PII pre[N][N];
void bfs(int sx, int sy)
{
memset(pre, -1, sizeof pre);
int hh = 0, tt = 0;
q[0] = {sx, sy};
pre[sx][sy] = {0, 0};
int dx[4] = {0, -1, 0, 1}, dy[4] = {1, 0, -1, 0};
while(hh <= tt) {
PII t = q[hh ++];
int tx = t.F, ty = t.S;
for(int i = 0; i < 4; i++) {
int x = tx + dx[i], y = ty + dy[i];
if(x < 0 || x >= n || y < 0 || y >= n || pre[x][y].S > -1) continue;
if(g[x][y] == 1) continue;
pre[x][y] = t;
q[++ tt] = {x, y};
}
}
}
int main()
{
sc("%d", &n);
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
sc("%d", &g[i][j]);
bfs(n - 1, n - 1);
PII end(0, 0);
while(true) {
pf("%d %d\n", end.F, end.S);
if(end.F == n - 1 && end.S == n - 1) break;
end = pre[end.F][end.S];
}
return 0;
}
武士风度的牛
题目描述:
农民 John 有很多牛,他想交易其中一头被 Don 称为 The Knight 的牛。
这头牛有一个独一无二的超能力,在农场里像 Knight 一样地跳(就是我们熟悉的象棋中马的走法)。
虽然这头神奇的牛不能跳到树上和石头上,但是它可以在牧场上随意跳,我们把牧场用一个 x , y x,y x,y 的坐标图来表示。
这头神奇的牛像其它牛一样喜欢吃草,给你一张地图,上面标注了 The Knight 的开始位置,树、灌木、石头以及其它障碍的位置,除此之外还有一捆草。
现在你的任务是,确定 The Knight 要想吃到草,至少需要跳多少次。
The Knight 的位置用 K
来标记,障碍的位置用 *
来标记,草的位置用 H
来标记。
这里有一个地图的例子:
11 | . . . . . . . . . .
10 | . . . . * . . . . .
9 | . . . . . . . . . .
8 | . . . * . * . . . .
7 | . . . . . . . * . .
6 | . . * . . * . . . H
5 | * . . . . . . . . .
4 | . . . * . . . * . .
3 | . K . . . . . . . .
2 | . . . * . . . . . *
1 | . . * . . . . * . .
0 ----------------------
1
0 1 2 3 4 5 6 7 8 9 0
The Knight 可以按照下图中的 A,B,C,D…A,B,C,D… 这条路径用 55 次跳到草的地方(有可能其它路线的长度也是 55):
11 | . . . . . . . . . .
10 | . . . . * . . . . .
9 | . . . . . . . . . .
8 | . . . * . * . . . .
7 | . . . . . . . * . .
6 | . . * . . * . . . F<
5 | * . B . . . . . . .
4 | . . . * C . . * E .
3 | .>A . . . . D . . .
2 | . . . * . . . . . *
1 | . . * . . . . * . .
0 ----------------------
1
0 1 2 3 4 5 6 7 8 9 0
代码实现:
#include <bits/stdc++.h>
#define sc scanf
#define pf printf
#define F first
#define S second
using namespace std;
typedef pair<int, int> PII;
const int N = 200, M = N * N;
int n, m;
char g[N][N];
int dis[N][N];
PII q[M];
void bfs(int sx, int sy)
{
memset(dis, -1, sizeof dis);
int hh = 0, tt = 0;
q[0] = {sx, sy};
dis[sx][sy] = 0;
int dx[8] = {1, 1, 2, 2, -1, -1, -2, -2};
int dy[8] = {2, -2, 1, -1, 2, -2, -1, 1};
while(hh <= tt) {
PII t = q[hh ++];
int tx = t.F, ty = t.S;
for(int i = 0; i < 8; i++) {
int x = tx + dx[i], y = ty + dy[i];
if(x < 0 || x >= n || y < 0 || y >= m) continue;
if(g[x][y] == '*' || dis[x][y] > -1) continue;
dis[x][y] = dis[tx][ty] + 1;
if(g[x][y] == 'H') {
pf("%d\n", dis[x][y]);
return ;
}
q[++ tt] = {x, y};
}
}
}
int main()
{
sc("%d %d", &m, &n);
for(int i = 0; i < n; i++) sc("%s", &g[i]);
int sx = -1, sy = -1;
for(int i = 0; i < n; i++) {
for(int j = 0; j < m; j++)
if(g[i][j] == 'K') {
sx = i, sy = j;
break;
}
if(sx != -1) break;
}
bfs(sx, sy);
return 0;
}
抓住那头牛
题目描述:
农夫知道一头牛的位置,想要抓住它。
农夫和牛都位于数轴上,农夫起始位于点 N N N,牛位于点 K K K。
农夫有两种移动方式:
- 从 X X X 移动到 X − 1 X−1 X−1 或 X + 1 X+1 X+1,每次移动花费一分钟
- 从 X X X 移动到 2 ∗ X 2∗X 2∗X,每次移动花费一分钟
假设牛没有意识到农夫的行动,站在原地不动。
农夫最少要花多少时间才能抓住牛?
注意:不能让农夫一直无限延伸,观察题目显然最多只能到 2*N
代码实现:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 2e5;
int n, k;
int q[N];
int dist[N];
int bfs()
{
memset(dist, -1, sizeof dist);
int hh = 0, tt = 0;
dist[n] = 0;
q[0] = n;
while(hh <= tt) {
int t = q[hh ++];
if(t == k) return dist[t];
if(t + 1 <= N && dist[t + 1] == -1) {
dist[t + 1] = dist[t] + 1;
q[++ tt] = t + 1;
}
if(t - 1 >= 0 && dist[t - 1] == -1) {
dist[t - 1] = dist[t] + 1;
q[++ tt] = t - 1;
}
if(t * 2 <= N && dist[t * 2] == -1) {
dist[t * 2] = dist[t] + 1;
q[++ tt] = t * 2;
}
}
return -1;
}
int main()
{
cin >> n >> k;
cout << bfs() << endl;
return 0;
}
最小步数模型
广搜通常搜索的是在同一个平面上的最短路,平面的状态属性通常不会被改变。
然而,在最小步数模型中,平面的状态会随着搜索决策的不同而进行相应的改变,并且要求的是平面从某个初始状态到达最终状态的最小改变步数。
显然,此时的平面状态就可以被当作一种搜索转移时的节点。
该模型的平面状态(节点),通常可以用哈希处理成一个整数来作为临时节点,这样就可以进行节点的存储和变形转换了,相应的搜索转移也就好操作的多。(在c++11中,我们通常可以用 u n o r d e r e d _ m a p unordered\_map unordered_map 进行相应的哈希处理)(有时候康托展开也是可以用的)。
例题
魔板
/*
对于每一个平面状态,我们可以用unordered_map进行哈希存储。
三个操作的相应序列也是可以推导出来的,实在不行就直接模拟。
*/
#include <iostream>
#include <algorithm>
#include <cstring>
#include <unordered_map>
#include <queue>
using namespace std;
typedef pair<char, string> PCS;
queue<string> q;
unordered_map<string, int> dis; //维护最小步数
unordered_map<string, PCS> pre; //维护前驱操作和节点(状态)
string move0(string state)
{
reverse(state.begin(), state.end());
return state;
}
string move1(string state)
{
string res;
res = state[3];
for(int i = 0; i < 3; i++) res += state[i];
for(int i = 5; i < 8; i++) res += state[i];
res += state[4];
return res;
}
string move2(string state)
{
swap(state[2], state[6]);
swap(state[1], state[2]);
swap(state[5], state[6]);
return state;
}
void bfs(string start, string end)
{
if(start == end) return ;
q.push(start);
dis[start] = 0;
bool flag = false;
while(q.size() && !flag) {
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 moved = m[i];
if(dis.count(moved) == 0) {
dis[moved] = dis[t] + 1;
pre[moved] = {char(i + 'A'), t};
if(moved == end) {
flag = true;
break;
}
q.push(moved);
}
}
}
}
int main()
{
string st, ed;
for(int i = 0; i < 8; i++) {
int x; cin >> x;
ed += (x + '0');
}
for(int i = 0; i < 8; i++) st += (i + '1');
bfs(st, ed);
cout << dis[ed] << endl;
if(dis[ed]) {
string res;
while(ed != st) {
res += pre[ed].first;
ed = pre[ed].second;
}
reverse(res.begin(), res.end());
cout << res << endl;
}
return 0;
}
广搜变形
双端队列BFS
该类型的问题,主要可以解决边权只为 0 0 0 和 1 1 1 的最短路问题。(特殊dijkstra算法)
主要操作为:
- 当边权为 0 0 0 时,将节点插入队头
- 当边权为 1 1 1 时,将节点插入队尾
上述两个操作,能够保证队列的两段性和单调性,可自行证明。
例题
电路维修 (CH2601)
题目描述:
达达是来自异世界的魔女,她在漫无目的地四处漂流的时候,遇到了善良的少女翰翰,从而被收留在地球上。
翰翰的家里有一辆飞行车。
有一天飞行车的电路板突然出现了故障,导致无法启动。
电路板的整体结构是一个 R R R 行 C C C 列的网格( R , C ≤ 500 R R,C≤500R R,C≤500R, C ≤ 500 C≤500 C≤500),如下图所示。
每个格点都是电线的接点,每个格子都包含一个电子元件。
电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。
在旋转之后,它就可以连接另一条对角线的两个接点。
电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。
达达发现因为某些元件的方向不小心发生了改变,电路板可能处于断路的状态。
她准备通过计算,旋转最少数量的元件,使电源与发动装置通过若干条短缆相连。
不过,电路的规模实在是太大了,达达并不擅长编程,希望你能够帮她解决这个问题。
注意:只能走斜向的线段,水平和竖直线段不能走。
思路:
首先,这题显然有一个很强的性质:对于任意 i + j i+j i+j 为偶数的点,都是断路(即走不到)
所以,对于整个图,我们只需要考虑一半的点就行了;而对于每一个点 ( i , j ) (i,j) (i,j),我们只要考虑该点的左上、右上、左下和右下四个点,即 ( i − 1 , j − 1 ) 、 ( i − 1 , j + 1 ) 、 ( i + 1 , j − 1 ) 、 ( i + 1 , j + 1 ) (i-1,j-1)、(i-1,j+1)、(i+1,j-1)、(i+1,j+1) (i−1,j−1)、(i−1,j+1)、(i+1,j−1)、(i+1,j+1)四个点。
由于题目只给出了对角线,此时我们还需要考虑每个点对应到上述四个点的各自的线路情况,每一条线对应到字符的坐标上,分别为: ( i − 1 , j − 1 ) 、 ( i − 1 , j ) 、 ( i , j ) 、 ( i , j − 1 ) (i-1,j-1)、(i-1,j)、(i,j)、(i,j-1) (i−1,j−1)、(i−1,j)、(i,j)、(i,j−1)。
最后考虑行进时的两种情况:
- 该点可以通过原路线到达,即 g [ g x ] [ g y ] = c s [ i ] g[gx][gy]=cs[i] g[gx][gy]=cs[i]
- 该点不能通过原路线到达,即 g [ g x ] [ g y ] ≠ c s [ i ] g[gx][gy]\neq cs[i] g[gx][gy]=cs[i]
我们可以将图抽象成:每个坐标点为图中的节点,两点之间如果符合 1 1 1 则权值为 0 0 0,否则权值为 1 1 1。权值 1 1 1 表示的是 “需要通过旋转路线到达”。
于是整个问题就抽象成了双端队列 BFS模型。
时间复杂度为:O(R * C)
注意:由于每个状态有可能再次入队,所以需要进行标记
代码实现:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <deque>
using namespace std;
typedef pair<int, int> PII;
const int N = 510;
int n, m;
char g[N][N];
int dis[N][N];
bool st[N][N];
int bfs()
{
memset(st, false, sizeof st);
memset(dis, 0x3f, sizeof dis);
deque<PII> q;
q.push_front({0, 0});
dis[0][0] = 0;
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};
while(q.size()) {
PII t = q.front(); q.pop_front();
int tx = t.first, ty = t.second;
if(tx == n && ty == m) return dis[tx][ty];
if(st[tx][ty]) continue;
st[tx][ty] = true;
for(int i = 0; i < 4; i++) {
int x = tx + dx[i], y = ty + dy[i];
if(x < 0 || x > n || y < 0 || y > m) continue;
int gx = tx + ix[i], gy = ty + iy[i];
int w = (g[gx][gy] != cs[i]);
int d = dis[tx][ty] + w;
if(d <= dis[x][y]) {
dis[x][y] = d;
if(!w) q.push_front({x, y});
else q.push_back({x, y});
}
}
}
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;
}
优先队列BFS
例题
Full Tank (POJ3635)
双向BFS
例题
Nightmare II (HDOJ3085)
字串变换
题目描述
已知有两个字串 AA, BB 及一组字串变换的规则(至多 66 个规则):
A 1 → B 1 A1→B1 A1→B1
A 2 → B 2 A2→B2 A2→B2
… … …
规则的含义为:在 A A A 中的子串 A 1 A1 A1 可以变换为 B 1 B1 B1、 A 2 A2 A2 可以变换为 B 2 … B2… B2…。
例如:AA=abcd
BB=xyz
变换规则为:
abc
→
→
→ xu
ud
→
→
→ y
y
→
→
→ yz
则此时,AA 可以经过一系列的变换变为 BB,其变换的过程为:
abcd
→
→
→ xud
→
→
→ xy
→
→
→ xyz
共进行了三次变换,使得 A A A 变换为 B B B。
输入格式
输入格式如下:
A
A
A
B
B
B
A
1
A1
A1
B
1
B1
B1
A
2
A2
A2
B
2
B2
B2
…
…
… …
……
第一行是两个给定的字符串 A A A 和 B B B。
接下来若干行,每行描述一组字串变换的规则。
所有字符串长度的上限为 20 20 20。
输出格式
若在
10
10
10 步(包含
10
10
10 步)以内能将
A
A
A 变换为
B
B
B ,则输出最少的变换步数;否则输出 NO ANSWER!
。
输入样例:
abcd xyz
abc xu
ud y
y yz
输出样例:
3
代码实现:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <unordered_map>
#include <queue>
using namespace std;
int n;
string a[10], b[10];
int extend(queue<string>& q, unordered_map<string, int>& da, unordered_map<string, int>& db, string a[], string b[])
{
string t = q.front(); q.pop();
for(int i = 0; i < (int)t.size(); i++)
for(int j = 0; j < n; j++)
{
int len = a[j].size();
if(t.substr(i, len) != a[j]) continue;
string next = t.substr(0, i) + b[j] + t.substr(i + len);
if(db.count(next)) return da[t] + 1 + db[next];
if(da.count(next)) continue;
da[next] = da[t] + 1;
q.push(next);
}
return 11;
}
int bfs(string A, string B)
{
queue<string> qa, qb;
unordered_map<string, int> da, db;
qa.push(A), da[A] = 0;
qb.push(B), db[B] = 0;
while(qa.size() && qb.size()) {
int res = 0;
if(qa.size() <= qb.size()) res = extend(qa, da, db, a, b);
else res = extend(qb, db, da, b, a);
if(res <= 10) return res;
}
return 11;
}
int main()
{
string A, B;
cin >> A >> B;
while(cin >> a[n] >> b[n]) n++;
int step = bfs(A, B);
if(step > 10) cout << "NO ANSWER!" << endl;
else cout << step << endl;
return 0;
}