【AcWing算法提高课】2.1.2BFS中的多源BFS-双端队列BFS

零、(单源)BFS正确性的证明

首先证明BFS过程用到的队列具有以下两个性质:

  1. 两端性,即同一时刻队列中的元素只有两种数值,且这两种数值相差 1 1 1
  2. 单调性:即同一时刻队列中的元素是单调增加的

用归纳法证明:

  1. 初始时,队列中只有一个元素 0 0 0,满足上述性质;
  2. 假设某一时刻队列满足上述性质,把队头 x x x 取出进行扩展,若扩展到的点之前未被遍历过,则将其距离 x + 1 x+1 x+1 放入队尾,扩展之后,队列仍满足上述性质。

有了上述性质之后,可以证明BFS的正确性,亦有两种方法:

  1. 已知堆优化的Dijkstra算法求最短路是正确的,我们把BFS中的队列类比Dijkstra中的优先队列,显然BFS是正确的。
  2. 由于Dijkstra算法与BFS尚存在一定区别 (Dijkstra中第一次出队一定是最小值,BFS中第一次入队即是最小值),我们不借助Dijkstra算法,而是用归纳法和反证法证明:
    (1) 初始时显然;
    (2) 假设某一时刻,已经出队的点的最小距离已经确定,且队头 x x x 取出时不是最小值,那么它一定会被队列中后面的点经过某条路径后更新成更短的距离,由于出队的点最小距离已经被确定,故路径上一定不会经过已经出队的点,由队列的单调性和边权均为 1 1 1 可推出该路径的距离一定大于 x x x ,矛盾。

一、矩阵距离

173.矩阵距离 题目链接

本题需要求出任意一点到离它曼哈顿距离最小的 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;
}

二、魔板

1107.魔板 题目链接

本题与八数码类似,把棋盘(矩阵)看成一种状态,均属于“最小步数模型”。这种模型的要点是状态的存储,对于全排列,可以运用康托展开将其与自然数映射;对于更一般的状态,可以使用哈希函数。C++STL中的 map 基于红黑树实现,可以很方便的存储状态,C++11中的 unordered_map 基于哈希函数,效率比 map 更高。这里运用 unordered_map 来存储状态。

题目还要求操作序列的字典序最小,只要在扩展时按照 A 、 B 、 C A、B、C ABC 的顺序扩展即可。用数学归纳法容易证明。

本题思路不难,但是代码量较大,操作较繁琐,要注意细节。

代码实现:

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

三、电路维修

175.电路维修 题目链接

本题可以看成求左上角到右下角的最短路,其中边权有两种:电缆不需旋转时,边权为 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值