016搜索之广度优先BFS——算法备赛
广度优先BFS
广度优先搜索是一种逐层遍历的方式,是图论,树论的基本搜索方式,在决策类问题上也有应用。
算法的关键是准备一个节点队列
,每遍历一个节点将其所有未访问的子节点(或所有的邻接节点)入队,遍历完一个节点后及时从队列中出队。当队列为空遍历结束。
BFS作为基础搜索算法,其逐层扩散的特点在很多高级算法有着广泛的应用,如Djstra
算法就是用优先队列实现的BFS
。
扩散模型
01矩阵
问题描述
给定一个由 0
和 1
组成的矩阵 mat
,请输出一个大小相同的矩阵,其中每一个格子是 mat
中对应位置元素到最近的 0
的距离。
两个相邻元素间的距离为 1
。
原题链接
思路分析
把0看作海水,1看作陆地,利用bfs的逐层扩散的原理,先将0全部入队,维护一个扩散层数cnt,每次扩散到一个陆地,该陆地到最近的海水的距离就是cnt,该陆地变成海水,入队进行下一层扩散,每次扩散完一层cnt+1。因为矩阵中只有0和1没有阻碍物,可以保证所有节点都遍历到。
代码
vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {int n=mat.size(),m=mat[0].size();vector<vector<int>>ans(n,vector<int>(m,-1));queue<pair<int,int>>q;for(int i=0;i<n;i++){for(int j=0;j<m;j++){if(mat[i][j]==0){q.push({i,j});ans[i][j]=0;} }}int dx[4]={1,0,-1,0};int dy[4]={0,1,0,-1};int cnt=0; //扩展的层数while(!q.empty()){ //bfsint s=q.size();cnt++;for(int i=0;i<s;i++){auto [r,c]=q.front();q.pop();for(int i=0;i<4;i++){ //4个方向int row=r+dx[i];int col=c+dy[i];if(row<0||row>=n||col<0||col>=m||ans[row][col]!=-1) continue;q.push({row,col});ans[row][col]=cnt;}}}return ans;
}
岛屿个数
问题描述
有一个M*N的矩阵,代表一个地图,其中只包含0
和1
,0
代表海水,1
代表陆地,地图之外视作全是海水,每个岛屿有上下左右四个方向上相邻的1
相连接组成。
在岛屿A所占据的所有陆地中,如果可以选出k个不同的格子,使得他们的坐标能组成这样的排列:
(x0,y0),(x1,y1),…,(xk-1,yk-1),其中(xi+1 mod k,yi+1 mod k)是由(xi,yi)上下左右移动一格得来的,那这k个陆地就构成了一个环。如果另一个岛屿B位于这个环内部,此时我们将岛屿B视作岛屿A的子岛屿。若B是A的子岛屿,C是B的子岛屿,那C也是A的子岛屿。
统计图中非子岛屿
的个数?
输入
第一行一个整数 T,表示有 T 组测试数据。
接下来输入 T 组数据。对于每组数据,第一行包含两个用空格分隔的整数 M、N 表示地图大小;接下来输入 M 行每行包含 N 个字符,字符只可能是’0’或’1’。
输出
对于每个T输出一个整数代表答案
原题链接
思路分析
统计岛屿个数不难,其实就是统计图中连通分量的个数。
统计非子岛屿
的个数就复杂一点了。
首先考虑最直接的方法,枚举每个岛屿,判断它是否是子岛屿?
如上图(3,5)单像素岛屿(以右上角为原点(0,0))就是一个子岛屿,应为它在外面一个岛屿构成的一个环中。如果暴力找出环来看环中是否有岛屿那又是复杂的工程,仔细观察思考一下会发现子岛屿是被完全封闭在一个岛屿内部,这也就是说从该子岛屿扩散海水是扩散不到它的父岛屿的外部的更扩散不到图的边界。因此判断一个岛屿是否是子岛屿可以从它旁边的海水做BFS扩散,若能扩散到边界说明它不是子岛屿,否则是。
上述方法每遍历到一个岛屿就往外作扩散,有时常常就是一次又一次的扩散全图,效率较低。这里不妨进一步思考,子岛屿的特点是被外部的父岛屿完全包围,内部的水流不出去,反过来,外部的水也流不进来。那我们可以一开始就从最外的海水出发BFS扩散遍历全图,访问到陆地就再用(BFS或DFS)对岛屿进行染色标记,哪些访问不到的陆地就是子岛屿的陆地像素(题目也没问子岛屿的数量,压根就不用管)。最后统计的岛屿数就是非子岛屿的数量。该方法最多进行一次扩散遍历全图,效率较高。
细节
- 算法需要找一个最外面的海水像素作为BFS遍历起点,有时候最外一层全是陆地像素,所以可以先预处理数据,在原图上再加一圈海水,选择(0,0)作为遍历起点。
- 结合题目关于环的定义,海水扩散应该是从8个方向扩散,陆地染色标记应该是从4个方向扩散。
总结
这题其实很有显示意义,有没有发现它和围棋很想像。在围棋的规则中,被围的子就像是子岛屿。
代码
#include <bits/stdc++.h>
using namespace std;
int t;
int n, m, cnt;
int dx[8] = { 0,1,0,-1,-1,1,1,-1 }; //前4个为上下左右四个方向,后4个为四个对角线方向
int dy[8] = { 1,0,-1,0,1,1,-1,-1 }; vector<vector<char>>mp;
vector<vector<bool>>vis;bool check(int r, int c) { //判断是否越界return r < 0 || r >= n+2 || c < 0 || c >= m+2; //注意长宽加2
}void bfsDye(int r, int c) {queue<pair<int, int>>q; q.push({ r,c });vis[r][c]=true;while (!q.empty()) {auto [row, col] = q.front();q.pop();for (int i = 0; i < 4; i++) { //岛屿扩展染色标记,搜索4个方向int rt = row + dx[i];int ct = col + dy[i];if (check(rt, ct)||vis[rt][ct]) continue; //搜索到边界或已搜索if (mp[rt][ct] == '1') {vis[rt][ct] = true;q.push({ rt,ct });}}}
}void bfs(int r, int c) {queue<pair<int, int>>q;q.push({ r,c });vis[r][c] = true;while (!q.empty()) {auto [row, col] = q.front();q.pop();for (int i = 0; i < 8; i++) { //外海域扩展,从8个方向搜索int rt = row + dx[i];int ct = col + dy[i];if (check(rt, ct)||vis[rt][ct]) continue; //搜索到边界或已搜索if (mp[rt][ct] == '0') {vis[rt][ct] = true;q.push({ rt,ct });}else{ //搜索到新陆地像素cnt++; //搜索到一个岛屿bfsDye(rt,ct); //染色该岛屿的所有像素}}}
}void solve(int t) {while (t--) {cin >> n >> m;cnt = 0;mp = vector<vector<char>>(n+2,vector<char>(m+2)); //往外扩展一圈海域,不影响最后结果 vis = vector<vector<bool>>(n+2, vector<bool>(m+2));mp[0]=vector<char>(m+2,'0'); //上面扩展一层海域for (int i = 1; i <= n; i++){mp[i][0]='0'; //左边添加一个海水像素for(int j=1;j<=m;j++){cin>>mp[i][j];}mp[i][m+1]='0'; //右边添加一个海水像素} mp[n+1]=vector<char>(m+2,'0'); //下面扩展一层海域bfs(0,0);cout << cnt << endl;}
}
int main()
{// 请在此输入您的代码ios::sync_with_stdio(0);cin.tie(0);cin >> t;solve(t);return 0;
}
决策树模型
卡片换位
蓝桥杯2016年省赛题
问题描述
一个简单的华容道问题:给定一个2*3的格子矩阵,如:
* A
**B
其中放5张牌,A代表关羽,B代表张飞,*代表士兵,还有一个空的格子。每次你可以将一张牌移到空的格子上,游戏目标是关羽和张飞换位(其他牌位置没有要求)。问最少操作数?
原题链接
思路分析
确定空格,A,B的位置能唯一确定整张图的位置,定义一个node记录它们的位置。
在输入时输入的是两个字符串,为方便计算和处理,我们将字符串拼接起来,原矩阵中(r,c)
的位置对应拼接后字符串的下标idr*n+c
(n表示行数,在原题中为2)。
每次操作只能是空格与周围牌交换位置,可以先提前准备好一个邻接表edges
,edges[i]记录了与id为i相邻的id
。
多次操作的选择构成了一棵决策树,因为边的权值是固定的(对答案的贡献都是1),可以直接采用bfs的方式搜索决策树,搜索到指定node
时,该node
所在的层数就是答案。
其他细节:在bfs搜索的时候,为避免搜索已访问的节点,我们可以用一个set
记录已访问的节点,后面已访问的不在继续访问,但节点node
是自定义的数据结构,为方便编码,可以用字符串拼接的方式作为它的hash
值。
以题给矩阵构造决策树如下:
代码
#include <bits/stdc++.h>
using namespace std;
int dx[4] = { 1,0,-1,0 };
int dy[4] = { 0,1,0,-1 };
unordered_set<string>st;
vector<vector<int>>edges;
int n = 2, m = 3;
string str;
struct node {int blank, aId, bId; //空格,a,b的位置
};
string getHash(node t) { //获取node对应的hash字符串string v;v += to_string(t.blank) + ".";v += to_string(t.aId) + ".";v += to_string(t.bId);return v;
}
void init() {edges.resize(n * m);for (int i = 0; i < n; i++) {string s;getline(cin, s);str += s;}for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {int id = i * m + j;for (int k = 0; k < 4; k++) {int r = i + dx[k];int c = j + dy[k];if (r < 0 || r >= n || c < 0 || c >= m) continue;edges[id].push_back(r * m + c);}}}
}
int bfs() {int At, Bt, blank;for (int i = 0; i < n*m; i++) {if (str[i] == 'A') Bt = i;else if (str[i] == 'B') At = i;else if (str[i] == ' ') blank = i;}node top = { blank,Bt,At };queue<node>q;st.insert(getHash(top));q.push(top);int ans = 0;while (!q.empty()) {int s = q.size();for (int i = 0; i < s; i++) {node tr = q.front();q.pop();if (tr.aId == At && tr.bId == Bt) {return ans;}int start = tr.blank;for (int i = 0; i < edges[start].size(); i++) {int to = edges[start][i];node ty = tr;if (to == ty.aId) {swap(ty.aId, ty.blank);}else if (to == tr.bId) {swap(ty.bId, ty.blank);}else {ty.blank = to;}if (!st.count(getHash(ty))) {q.push(ty);st.insert(getHash(ty));}}}ans++;}return ans;
}
int main()
{init();cout << bfs();return 0;
}
打开转盘锁
问题描述
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
。每个拨轮可以自由旋转:例如把 '9'
变为 '0'
,'0'
变为 '9'
。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 '0000'
,一个代表四个拨轮的数字的字符串。
列表 deadends
包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target
代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1
。
原题链接
代码
int openLock(vector<string>& deadends, string target) {unordered_set<string>st(deadends.begin(),deadends.end());if(st.count("0000")) return -1;st.insert("0000");queue<string>q;q.push("0000");int ans=0;while(!q.empty()){ans++;int s=q.size();for(int i=0;i<s;i++){string t=q.front();q.pop();if(t==target) return ans-1;for(int j=0;j<4;j++){string tr1=t;string tr2=t;tr1[j]=tr1[j]=='9'?'0':tr1[j]+1;tr2[j]=tr2[j]=='0'?'9':tr2[j]-1;if(!st.count(tr1)){q.push(tr1);st.insert(tr1);}if(!st.count(tr2)){q.push(tr2);st.insert(tr2);}}}}return -1;
}
层级模型
公交线路
问题描述
给你一个数组 routes
,表示一系列公交线路,其中每个 routes[i]
表示一条公交线路,第 i
辆公交车将会在上面循环行驶。
- 例如,路线
routes[0] = [1, 5, 7]
表示第0
辆公交车会一直按序列1 -> 5 -> 7 -> 1 -> 5 -> 7 -> 1 -> ...
这样的车站路线行驶。
现在从 source
车站出发(初始时不在公交车上),要前往 target
车站。 期间仅可乘坐公交车。
求出 最少乘坐的公交车数量 。如果不可能到达终点车站,返回 -1
示例:
输入:routes = [[1,2,7],[3,6,7]], source = 1, target = 6
输出:2
解释:最优策略是先乘坐第一辆公交车到达车站 7 , 然后换乘第二辆公交车到车站 6 。
原题链接
思路分析
预处理,定义一个无序哈希map:stop_to_buses
,stop_to_buses[i]
储存的是进过站点i的公交车编号数组,用于后面快速定位公交车。
将每个站点抽象成树的节点,问题转换成,从根节点到指定节点经过的最小边数,相当于求指定点的深度-1。(在很多边权固定为1的场景求最短路径,都可以直接BFS求层数)
首先将起点入队,通过起点能直接到达的点构成该树的第二层,第二层节点全部入队,记录第二层每个节点到根节点的最小边数
依此类推,直到遍历到目标节点。
注意:遍历过的节点不再访问
时间复杂度O(S),S为routes的总长度
代码
int numBusesToDestination(vector<vector<int>>& routes, int source, int target) {// 记录经过车站 x 的公交车编号unordered_map<int, vector<int>> stop_to_buses;for (int i = 0; i < routes.size(); i++) {for (int x : routes[i]) {stop_to_buses[x].push_back(i);}}// 小优化:如果没有公交车经过起点或终点,直接返回if (!stop_to_buses.contains(source) || !stop_to_buses.contains(target)) {// 注意原地 TP 的情况return source != target ? -1 : 0;}// BFSunordered_map<int, int> dis;dis[source] = 0;queue<int> q;q.push(source);while (!q.empty()) {int x = q.front(); // 当前在车站 xq.pop();int dis_x = dis[x];for (int i : stop_to_buses[x]) { // 遍历所有经过车站 x 的公交车 ifor (int y : routes[i]) { // 遍历公交车 i 的路线if (!dis.contains(y)) { // 没有访问过车站 ydis[y] = dis_x + 1; // 从 x 站上车然后在 y 站下车q.push(y);}}routes[i].clear(); // 标记 routes[i] 遍历过}}return dis.contains(target) ? dis[target] : -1;}
总结
对于上题,解决的关键
- 如何将问题建模,从而转换为一般的求树的深度问题
- 将routes中的值转换为键,从而实现相邻节点的快速求解