🌠作者:@阿亮joy.
🎆专栏:《数据结构与算法要啸着学》
🎇座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
- 👉广度优先搜索👈
- 👉N叉树的层序遍历👈
- 👉腐烂的橘子👈
- 👉单词接龙👈
- 👉最小基因变化👈
- 👉打开转盘锁👈
- 👉总结👈
👉广度优先搜索👈
广度优先搜索算法(BFS)是通过队列来实现搜索的,其通常用于解决最短路径、最小步数等问题,深度优先搜索往往无法解决这些问题。
广度优先搜索模板
BFS()
{
1. 建立起始步骤,队列初始化
2. 遍历队列中的每一种可能,whlie(队列不为空)
{
通过队头元素带出下一步的所有可能,并且依次入队
{
判断当前情况是否达成目标:按照目标要求处理逻辑
}
继续遍历队列中的剩余情况
}
}
👉N叉树的层序遍历👈
给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。
思路:用当前层的节点将下一层的节点带入队列,当前层的节点全部出完队列,下一层的节点就全部入队列了,队列的大小就是下一层节点的个数。
/*
// Definition for a Node.
class Node
{
public:
int val;
vector<Node*> children;
};
*/
class Solution
{
public:
vector<vector<int>> levelOrder(Node* root)
{
vector<vector<int>> vv;
queue<Node*> q;
// 根节点不为空才入队列
if(root)
q.push(root);
while(!q.empty())
{
// 获取当前层节点的个数
int levelSize = q.size();
vector<int> v;
// 收集一层的节点
while(levelSize--)
{
Node* front = q.front();
q.pop();
v.push_back(front->val);
// 下一层节点入队列
for(auto& e : front->children)
{
q.push(e);
}
}
// 将一层节点存储的值放入结果中
vv.push_back(v);
}
return vv;
}
};
👉腐烂的橘子👈
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
- 值 0 代表空单元格;
- 值 1 代表新鲜橘子;
- 值 2 代表腐烂的橘子。
每分钟,腐烂的橘子周围 4 个方向上相邻的新鲜橘子都会腐烂。
返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
思路:本题可以先找到所有的腐烂橘子,入队列,用第一批带出新一批腐烂的橘子,每一批橘子都会在一分钟之内腐烂,所以此题可以转化为求广度优先搜索执行的最大循环次数。这里的 step 的更新需要有一个标记,只有新的腐烂的橘子加入,step 才能自加。最后广度优先搜索执行完之后,说明所有可以被腐烂的都完成了。再去遍历 grid,如何还有值为 1 的,说明没有办法完全腐烂,返回 -1,如果没有,则返回 step(注:step 是直到单元格中没有新鲜橘子为止所必须经过的最小分钟数)。
class Solution
{
public:
int orangesRotting(vector<vector<int>>& grid)
{
// 队列是用来存储腐烂橘子的坐标的
queue<pair<int, int>> q;
int row = grid.size();
int col = grid[0].size();
// 腐烂的橘入队列
for(int i = 0; i < row; ++i)
{
for(int j = 0; j < col; ++j)
{
if(grid[i][j] == 2)
q.push(make_pair(i, j));
}
}
// 可以蔓延的方向
int nextPosition[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int step = 0; // 最小分钟数
while(!q.empty())
{
int size = q.size();
bool flag = false; // 判断腐烂橘子周围有没有新鲜橘子
// 用当前这一批已经腐烂的橘子带出下一批要腐烂的橘子
// 故要遍历队列中的所有位置
while(size--)
{
auto curPos = q.front();
q.pop();
// 查找腐烂橘子周围有没有新鲜橘子
for(int i = 0; i < 4; ++i)
{
int newX = curPos.first + nextPosition[i][0];
int newY = curPos.second + nextPosition[i][1];
// 如果位置越界或者是空格,或者已经是腐烂的位置,则跳过
if(newX < 0 || newX >= row
|| newY < 0 || newY >= col
|| grid[newX][newY] != 1)
continue;
// 说明腐烂橘子的周围有新鲜橘子
flag = true;
grid[newX][newY] = 2;
// 腐烂橘子入队列
q.push(make_pair(newX, newY));
}
}
// 腐烂橘子周围有新鲜橘子才能加加step
if(flag)
++step;
}
// 查看还有没有新鲜的橘子,如果有,返回-1
// 如果没有,返回step
for(int i = 0; i < row; ++i)
{
for(int j = 0; j < col; ++j)
{
if(grid[i][j] == 1)
return -1;
}
}
return step;
}
};
👉单词接龙👈
思路:
- 通过广度优先搜索,首先用 beginWord 带出转换一个字母之后所有可能的结果。
- 每一步都要把队列中上一步添加的所有单词转换一遍,最短的转换肯定在这些单词当中, 所有这些词的转换只能算一次转换,因为都是上一步转换出来的。这里对于每个单词的每个位置都可以用 25 个字母进行转换,所以一个单词一次转换的可能有:单词的长度 * 25。
- 把转换成功的新词入队,进行下一步的转换。
- 最后整个转换的长度就和广度优先搜索执行的次数相同。
class Solution
{
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList)
{
// 用哈希表来查询单词是否存在
unordered_set<string> wordDict(wordList.begin(), wordList.end());
// 在visited中的单词表示已经搜索过了,不能再搜索了
unordered_set<string> visited;
visited.insert(beginWord);
// 初始化队列
queue<string> q;
q.push(beginWord);
int step = 1;
while(!q.empty())
{
int size = q.size();
// 每一步都要把队列中上一步添加的所有单词转换一遍
// 最短的转换肯定在这些单词当中,所有这些词的转换只能算一次转换
// 因为都是上一步转换出来的
while(size--)
{
string curStr = q.front();
q.pop();
// 存在转换序列
if(curStr == endWord)
return step;
// 尝试转换当前单词的每一个位置
for(int i = 0; i < curStr.size(); ++i)
{
string newStr = curStr;
// 每个位置上的字母都转换一次
for(char ch = 'a'; ch <= 'z'; ++ch)
{
newStr[i] = ch;
// 判断新的单词是否在词典中且没有搜索过
if(wordDict.count(newStr) == 1
&& visited.count(newStr) == 0)
{
q.push(newStr);
// 标记已经搜索过
visited.insert(newStr);
}
}
}
}
++step;
}
return 0; // 不存在转换序列
}
};
👉最小基因变化👈
注:最小基因变化的思路和单词接龙的思路是一样的,可参考单词接龙的思路。
class Solution
{
public:
int minMutation(string startGene, string endGene, vector<string>& bank)
{
// 用哈希表作为基因库,查询效率更高
unordered_set<string> HashBank(bank.begin(), bank.end());
// 已经搜索过的基因序列
unordered_set<string> visited;
visited.insert(startGene);
// 初始化队列
queue<string> q;
q.push(startGene);
// 基因
char arr[4] = {'A', 'C', 'G', 'T'};
int step = 0;
while(!q.empty())
{
int size = q.size();
// 队列中的基因序列都是一步转换得来的,所以最小的基因变化
// 肯定在队列中
while(size--)
{
string curStr = q.front();
q.pop();
// 能将startGene转化为endGene
if(curStr == endGene)
return step;
// 将每个位置上的基因都尝试变一变
for(int i = 0; i < 8; ++i)
{
string newStr = curStr;
// 基因有四种变法
for(int j = 0; j < 4; j++)
{
newStr[i] = arr[j];
// 有效且没有被搜索过的基因序列才能够入队列
if(HashBank.count(newStr) == 1
&& visited.count(newStr) == 0)
{
q.push(newStr);
// 标记该基因序列已经搜索过了
visited.insert(newStr);
}
}
}
}
++step;
}
return -1;
}
};
👉打开转盘锁👈
打开转盘锁的思路还是和单词接龙的思路一样,可以参照单词接龙的思路。需要注意的是,本题的密码为 4 位密码,每位密码可以通过拨动一次进行改变,注意这里的数的回环以及拨动的方向。如果当前是 9 时,那么逆时针转就会变成 0,;如果当前是 0 时,那么顺时针转就会变成 9。
class Solution
{
public:
int openLock(vector<string>& deadends, string target)
{
// 将死亡数字加入到哈希表中,提供查询效率
unordered_set<string> deadendsSet(deadends.begin(), deadends.end());
//如果"0000"也是死亡数字,则永远到达不了
if(deadendsSet.count("0000"))
return -1;
// 已经搜索过的字符串不需要再次搜索
unordered_set<string> visited;
visited.insert("0000");
// 初始化队列
queue<string> q;
q.push("0000");
int step = 0;
while(!q.empty())
{
int size = q.size();
// 从上一步转换之后的字符串都需要进行验证和转换
// 并且只算做一次转换,类似于层序遍历,转换的步数和层相同
// 同一层的元素都是经过一步转换得到的
while(size--)
{
string curStr = q.front();
q.pop();
if(curStr == target)
return step;
for(int i = 0; i < 4; ++i)
{
// newStr1是顺时针旋转得到的密码
// newStr2是逆时针旋转得到的密码
string newStr1 = curStr;
string newStr2 = curStr;
// 顺时针
newStr1[i] = newStr1[i] == '0' ? '9' : newStr1[i] - 1;
if(deadendsSet.count(newStr1) == 0
&& visited.count(newStr1) == 0)
{
q.push(newStr1);
visited.insert(newStr1);
}
// 逆时针旋转
newStr2[i] = newStr2[i] == '9' ? '0' : newStr2[i] + 1;
if(deadendsSet.count(newStr2) == 0
&& visited.count(newStr2) == 0)
{
q.push(newStr2);
visited.insert(newStr2);
}
}
}
++step;
}
return -1;
}
};
👉总结👈
本篇博客主要讲解了主要讲解了广度优先搜索的模型以及几道广度优先搜索的题目:N 叉树的层序遍历、腐烂的橘子、单词接龙、最小基因变化、打开转盘锁等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️