多源BFS,即同时存在多个起点,然后要求计算出图中任意一个点距离所有起点的最短距离,即图中任意点到每个起点的距离的最小值。 和一般的BFS的区别在于存在多个起点,而我们可以对所有的起点建立一个虚拟超级起点S,S与所有给定的起点一步相邻。设S到点A的最短距离为x,则 min(给定的所有起点中到A的最短距离) = x。
开方向数组 dx, dy
下一个点进队之前先判断是否出界
开 visited 记录已经选过的点,下一个点进队前,先用它判重
队列中存的数据,除了节点的坐标可能还有别的信息,有时可以封装结构体,结构图题中除了坐标,还可以持有节点上的状态
Step 1: 将所有的起点都加入队列中 Step 2: void bfs()//广度优先遍历 { int dx[] = {-1, 0 , 1, 0}, dy[] = {0, 1, 0,-1};//上下左右四个方向 while(q.size()) { PII f = q.front();//取队头 q.pop();//出队 for(int i = 0; i < 4 ; i++)//向四个方向进行遍历 { int x = f.first + dx[i], y = f.second + dy[i]; if(g[x][y] == 1)//如果能走过去 { if(dist[x][y] > dist[f.first][f.second] + 1)//如果距离变小 { dist[x][y] = dist[f.first][f.second] + 1;//更新距离 q.push({x, y});//并且入队 } } } } }
给定一个 N 行 M 列的 01 矩阵 A,A[i][j] 与 A[k][l] 之间的曼哈顿距离定义为: dist(A[i][j],A[k][l])=|i−k|+|j−l| 输出一个 N 行 M 列的整数矩阵 B,其中: B[i][j]=min1≤x≤N,1≤y≤M,A[x][y]=1dist(A[i][j],A[x][y]) 输入格式 第一行两个整数 N,M。 接下来一个 N 行 M 列的 01 矩阵,数字之间没有空格。 输出格式 一个 N 行 M 列的矩阵 B,相邻两个整数之间用一个空格隔开。 数据范围 1≤N,M≤1000 输入样例: 3 4 0001 0011 0110 输出样例: 3 2 1 0 2 1 0 0 1 0 0 1 #include <cstring> #include <iostream> #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]; PII q[M]; int dist[N][N]; 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) { auto 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; }
BFS 一个经典应用,即类似八数码问题。在这类问题中,题目中事物的每一个状态看作一个节点,初始状态是起点,目标状态是终点,求解从起点到终点的最小步数。因此在这类题目中往往是状态的表示和变化比较复杂,代码量较大,细节易出错。
Rubik 先生在发明了风靡全球的魔方之后,又发明了它的二维版本——魔板。 这是一张有 8 个大小相同的格子的魔板: 1 2 3 4 8 7 6 5 我们知道魔板的每一个方格都有一种颜色。 这 8 种颜色用前 8 个正整数来表示。 可以用颜色的序列来表示一种魔板状态,规定从魔板的左上角开始,沿顺时针方向依次取出整数,构成一个颜色序列。 对于上图的魔板状态,我们用序列 (1,2,3,4,5,6,7,8) 来表示,这是基本状态。 这里提供三种基本操作,分别用大写字母 A,B,C 来表示(可以通过这些操作改变魔板的状态): A:交换上下两行; B:将最右边的一列插入到最左边; C:魔板中央对的4个数作顺时针旋转。 下面是对基本状态进行操作的示范: A: 8 7 6 5 1 2 3 4 B: 4 1 2 3 5 8 7 6 C: 1 7 2 4 8 6 3 5 对于每种可能的状态,这三种基本操作都可以使用。 你要编程计算用最少的基本操作完成基本状态到特殊状态的转换,输出基本操作序列。 注意:数据保证一定有解。 输入格式 输入仅一行,包括 8 个整数,用空格分开,表示目标状态。 输出格式 输出文件的第一行包括一个整数,表示最短操作序列的长度。 如果操作序列的长度大于0,则在第二行输出字典序最小的操作序列。 数据范围 输入数据中的所有数字均为 1 到 8 之间的整数。 输入样例: 2 6 8 4 5 7 3 1 输出样例: 7 BCABCCB
其实解法在开头就已经说明白了,因此提几点要注意的:
1、三种操作实现;
2、记录上一次的操作;
当然为了满足字典序,其实每次按照ABC的顺序依次操作就好了。
#include <cstring> #include <iostream> #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; 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]; } 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) { set(state); for (int i = 0; i < 4; i ++ ) swap(g[0][i], g[1][i]); return get(); } string move1(string state) { set(state); int v0 = g[0][3], v1 = g[1][3]; for (int i = 3; i >= 0; 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(); } 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(); } int bfs(string start, string end) { if (start == end) return 0; queue<string> q; q.push(start); dist[start] = 0; while (!q.empty()) { auto 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 ++ ) if (!dist.count(m[i])) { dist[m[i]] = dist[t] + 1; pre[m[i]] = {'A' + i, t}; q.push(m[i]); if (m[i] == end) return dist[end]; } } return -1; } 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('0' + i); int step = bfs(start, end); cout << step << endl; string res; while (end != start) { res += pre[end].first; end = pre[end].second; } reverse(res.begin(), res.end()); if (step > 0) cout << res << endl; return 0; }
如果最短路的边权只有01两种,那么我们可以使用双端队列BFS。
用双端队列BFS实现01最短路通常能比SPFA和dijkstra省时。
时间复杂度:O(N)。
如果当前处理的点的边权为0,那么将这个点移至队首,否则移至队尾。
如果边权为0,那么这条路是当前的最短路(移入队首),反之,如果边权为1,那么就不是最短路(移入队尾)。
这样,我们可以维护队列的的单调性。
这是最短路径问题中的特例,因为其边权只可能是0或1;同时这也是许多问题的抽象形式,很多问题可以抽象为上述模型1-1。我们的目标就是找到一个尽可能高效的算法解决上述问题模型。
最短路径算法:
这是很容易想到的图论算法,解决此题的效率为O(NM)或O(M log N),取决于不同的算法。
双端队列BFS
算法描述:
由于这是一张边权要么为0、要么为1的图。在这样的图上,我们可以通过双端队列广搜来计算。算法的整体框架与一般的广搜类似,只是在每个节点沿分支拓展时稍作改变。如果这条分支边权为0,则从队首入队,否则从队尾入队。这样我们能保证,任意时刻广搜队列中节点对应的距离值都有“两端性”和“单调性”,每个节点第一次被访问时,就能得到从左上角到该节点的最短距离。
效率分析:
我们可以知道该算法中每个节点仅被访问一次,因为这是BFS的特性,所以该算法的效率为O(N)。
正确性证明:
由于我们最终目标是求路径权值和,而权值为0的边无论走多少条权值和依旧是+0,因此我们可以优先走权值为0的边,更新与这些边相连的点x的d[x](d[i] 为从s到i最小权值和),此时d[x]一定是最小的,因为它是由尽可能多的权值为0的边更新而来。所以在队列中取出的节点同时满足“连通性”和“权值最小”,因此每个节点仅需被更新一次。
达达是来自异世界的魔女,她在漫无目的地四处漂流的时候,遇到了善良的少女翰翰,从而被收留在地球上。 翰翰的家里有一辆飞行车。 有一天飞行车的电路板突然出现了故障,导致无法启动。 电路板的整体结构是一个 R 行 C 列的网格(R,C≤500),如下图所示。 每个格点都是电线的接点,每个格子都包含一个电子元件。 电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。 在旋转之后,它就可以连接另一条对角线的两个接点。 电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。 达达发现因为某些元件的方向不小心发生了改变,电路板可能处于断路的状态。 她准备通过计算,旋转最少数量的元件,使电源与发动装置通过若干条短缆相连。 不过,电路的规模实在是太大了,达达并不擅长编程,希望你能够帮她解决这个问题。 注意:只能走斜向的线段,水平和竖直线段不能走。 输入格式 输入文件包含多组测试数据。 第一行包含一个整数 T,表示测试数据的数目。 对于每组测试数据,第一行包含正整数 R 和 C,表示电路板的行数和列数。 之后 R 行,每行 C 个字符,字符是"/"和"\"中的一个,表示标准件的方向。 输出格式 对于每组测试数据,在单独的一行输出一个正整数,表示所需的最小旋转次数。 如果无论怎样都不能使得电源和发动机之间连通,输出 NO SOLUTION。 数据范围 1≤R,C≤500, 1≤T≤5 输入样例: 1 3 5 \\/\\ \\/// /\\\\ 输出样例: 1
#include <cstring> #include <iostream> #include <algorithm> #include <deque> #define x first #define y second using namespace std; typedef pair<int, int> PII; const int N = 510, M = N * N; int n, m; char g[N][N]; int dist[N][N]; bool st[N][N]; int bfs() { memset(dist, 0x3f, sizeof dist); memset(st, 0, sizeof st); dist[0][0] = 0; deque<PII> q; q.push_back({0, 0}); char cs[] = "\\/\\/"; 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(); 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; }