第三章 搜索(1):BFS


树和图的存储

  • 树是一种特殊的图,即无环连通图,其存储方式与图的存储方式相同;

  • 对于无向图而言,可以按照有向图来存储,即对无向图的边a - b,存储两条有向图的边即可a -> b; b -> a

因此仅考虑有向图的存储即可。

有两种存储方式:

邻接矩阵:适用于存储边稠密的图。
邻接表:适用于存储边稀疏的图。

// 邻接表存储
const int N = ___, M = 2 * M; // 因为邻接表的结点可能多次出现

// h存储链表头
// e存储每个结点的值
// ne存储每个结点的next指针
int h[N], e[M], ne[M], idx;

// 添加一条有向边 a -> b
void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; // 头插法
}

// 初始化
idx = 0;
memset(h, -1, sizeof h);

BFS分类

  • 最短距离:从地图上的某个点走到指定点的最小距离;
  • 最小步数:将地图看成是一种状态,从一个状态走到另一个状态的最小操作数,相当于是对地图进行操作。

BFS特点

  • 求最小值;
  • 基于迭代,不会爆栈。

代码模板

queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);

while (q.size())
{
    int t = q.front();
    q.pop();

    for (int i = h[t]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }
}

1、Flood Fill(洪水覆盖/填充算法)

可以在线性时间复杂度内,找到某个点所在的连通块。

1.1 池塘计数

ACwing 1097

连通

  • 四连通:相邻格子有公共边即连通
  • 八连通:相邻格子有公共点即连通

本题是八连通。

#include <iostream>
#include <algorithm>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;
const int N = 1010, M = N * N;

int n, m;
char g[N][N];
PII q[M];
bool st[N][N];

void bfs(int sx, int sy) {
    int hh = 0, tt = 0; q[0] = {sx, sy};
    st[sx][sy] = true;
    while (hh <= tt) {
        PII t = q[hh++];
        for (int i = t.x - 1; i <= t.x + 1; i++)
            for (int j = t.y - 1; j <= t.y + 1; j++) {
                if (i == t.x && j == t.y) continue;
                if (i < 0 || i >= n || j < 0 || j >= m) continue;
                if (g[i][j] == '.' || st[i][j]) continue;
                q[++tt] = {i, j};
                st[i][j] = true;
            }
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) scanf("%s", g[i]);
    int res = 0;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            if (g[i][j] == 'W' && !st[i][j]) {
                bfs(i, j); res++;
            }
    printf("%d\n", res);
    return 0;
}

DFS版本:

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
char g[N][N];
bool st[N][N];

void dfs(int x, int y) {
    st[x][y] = true;
    for (int i = -1; i <= 1; i ++ )
        for (int j = -1; j <= 1; j ++ ) {
            int a = x + i, b = y + j;
            if (a < 0 || a >= n || b < 0 || b >= m) continue;
            if (g[a][b] == '.' || st[a][b]) continue;
            dfs(a, b);
        }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i ++ ) scanf("%s", g[i]);
    int res = 0;
    for (int i = 0; i < n; i ++ ) 
        for (int j = 0; j < m; j ++ )
            if (g[i][j] == 'W' && !st[i][j]) {
                dfs(i, j);
                res ++ ;
            }
    printf("%d\n", res);
    return 0;
}

1.2 城堡问题

ACwing 1098

在这里插入图片描述

注意这个题的数据输入,相当于用一个 4 4 4 位二进制数来表示一个小方格四周 w a l l wall wall 的数量。比如: 11 = 8 + 2 + 1 11 = 8 + 2 + 1 11=8+2+1,所以值为 11 11 11 的小方格周围有西墙、北墙和南墙。

代码中,实现判断该处是否是墙的代码:g[t.x][t.y] >> i & 1,如果该值为 1 1 1,则该处为墙。

可见,这个题是四连通。

#include <iostream>
#include <algorithm>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;
const int N = 55, M = N * N;

int n, m;
int g[N][N];
PII q[M];
bool st[N][N];

int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0}; // 方向按照 西北东南 顺序设置

int bfs(int sx, int sy) {
    int area = 0;
    int hh = 0, tt = 0; q[0] = {sx, sy};
    st[sx][sy] = true;
    while (hh <= tt) {
        auto t = q[hh++];
        area++;
        for (int i = 0; i < 4; i++) {
            int a = t.x + dx[i], b = t.y + dy[i];
            if (a < 0 || a >= n || b < 0 || b >= m) continue;
            if (st[a][b] || g[t.x][t.y] >> i & 1) continue;
            q[++tt] = {a, b};
            st[a][b] = true;
        }
    }
    return area;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            scanf("%d", &g[i][j]);
    int cnt = 0, area = 0;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            if (!st[i][j]) area = max(area, bfs(i, j)), cnt++;
    printf("%d\n%d\n", cnt, area);
    return 0;
}

1.3 山峰和山谷

ACwing 1106

  • 山峰:周围格子都比它小
  • 山谷:周围格子都比它大
  • BFS找连通块:它与周围格子一样大,有可能是山峰,有肯能是山谷,也可能啥也不是,需要判断它与相邻的格子的大小关系

这个题是八连通。

对一段代码的解释:

// if (st[i][j]) continue; 
if (h[i][j] != h[t.x][t.y]) {
    if (h[i][j] > h[t.x][t.y]) has_higher = true;
    else has_lower = true;
} else if (!st[i][j]) 
    q[++tt] = {i, j}, st[i][j] = true;

每一次都是遍历一个数值相等的连通块,如果数值相等就不需要遍历。如果将条件判断 if (st[i][j]) continue 放在前面,这时即便 h[i][j] != h[t.x][t.y],也不需要遍历,但是会错失依次判断山峰和山谷的机会。

#include <iostream>
#include <algorithm>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;
const int N = 1010, M = N * N;

int n;
int h[N][N];
PII q[M];
bool st[N][N];

void bfs(int sx, int sy, bool &has_higher, bool &has_lower) {
    int hh = 0, tt = 0; q[0] = {sx, sy};
    st[sx][sy] = true;
    while (hh <= tt) {
        auto t = q[hh++];
        for (int i = t.x - 1; i <= t.x + 1; i++)
            for (int j = t.y - 1; j <= t.y + 1; j++) {
                if (i == t.x && j == t.y) continue;
                if (i < 0 || i >= n || j < 0 || j >= n) continue;
                if (h[i][j] != h[t.x][t.y]) { // 山脉的边界
                    if (h[i][j] > h[t.x][t.y]) has_higher = true;
                    else has_lower = true;
                } else if (!st[i][j]) {
                    q[++tt] = {i, j};
                    st[i][j] = true;
                }
            }
    }
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            scanf("%d", &h[i][j]);
    int peak = 0, valley = 0;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            if (!st[i][j]) {
                bool has_higher = false, has_lower = false;
                bfs(i, j, has_higher, has_lower);
                if (!has_higher) peak++;
                if (!has_lower) valley++;
            }
    printf("%d %d\n", peak, valley);
    return 0;
}

2、最短路模型(所有边权值相同的图)

下面有两种 B F S BFS BFS 代码,一种是对数组 d i s t dist dist 初始化为 0 x 3 f 0x3f 0x3f,另一种是初始化为 − 1 -1 1,这两种代码在后续的判断中略有不同。第二种代码更像是一个 b o o l bool bool 数组,记录其点是否走过并且同时记录其到起点的最短距离。这两种代码都是可以的,因为 B F S BFS BFS 是在第一次遍历到某点的时候,这个时候的距离就是最短距离,即整个遍历过程仅入队一次,后续就不需要对其进行更新。

2.1 走迷宫

ACWing 844

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;
const int N = 110, M = N * N;

int n, m;
int g[N][N];
PII q[M];
int dist[N][N];
int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0};

int bfs() {
    memset(dist, 0x3f, sizeof dist); dist[0][0] = 0;
    int hh = 0, tt = 0; q[0] = {0, 0};
    while (hh <= tt) {
        auto t = q[hh++];
        for (int i = 0; i < 4; i++) {
            int a = t.x + dx[i], b = t.y + dy[i];
            if (a < 0 || a >= n || b < 0 || b >= m) continue;
            if (g[a][b]) continue;
            if (dist[a][b] > dist[t.x][t.y] + 1) {
                dist[a][b] = dist[t.x][t.y] + 1;
                q[++tt] = {a, b};
            }
        }
    }
    return dist[n - 1][m - 1];
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            scanf("%d", &g[i][j]);
    printf("%d\n", bfs());
    return 0;
}

2.1.1 游戏

ACwing 3230

这个题目和 ACwing 3200 无线网络 有些类似的地方。

代码中 M = 310 M = 310 M=310 的由来:由题目中可知 0 ≤ a , b ≤ 100 0 \le a, b \le 100 0a,b100,因此当时间 t ≥ 100 t \ge 100 t100 的时候,所有格子都可以走,从左上角到右下角最多走 200 200 200 步,因此 M M M 最多为 300 300 300

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 110, M = 310;

int n, m, T;
bool g[N][N][M]; // 表示格子(x,y)在t时刻能否走
int dist[N][N][M]; // 从起点到(x,y)的最短时间为m
struct Node {
    int x, y, t;
} q[N * N * M];
int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0};

inline int bfs() {
    memset(dist, 0x3f, sizeof dist);
    int tt = 0, hh = 0; q[0] = {1, 1, 0};
    while (hh <= tt) {
        Node t = q[hh++];
        if (t.x == n && t.y == m) return dist[t.x][t.y][t.t];
        for (int i = 0; i < 4; i++) {
            int a = t.x + dx[i], b = t.y + dy[i];
            if (a <= 0 || a > n || b <= 0 || b > m) continue;
            if (g[a][b][t.t + 1]) continue;
            if (dist[a][b][t.t + 1] > t.t + 1) {
                dist[a][b][t.t + 1] = t.t + 1;
                q[++tt] = {a, b, t.t + 1};
            }
        }
    }
    return 0;
}

int main() {
    scanf("%d%d%d", &n, &m, &T);
    while (T--) {
        int r, c, a, b; scanf("%d%d%d%d", &r, &c, &a, &b);
        for (int i = a; i <= b; i++) g[r][c][i] = true;
    }
    printf("%d\n", bfs());
    return 0;
}

2.2 迷宫问题 (记录方案)

ACwing 1076

注意代码中 pre[sx][sy] = {0, 0},这里起点 (sx, sy)(n-1, n-1) 的上一个状态是可以任意填写,只要不为 -1 即可,因为在最后输出的时候并不会遍历到。

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;
const int N = 1010, M = N * N;

int n;
int g[N][N];
PII q[M];
PII pre[N][N]; // 以前的st变成了pre,记录方案
int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0};

void bfs(int sx, int sy) {
    memset(pre, -1, sizeof pre); pre[sx][sy] = {0, 0}; 
    int hh = 0, tt = 0; q[0] = {sx, sy};
    while (hh <= tt) {
        auto t = q[hh++];
        for (int i = 0; i < 4; i++) {
            int a = t.x + dx[i], b = t.y + dy[i];
            if (a < 0 || a >= n || b < 0 || b >= n) continue;
            if (g[a][b] || pre[a][b].x != -1) continue;
            pre[a][b] = t;
            q[++tt] = {a, b};
        }
    }
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            scanf("%d", &g[i][j]);
    bfs(n - 1, n - 1);
    PII end(0, 0);
    while (true) {
        printf("%d %d\n", end.x, end.y);
        if (end.x == n - 1 && end.y == n - 1) break;
        end = pre[end.x][end.y];
    }
    return 0;
}

2.3 图中点的层次

ACWing 847

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], e[N], ne[N], idx;
int dist[N], q[N];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

int bfs() {
    memset(dist, -1, sizeof dist);
    dist[1] = 0;
    int hh = 0, tt = 0;
    q[0] = 1;
    while (hh <= tt) {
        int t = q[hh ++ ];
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] != -1) continue;
            dist[j] = dist[t] + 1;
            q[ ++ tt] = j;
        }
    }
    return dist[n];
}

int main() {
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i ++ ) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }
    printf("%d\n", bfs());
    return 0;
}

2.4 武士风度的牛

ACwing 188

注意本题先输入的是列,后输入的是行!

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;
const int N = 155, M = N * N;

int n, m;
char g[N][N];
PII q[M];
int dist[N][N];
int dx[] = {-2, -2, -1, 1, 2, 2, 1, -1};
int dy[] = {-1, 1, 2, 2, 1, -1, -2, -2};

int bfs(int sx, int sy) {
    memset(dist, -1, sizeof dist); dist[sx][sy] = 0;
    int hh = 0, tt = 0; q[0] = {sx, sy};
    while (hh <= tt) {
        auto t = q[hh++];
        for (int i = 0; i < 8; i++) {
            int a = t.x + dx[i], b = t.y + dy[i];
            if (a < 0 || a >= n || b < 0 || b >= m) continue;
            if (g[a][b] == '*' || dist[a][b] != -1) continue;
            if (g[a][b] == 'H') return dist[t.x][t.y] + 1;
            dist[a][b] = dist[t.x][t.y] + 1;
            q[++tt] = {a, b};
        }
    }
    return -1;
}

int main() {
    scanf("%d%d", &m, &n);
    for (int i = 0; i < n; i++) scanf("%s", g[i]);
    int sx = 0, sy = 0;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            if (g[i][j] == 'K') sx = i, sy = j;
    printf("%d\n", bfs(sx, sy));
    return 0;
}

2.5 抓住那头牛

ACwing 1100

首先,农夫不可能走到 ≤ 0 \le 0 0 的点。如果农夫走到了 ≤ 0 \le 0 0 的点,而题目中 k ≥ 0 k\ge0 k0,那么农夫要想抓到牛,就必须走回 ≥ 0 \ge0 0 的点上。从 ≥ 0 \ge0 0 的点走到 ≤ 0 \le0 0 的点只有通过 x − 1 x-1 x1,而从 ≤ 0 \le0 0 的点走到 ≥ 0 \ge0 0 的点只有通过 x + 1 x+1 x+1,这样一去一来又回到了原点,本题求的是最短路径,所以这样走等于农夫白走了一段路,故农夫走到的点都是 ≥ 0 \ge0 0 的。

这里有个细节,就是要确定 N N N 的取值范围。考虑当 n > k n > k n>k 的时候,这个时候就只能通过 x − 1 x-1 x1 来抓到牛;可是当 n < k n < k n<k 的时候,一个极端的情况是 n = k − 1 n = k-1 n=k1,这个时候如果使用 2 × x 2 \times x 2×x,那么 N N N 的取值范围最大就要开到 2 e 5 2e5 2e5。但是代码中使用的是 1 e 5 1e5 1e5,是因为上面这种情况考虑的是先乘后减,其实也可以考虑先减后乘,同样可以抓到牛,这个时候 N N N 的范围就不用开到 2 e 5 2e5 2e5了。

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 1e5 + 10;

int n, k;
int q[N], dist[N];

int bfs() {
    memset(dist, -1, sizeof dist); dist[n] = 0;
    int hh = 0, tt = 0; q[0] = n;
    while (hh <= tt) {
        auto t = q[hh++];
        if (t == k) return dist[k];
        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() {
    scanf("%d%d", &n, &k);
    printf("%d\n", bfs());
    return 0;
}

2.7 地铁修建(BFS + 二分)

ACwing 3245

从点 1 1 1 到点 n n n m m m 条边可以选择,现在要求做多选择 n n n 条边,使得从 1 1 1 n n n 连通且边上的最大权值最小

注意审题,因为每家公司的施工时间一样,所以如果选择的 k k k 条线路同时施工,那么整条线路的施工时间就取决于施工时间最长的一条线路。

如果每一次都只选择边权小于等于 m i d mid mid 的边,那么这个图中所有边权小于等于 m i d mid mid 的边的边权可以看做 1 1 1,大于 m i d mid mid 的边的边权看做 0 0 0,因此可以使用 b f s bfs bfs 即可。

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 1e5 + 10, M = 4e5 + 10;

int n, m;
int h[N], e[M], w[M], ne[M], idx;
int q[N], dist[N];

inline void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

inline int bfs(int mid) {
    memset(dist, 0x3f, sizeof dist); dist[1] = 0;
    int hh = 0, tt = 0; q[0] = 1;
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            if (w[i] > mid) continue;
            int j = e[i];
            if (dist[j] > dist[t] + 1)
                dist[j] = dist[t] + 1, q[++tt] = j;
        }
    }
    return dist[n];
}

int main() {
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    while (m--) {
        int a, b, c; scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    int l = 0, r = 1e6;
    while (l < r) {
        int mid = (l + r) >> 1;
        if (bfs(mid) <= n) r = mid;
        else l = mid + 1;
    }
    printf("%d\n", r);
    return 0;
}

3、多源BFS

3.1 矩阵距离

ACwing 173

题意:求解题目中每个值为 1 1 1 的格子到其余所有格子为的最短距离。

将其转换成单源最短路径问题。可以建立一个虚拟起点,其到所有真实起点的距离是 0 0 0

在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;
const int N = 1010, M = N * N;

int n, m;
char g[N][N]; // 输入没有空格
PII q[M];
int dist[N][N];
int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0};

void bfs() {
    memset(dist, -1, sizeof dist);
    int hh = 0, tt = -1;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; 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 < 0 || a >= n || b < 0 || 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 = 0; i < n; i ++ ) scanf("%s", g[i]);
    bfs();
    for (int i = 0; i < n; i ++ ) {
        for (int j = 0; j < m; j ++ )
            printf("%d ", dist[i][j]);
        puts("");
    }
    return 0;
}

4、最小步数模型

4.1 八数码

ACWing 845

算法思想:

将网格的每一个状态看成一个结点,求出从最初状态到规定状态的最小步数即可。

难点:

  • 状态表达,即如何存入队列中?使用9个字符的字符串来表示一个状态,队列使用queue<string>来表示,距离使用unordered map<string, int> dist来表示。
  • 如何计算状态之间的距离(状态转移)?先恢复成原来的3x3矩阵,然后利用bfs进行变换,最后将3x3矩阵转化成字符串。

注:

  • 找到k在3x3矩阵中的位置:int x = k / 3, y = k % 3;
  • 从矩阵位置恢复k在原数组中的位置:k = a * 3 + b
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
#include <unordered_map>
using namespace std;

string start;
queue<string> q;
unordered_map<string, int> dist;
int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0};

int bfs() {
    string end = "12345678x";
    if (start == end) return 0;
    dist[start] = 0;
    q.push(start);
    while (q.size()) {
        auto t = q.front(); q.pop();
        if (t == end) return dist[t];
        int k = t.find('x');
        int x = k / 3, y = k % 3;
        for (int i = 0; i < 4; i++) {
            int a = x + dx[i], b = y + dy[i];
            if (a < 0 || a >= 3 || b < 0 || b >= 3) continue;
            string r = t;
            swap(r[k], r[a * 3 + b]);
            if (dist.count(r)) continue;
            dist[r] = dist[t] + 1;
            q.push(r);
        }

    }
    return -1;
}

int main() {
    for (int i = 0; i < 9; i++) {
        char c[2]; scanf("%s", c);
        start += *c;
    }
    printf("%d\n", bfs());
    return 0;
}

4.2 玛雅人的密码

ACwing 3385

#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
#include <unordered_map>
using namespace std;

int n;
string start;

int bfs() {
    unordered_map<string, int> dist; dist[start] = 0;
    queue<string> q; q.push(start);
    while (q.size()) {
        auto t = q.front(); q.pop();
        for (int i = 0; i < n; i++)
            if (t.substr(i, 4) == "2012")
                return dist[t];
        for (int i = 1; i < n; i++) {
            string r = t;
            swap(r[i], r[i - 1]);
            if (!dist.count(r))
                dist[r] = dist[t] + 1, q.push(r);
        }
    }
    return -1;
}

int main() {
    cin >> n >> start;
    cout << bfs() << endl;
    return 0;
}

4.3 魔板

ACwing 1107

  • 存储状态:哈希
  • 存储方案:类比上面的迷宫问题
  • 字典序最小方案:扩展(插入队列)的时候按照 A 、 B 、 C A、B、C ABC 插入即可

代码中一点解释:

  • p r e pre pre: 每个状态从哪个状态转移过来以及它的操作是 A A A B B B C C C
  • d i s t dist dist:每个状态的步数
  • 函数 s e t set set:将字符串放回2x4矩阵
  • 函数 g e t get get:将2x4矩阵变成字符串
  • 函数 m o v e 0 move0 move0:交换两行
  • 函数 m o v e 1 move1 move1:将最后一列放到第一列
  • 函数 m o v e 2 move2 move2:顺时针旋转一圈
#include <iostream>
#include <algorithm>
#include <cstring>
#include <unordered_map>
#include <queue>
using namespace std;

#define x first
#define y second

char g[2][4];
unordered_map<string, pair<char, string>> pre;
unordered_map<string, int> dist;

inline void set(string state) {
    for (int i = 0; i < 4; i++) g[0][i] = state[i];
    for (int i = 7, j = 0; j < 4; i--, j++) g[1][j] = state[i];
}

inline 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;
}

inline string move0(string state) {
    set(state);
    for (int i = 0; i < 4; i++) swap(g[0][i], g[1][i]);
    return get();
}

inline string move1(string state) {
    set(state);
    int v0 = g[0][3], v1 = g[1][3];
    for (int i = 3; i; i--)
        g[0][i] = g[0][i - 1], g[1][i] = g[1][i - 1];
    g[0][0] = v0, g[1][0] = v1;
    return get();
}

inline string move2(string state) {
    set(state);
    int 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();
}

inline int bfs(string start, string end) {
    if (start == end) return 0;
    dist[start] = 0;
    queue<string> q; q.push(start);
    while (q.size()) {
        string t = q.front(); q.pop();
        if (t == end) return dist[t];
        string m[3];
        m[0] = move0(t), m[1] = move1(t), m[2] = move2(t);
        for (int i = 0; i < 3; i++)
            if (!dist.count(m[i])) {
                dist[m[i]] = dist[t] + 1;
                pre[m[i]] = {'A' + i, t};
                q.push(m[i]);
            }
    }
    return -1;
}

int main() {
    int x; string start, end;
    for (int i = 0; i < 8; i++) {
        scanf("%d", &x); end += char('0' + x);
    }
    for (int i = 1; i <= 8; i++) start += char('0' + i);
    int step = bfs(start, end);
    printf("%d\n", step);
    string res;
    while (end != start) res += pre[end].x, end = pre[end].y;
    reverse(res.begin(), res.end());
    if (step > 0) printf("%s\n", res.c_str());
    return 0;
}

5、双端队列广搜

5.1 电路维修

ACwing 175

题解参考

这种图中存在一些性质。

  1. 计算每个点的横纵坐标之和,因为起点横纵坐标之和是偶数,所以我们只能到达横纵坐标之和是偶数的点,不能到达横纵坐标之和是奇数的点(因为本题是按照斜线走的)。故当图中起始节点和目标节点的奇偶性不同,则无解。
  2. 我们将这种图中原本出现的斜边上的权值设为 0 0 0,即两点之间存在路径的;若要修改斜边,即两点之间没有路径,需要将斜边旋转 9 0 o 90^o 90o 才会有路径,则这种两点之间斜边上的权值设为 1 1 1。那么这张图就变成了只包含权值为 0 0 0 1 1 1 的图,它的经典解法是双端队列(本质上还是一个Dijkstra算法)

因为输入的的格子中的值,所以对应的是格子的坐标,而我们遍历的是定点,所以要找到定点与格子坐标的关系。代码中的 d x dx dx i x ix ix 数组表示:
在这里插入图片描述
上图中表示出了定点坐标与格子坐标的对应关系,比如要从定点 ( 1 , 1 ) (1,1) (1,1) 走到 ( 0 , 2 ) (0,2) (0,2),就必须要经过格子 ( 0 , 1 ) (0,1) (0,1),所以定点 ( 1 , 1 ) (1,1) (1,1) 与路过的格子 ( 0 , 1 ) (0,1) (0,1) 的对应关系即为 ( − 1 , 0 ) (-1, 0) (1,0),表示横坐标 − 1 -1 1,纵坐标不变。

代码中的几点解释:

  • 注意格点的长度要比格子长度多1,也就是长度是 n + 1 , m − 1 n+1, m - 1 n+1,m1,在做边界判断的时候要注意;
  • 双端队列广搜:边权为0加入队头,边权为1加入队尾。
#include <iostream>
#include <algorithm>
#include <cstring>
#include <deque>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;
const int N = 510;

int n, m;
char g[N][N];
int dist[N][N];
bool st[N][N];
char cs[] = "\\/\\/"; // 每个方向上的通路(注意转义字符):左上、右上、右下、左下
int dx[] = {-1, -1, 1, 1}, dy[] = {-1, 1, 1, -1};
int ix[] = {-1, -1, 0, 0}, iy[] = {-1, 0, 0, -1};

int bfs() {
    memset(dist, 0x3f, sizeof dist); dist[0][0] = 0;
    deque<PII> q; q.push_back({0, 0});
    memset(st, 0, sizeof st);
    while (q.size()) {
        PII t = q.front(); q.pop_front();
        if (st[t.x][t.y]) continue; st[t.x][t.y] = true;
        for (int i = 0; i < 4; i++) {
            int a = t.x + dx[i], b = t.y + dy[i];
            if (a < 0 || a > n || b < 0 || b > m) continue;
            int ca = t.x + ix[i], cb = t.y + iy[i];
            int d = dist[t.x][t.y] + (g[ca][cb] != cs[i]);
            if (d < dist[a][b]) {
                dist[a][b] = d;
                if (g[ca][cb] != cs[i]) q.push_back({a, b});
                else q.push_front({a, b});
            }
        }
    }
    return dist[n][m];
}

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]);
        int t = bfs();
        if (t == 0x3f3f3f3f) puts("NO SOLUTION");
        else printf("%d\n", t);
    }
    return 0;
}

6、双向广搜 (一般用于优化最小步数模型)

6.1 字串变换

这个题目没有告诉输入中具体有多少个规则。

ACwing 190

对代码中的一点解释:

  • 使用两个方向的队列。 q a qa qa:从起点开始; q b qb qb:从终点开始
  • 哈希 d a 、 d b da、db dadb:分别表示每个状态到起点和终点的距离
#include <iostream>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;

const int N = 6; // 最多6个变换规则

int n;
string A, B;
string a[N], b[N]; // 变换规则

int extend(queue<string> &q, unordered_map<string, int> &da, unordered_map<string, int> &db,
           string a[], string b[]) {
    int d = da[q.front()];
    while (q.size() && da[q.front()] == d) { // 每一次都扩展一层:即到起始节点距离都等于d的点
        auto t = q.front(); q.pop();
        for (int i = 0; i < n; i++) // 枚举扩展的规则
            for (int j = 0; j < t.size(); j++) // 枚举t从哪个字母开始扩展
                if (t.substr(j, a[i].size()) == a[i]) { // 是否匹配规则
                    string r = t.substr(0, j) + b[i] + t.substr(j + a[i].size()); // 使用规则变换
                    if (db.count(r)) return da[t] + 1 + db[r]; // 如果这个状态在另一个方向存在,则两个方向连通了
                    if (da.count(r)) continue; // 如果这个状态被扩展过了
                    da[r] = da[t] + 1;
                    q.push(r);
                }
    }
    return 11; // 返回一个比10的的数即可
}

int bfs() {
    if (A == B) return 0;
    queue<string> qa, qb; qa.push(A), qb.push(B);
    unordered_map<string, int> da, db; da[A] = db[B] = 0;
    int step = 0;
    while (qa.size() && qb.size()) { // 必须都有值,否则就两个状态会不连通
        int t;
        if (qa.size() < qb.size()) t = extend(qa, da, db, a, b); // 每次都往扩展出来规模更小的方向扩展
        else t = extend(qb, db, da, b, a);
        if (t <= 10) return t;
        if (++step == 10) return -1;
    }
    return -1;
}

int main() {
    cin >> A >> B;
    while (cin >> a[n] >> b[n]) n++;
    int t = bfs();
    if (t == -1) puts("NO ANSWER!"); else cout << t << endl;
    return 0;
}

无注释代码:

#include <iostream>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;

const int N = 10;

int n;
string A, B;
string a[N], b[N];

inline int extend(queue<string> &q, unordered_map<string, int> &da, unordered_map<string, int> &db,
                  string a[], string b[]) {
    int d = da[q.front()];
    while (q.size() && da[q.front()] == d) {
        string t = q.front(); q.pop();
        for (int i = 0; i < n; i++)
            for (int j = 0; j < t.size(); j++)
                if (t.substr(j, a[i].size()) == a[i]) {
                    string r = t.substr(0, j) + b[i] + t.substr(j + a[i].size());
                    if (db.count(r)) return da[t] + db[r] + 1;
                    if (da.count(r)) continue;
                    da[r] = da[t] + 1;
                    q.push(r);
                }
    }
    return 11;
}

inline int bfs() {
    if (A == B) return 0;
    queue<string> qa, qb; qa.push(A), qb.push(B);
    unordered_map<string, int> da, db; da[A] = db[B] = 0;
    int step = 0;
    while (qa.size() && qb.size()) {
        int t;
        if (qa.size() < qb.size()) t = extend(qa, da, db, a, b);
        else t = extend(qb, db, da, b, a);
        if (t <= 10) return t;
        if (++step == 10) return -1;
    }
    return -1;
}

int main() {
    cin >> A >> B;
    while (cin >> a[n] >> b[n]) n++;
    int t = bfs();
    if (t == -1) puts("NO ANSWER!"); else cout << t << endl;
    return 0;
}

7、A* (一般用于优化最小步数模型)

应用场景

  • 适用于会搜索大量状态的情景,在起点使用构建一个启发函数,使其能够搜索较少的状态就能搜到终点;
  • 整个图一定要保证有解,如果无解,那么与普通的 B F S BFS BFS 算法没区别,会将所有状态都遍历一遍,但是其效率低于普通的 B F S BFS BFS,因为普通的 B F S BFS BFS 算法使用的是 q u e u e queue queue,入队出队时间复杂度是 O ( 1 ) O(1) O(1)
  • 图中边权不能有负权回路。

过程
首先将 B F S BFS BFS 中的队列换成优先队列。队列中的每个状态需要存储:从起到走到当前点的真实距离 a a a从当前点到终点的估价距离 b b b。优先队列中的排序是根据 a + b a + b a+b 的和来排序的。

while (!q.empty())
{
	取优先队列队头t(小根堆)
	当终点第一次出队的时候 break;
	for t的所有邻边
		将邻边入队
}

A*算法正确性的前提
假设当期状态为 s t a t e state state,从起点到当前状态的距离为 d ( s t a t e ) d(state) d(state),从当前状态到终点的距离为 g ( s t a t e ) g(state) g(state),从当前状态到终点的估价距离是 f ( s t a t e ) f(state) f(state)。要使 A ∗ A^* A 算法正确就必须保证以下式子成立: d ( s t a t e ) + g ( s t a t e ) ≥ d ( s t a t e ) + f ( s t a t e ) d(state)+g(state) \ge d(state)+f(state) d(state)+g(state)d(state)+f(state) g ( s t a t e ) ≥ f ( s t a t e ) g(state) \ge f(state) g(state)f(state)

正确性证明:假设终点出队的时候不是最小值,设其到起点的距离距离为 d i s t dist dist,最优解的距离为 d i s t u dist_u distu,那么就一定有 d i s t > d i s t u dist > dist_u dist>distu。因为当前优先队列中一定会存在最优解中的某个点记为 u u u(比如起点一定是最优解中的点),那么从起点到 u u u 的距离加上从 u u u 到终点的估计距离 d ( u ) + f ( u ) d(u)+f(u) d(u)+f(u) ,就一定有 d ( u ) + f ( u ) ≤ d ( u ) + g ( u ) = d(u) + f(u) \le d(u) + g(u) = d(u)+f(u)d(u)+g(u)= 最优解 d i s t u dist_u distu。于是有 d i s t > d i s t u ≥ d ( u ) + f ( u ) dist > dist_u \ge d(u)+f(u) dist>distud(u)+f(u),即优先队列中存在一个点 u u u 到起点的距离 d i s t u dist_u distu 比一个出队点到起点的距离 d i s t dist dist 更小,与优先队列的性质矛盾,因此在优先队列中,终点第一次出队时的距离一定是最小的(注意,不能保证除了终点以外其它点第一次出队时是最优解上的值)

宽搜对比

  • BFS:每个状态只会入队一次,可以在入队的时候判重;
  • Dijkstra:每个状态只会出队一次,可以在出队的时候判重;
  • A*:不能判重,只有终点第一次出队才是最优解。

7.1 八数码

ACwing 179

八数码有解的充要条件
3 × 3 3\times3 3×3 矩阵中的数据读入一个数组中,这组数据逆序对的数量是偶数,则一定有解;为奇数,则无解。

估价函数
当前状态中每个数与它的目标位置的曼哈顿距离之和:当前位置和目标位置横坐标和纵坐标之差的和。

#include <iostream>
#include <algorithm>
#include <queue>
#include <unordered_map>

using namespace std;

// 求每个位置与其目标位置的曼哈顿距离
int f(string state) {
    int res = 0;
    for (int i = 0; i < state.size(); i++)
        if (state[i] != 'x') {
            int t = state[i] - '1';
            res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
        }
    return res;
}

string bfs(string start) {
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    char op[4] = {'u', 'r', 'd', 'l'};
    string end = "12345678x";
    unordered_map<string, int> dist; // 记录距离
    unordered_map<string, pair<string, char>> prev; // 前驱状态以及转移到当前状态的操作
    priority_queue<pair<int, string>, vector<pair<int, string>>, greater<pair<int, string>>> heap; // 存储估计距离,状态
    heap.push({f(start), start});
    dist[start] = 0;
    while (heap.size()) {
        auto t = heap.top();
        heap.pop();
        string state = t.second;
        if (state == end) break;
        int step = dist[state];
        int x, y;
        for (int i = 0; i < state.size(); i++) // 找到t中x的坐标
            if (state[i] == 'x') {
                x = i / 3, y = i % 3;
                break;
            }
        string source = state; // 方便恢复现场
        for (int i = 0; i < 4; i++) {
            int a = x + dx[i], b = y + dy[i];
            if (a >= 0 && a < 3 && b >= 0 && b < 3) {
                swap(state[x * 3 + y], state[a * 3 + b]);
                if (!dist.count(state) || dist[state] > step + 1) { // 未被扩展过或者当前距离比较大
                    dist[state] = step + 1;
                    prev[state] = {source, op[i]};
                    heap.push({dist[state] + f(state), state});
                }
                swap(state[x * 3 + y], state[a * 3 + b]);
            }
        }
    }
    string res; // 存储方案
    while (end != start) {
        res += prev[end].second;
        end = prev[end].first;
    }
    reverse(res.begin(), res.end());
    return res;
}

int main() {
    string g, c, seq; // seq存储输入的数字,方便求逆序对
    while (cin >> c) {
        g += c;
        if (c != "x") seq += c;
    }
    int t = 0;
    for (int i = 0; i < seq.size(); i++) // 求逆序对数量
        for (int j = i + 1; j < seq.size(); j++)
            if (seq[i] > seq[j])
                t++;
    if (t % 2) puts("unsolvable");
    else cout << bfs(g) << endl;
    return 0;
}

7.2 第K短路

ACwing 178

估价函数
每个点到终点的最小距离

第k短路求法
A ∗ A^* A算法的性质从优先队列中,终点第一次出队时的距离一定是最小的,那么终点从优先队列第二次出队的时候,就是第二小的,…,从终点优先队列第 k k k 次出队的时候,就是第 k k k 小的。

正确性证明:由之前的证明可知 d i s t > d i s t u ≥ d ( u ) + f ( u ) dist > dist_u \ge d(u)+f(u) dist>distud(u)+f(u),同理,假设第二次从优先队列中出队的值不是第二小的值, d i s t 2 dist_2 dist2 表示第二小的值, v v v 表示第二最短路上的某一个点,那么就有 d i s t 2 > d i s t 2 ≥ d ( v ) + f ( v ) dist_2 > dist_2 \ge d(v)+f(v) dist2>dist2d(v)+f(v),所以优先队列中的值 d ( v ) + f ( v ) d(v)+f(v) d(v)+f(v) 小于了出队的值 d i s t 2 dist_2 dist2,矛盾。因此第二次从优先队列中出队的值是第二小的值,对第 k k k 次出队的值同理可证:第 k k k 次优先队列中出队的值是第 k k k 小的值。

本题的估价距离使用Dijkstra逆向求解每个点到终点的距离,所以有邻接表表头rh[N]

#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;
typedef pair<int, PII> PIII;

const int N = 1010, M = 200010;

int n, m, S, T, K;
int h[N], rh[N], e[M], w[M], ne[M], idx; // rh表示方向遍历的表头
int dist[N], cnt[N];
bool st[N];

void add(int h[], int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 处理估价函数:Dijkstra倒序扩展
void dijkstra() {
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, T});
    memset(dist, 0x3f, sizeof dist);
    dist[T] = 0;
    while (heap.size()) {
        auto t = heap.top();
        heap.pop();
        int ver = t.y;
        if (st[ver]) continue; // 之前被遍历过
        st[ver] = true;
        for (int i = rh[ver]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[ver] + w[i]) {
                dist[j] = dist[ver] + w[i];
                heap.push({dist[j], j});
            }
        }
    }
}

int astar() {
    priority_queue<PIII, vector<PIII>, greater<PIII>> heap; // 起点估价值,真实值,编号
    heap.push({dist[S], {0, S}});
    while (heap.size()) {
        auto t = heap.top();
        heap.pop();
        int ver = t.y.y, distance = t.y.x;
        cnt[ver]++; //如果终点已经被访问过k次了 则此时的ver就是终点T 返回答案
        if (cnt[T] == K) return distance;
        for (int i = h[ver]; ~i; i = ne[i]) {
            int j = e[i];
             /* 
            如果走到一个中间点都cnt[j]>=K,则说明j已经出队k次了,且astar()并没有return distance,
            说明从j出发找不到第k短路(让终点出队k次),
            即继续让j入队的话依然无解,
            那么就没必要让j继续入队了
            */
            if (cnt[j] < K) heap.push({distance + w[i] + dist[j], {distance + w[i], j}});
        }
    }
    return -1;
}

int main() {
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    memset(rh, -1, sizeof rh);
    for (int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(h, a, b, c); // 建立正边
        add(rh, b, a, c); // 建立反边
    }
    scanf("%d%d%d", &S, &T, &K);
    // 起点==终点时, 则 d[S→S]=0 这种情况就要舍去, 总共第 K 大变为总共第 K+1 大 
    if (S == T) K++; 
    dijkstra(); // 先做一遍dijkstra
    printf("%d\n", astar());
    return 0;
}

8、补充

8.1 等差数列

ACwing 3402

清华研究生复试上机题

// 一个等差数列只需要知道两项就可以补全所有位置

#include <iostream>
#include <algorithm>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;
const int N = 1010, M = N * 2;

int n, m; // 行、列
int row[N], col[N]; // 行、列中元素个数
int q[M], hh, tt = -1; // 队列中0~n表示行,n~n+m-1表示列
bool st[M]; // 判重,每行、列更新一次后就全部填满,以后就不需要更新
PII ans[N * N]; // 结果
int top; // 结果数量
int g[N][N]; // 矩阵

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++) {
            scanf("%d", &g[i][j]);
            if (g[i][j]) row[i]++, col[j]++;
        }
    for (int i = 0; i < n; i++) // 枚举所有行
        if (row[i] >= 2 && row[i] < m) { // 如果该行元素个数为m,就不用更新
            q[++tt] = i;
            st[i] = true;
        }
    for (int i = 0; i < m; i++) // 枚举所有列
        if (col[i] >= 2 && col[i] < n) {
            q[++tt] = i + n;
            st[i + n] = true;
        }
    while (hh <= tt) {
        auto t = q[hh++];
        if (t < n) { // 行
            PII p[2];
            int cnt = 0;
            for (int i = 0; i < m; i++)
                if (g[t][i]) {
                    p[cnt++] = {i, g[t][i]};
                    if (cnt == 2) break;
                }
            int d = (p[1].y - p[0].y) / (p[1].x - p[0].x); // 公差
            int a = p[1].y - d * p[1].x; // 首项
            for (int i = 0; i < m; i++) // 将该行所有项填满
                if (!g[t][i]) {
                    g[t][i] = a + d * i;
                    ans[top++] = {t, i};
                    col[i]++;
                    if (col[i] >= 2 && col[i] < m && !st[i + n]) // 判断该元素所在的列是否需要更新
                        q[++tt] = i + n, st[i + n] = true;
                }
        } else { // 列
            t -= n;
            PII p[2];
            int cnt = 0;
            for (int i = 0; i < n; i++)
                if (g[i][t]) {
                    p[cnt++] = {i, g[i][t]};
                    if (cnt == 2) break;
                }
            int d = (p[1].y - p[0].y) / (p[1].x - p[0].x);
            int a = p[1].y - d * p[1].x;
            for (int i = 0; i < n; i++)
                if (!g[i][t]) {
                    g[i][t] = a + d * i;
                    ans[top++] = {i, t};
                    row[i]++;
                    if (row[i] >= 2 && row[i] < n && !st[i])
                        q[++tt] = i, st[i] = true;
                }
        }
    }
    sort(ans, ans + top);
    for (int i = 0; i < top; i++) {
        auto &p = ans[i];
        printf("%d %d %d\n", p.x + 1, p.y + 1, g[p.x][p.y]);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值