本篇文章为LeetCode 宽度优先搜索模块的刷题笔记,仅供参考。
广度优先搜索也是用来解决图问题的一种经典搜索算法,使用队列存储节点,while 循环进行遍历。为了防止重复,bfs 时 第一次 访问到某个节点时就标记 visit(如果弹出节点时再标记 visit 有可能会导致队列相当长,因为要搜索 n+n2+n3+…+nm 次,其中 m 是层数)。bfs 的过程还可以再开一个队列记录路径的长度 / 层数,但不能解决带权图的最短路径,如【Leetcode743.网络延迟时间】。
Leetcode102.二叉树的层序遍历
Leetcode102.二叉树的层序遍历
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
提示:
树中节点数目在范围 [0, 2000] 内
-1000 <= Node.val <= 1000
使用队列 + while 循环即可,由于要按层压入 vector,又开了一个队列专门记录每个节点的层数:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> ans;
if(root==nullptr) return ans;
queue<TreeNode*> q; //记录当前遍历节点
queue<int> level; //记录节点对应的level
q.push(root);
level.push(1);
vector<int> tmp;
ans.push_back(tmp);
while(!q.empty()){
TreeNode* head=q.front();
q.pop();
int l=level.front();
level.pop();
if(ans.size()==l){ // 第l层非第一个元素
ans[l-1].push_back(head->val);
}else{ // 第l层第一个元素
vector<int> tmp(1);
tmp[0]=head->val;
ans.push_back(tmp);
}
if(head->left!=nullptr){
q.push(head->left);
level.push(l+1);
}
if(head->right!=nullptr){
q.push(head->right);
level.push(l+1);
}
}
return ans;
}
};
Leetcode515.在每个树行中找最大值
Leetcode515.在每个树行中找最大值
给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。
示例1:
输入: root = [1,3,2,5,3,null,9]
输出: [1,3,9]
示例2:
输入: root = [1,2,3]
输出: [1,3]
提示:
二叉树的节点个数的范围是 [0,104]
-231 <= Node.val <= 231 - 1
层序遍历的一个简单变式:
class Solution {
public:
int maxV(vector<int>& v){
int ans=INT_MIN;
for(int i=0;i<v.size();i++){
ans=max(ans,v[i]);
}
return ans;
}
vector<int> largestValues(TreeNode* root) {
vector<int> ans;
if(root==nullptr) return {};
queue<TreeNode*> q; //记录当前遍历节点
queue<int> level; //记录节点对应的level
q.push(root);
level.push(1);
vector<int> tmp;
int cur_level=1;
while(!q.empty()){
TreeNode* head=q.front();
q.pop();
int l=level.front();
level.pop();
if(cur_level!=l){
ans.push_back(maxV(tmp));
tmp.clear();
}
if(head->left!=nullptr){
q.push(head->left);
level.push(l+1);
}
if(head->right!=nullptr){
q.push(head->right);
level.push(l+1);
}
cur_level=l;
tmp.push_back(head->val);
}
ans.push_back(maxV(tmp));
return ans;
}
};
Leetcode433.最小基因变化
Leetcode433.最小基因变化
基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 ‘A’、‘C’、‘G’ 和 ‘T’ 之一。
假设我们需要调查从基因序列 start 变为 end 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。
例如,“AACCGGTT” --> “AACCGGTA” 就是一次基因变化。
另有一个基因库 bank 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。(变化后的基因必须位于基因库 bank 中)
给你两个基因序列 start 和 end ,以及一个基因库 bank ,请你找出并返回能够使 start 变化为 end 所需的最少变化次数。如果无法完成此基因变化,返回 -1 。
注意:起始基因序列 start 默认是有效的,但是它并不一定会出现在基因库中。
示例 1:
输入:start = “AACCGGTT”, end = “AACCGGTA”, bank = [“AACCGGTA”]
输出:1
示例 2:
输入:start = “AACCGGTT”, end = “AAACGGTA”, bank = [“AACCGGTA”,“AACCGCTA”,“AAACGGTA”]
输出:2
示例 3:
输入:start = “AAAAACCC”, end = “AACCCCCC”, bank = [“AAAACCCC”,“AAACCCCC”,“AACCCCCC”]
输出:3
提示:
start.length = 8
end.length = 8
0 <= bank.length <= 10
bank[i].length = 8
start、end 和 bank[i] 仅由字符 [‘A’, ‘C’, ‘G’, ‘T’] 组成
由题意知,每次只能改变一个字符,并且改变字符后的字符串需要在 bank 中,将满足该条件的改变称为一次有效变化。于是可以 将字符串的一次有效变化视为变化前后的两个字符串连通,某字符串经过一次有效变化能够得到的字符串视为该字符串的孩子。
通过上述分析结合题意不难想到,可以将字符串 start 作为根节点,构造出一棵树。使用层序遍历,第一次遍历到字符串 end 时 end 的深度就是最少变化次数。
class Solution {
public:
bool inBank(string s,vector<string>& bank){
for(int i=0;i<bank.size();i++){
if(bank[i]==s) return true;
}
return false;
}
set<string> getChildren(string s,vector<string>& bank){
set<string> st;
for(int i=0;i<8;i++){
if(s[i]=='A'){
s[i]='C';
if(inBank(s,bank)) st.insert(s);
s[i]='G';
if(inBank(s,bank)) st.insert(s);
s[i]='T';
if(inBank(s,bank)) st.insert(s);
s[i]='A'; // 恢复原样
}
else if(s[i]=='C'){
s[i]='A';
if(inBank(s,bank)) st.insert(s);
s[i]='G';
if(inBank(s,bank)) st.insert(s);
s[i]='T';
if(inBank(s,bank)) st.insert(s);
s[i]='C';
}
else if(s[i]=='G'){
s[i]='A';
if(inBank(s,bank)) st.insert(s);
s[i]='C';
if(inBank(s,bank)) st.insert(s);
s[i]='T';
if(inBank(s,bank)) st.insert(s);
s[i]='G';
}
else if(s[i]=='T'){
s[i]='A';
if(inBank(s,bank)) st.insert(s);
s[i]='C';
if(inBank(s,bank)) st.insert(s);
s[i]='G';
if(inBank(s,bank)) st.insert(s);
s[i]='T';
}
}
return st;
}
int minMutation(string startGene, string endGene, vector<string>& bank) {
map<string,bool> flag;
queue<string> q;
q.push(startGene);
flag[startGene]=true;
queue<int> level;
level.push(1);
while(!q.empty()){
string tmp=q.front();
q.pop();
int l=level.front();
level.pop();
if(tmp==endGene) return l-1;
set<string> st=getChildren(tmp,bank);
for(set<string>::iterator it=st.begin();it!=st.end();it++){
if(flag[*it]==false){
q.push(*it);
level.push(l+1);
flag[*it]=true;
}
}
}
return -1;
}
};
Leetcode752.打开转盘锁
Leetcode752.打开转盘锁
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转:例如把 ‘9’ 变为 ‘0’,‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 ‘0000’ ,一个代表四个拨轮的数字的字符串。
列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。
示例 1:
输入:deadends = [“0201”,“0101”,“0102”,“1212”,“2002”], target = “0202”
输出:6
解释:
可能的移动序列为 “0000” -> “1000” -> “1100” -> “1200” -> “1201” -> “1202” -> “0202”。
注意 “0000” -> “0001” -> “0002” -> “0102” -> “0202” 这样的序列是不能解锁的,
因为当拨动到 “0102” 时这个锁就会被锁定。
示例 2:
输入: deadends = [“8888”], target = “0009”
输出:1
解释:把最后一位反向旋转一次即可 “0000” -> “0009”。
示例 3:
输入: deadends = [“8887”,“8889”,“8878”,“8898”,“8788”,“8988”,“7888”,“9888”], target = “8888”
输出:-1
解释:无法旋转到目标数字且不被锁定。
提示:
1 <= deadends.length <= 500
deadends[i].length = 4
target.length = 4
target 不在 deadends 之中
target 和 deadends[i] 仅由若干位数字组成
思路同【Leetcode433.最小基因变化】,但有一个比较坑的测试样例:[“0000”], “8888”,刚开始就不应该将 “0000” 压入队列:
class Solution {
public:
bool indeadends(string s,vector<string>& deadends){
for(int i=0;i<deadends.size();i++){
if(deadends[i]==s) return true;
}
return false;
}
set<string> getChildren(string s,vector<string>& deadends){
set<string> st;
for(int i=0;i<4;i++){
char cur=s[i];
if(s[i]>='1' && s[i]<='8'){
s[i]=s[i]+1;
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]=s[i]-2;
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]=cur; // 恢复
}else if(s[i]=='0'){
s[i]='1';
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]='9';
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]='0';
}else if(s[i]=='9'){
s[i]='8';
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]='0';
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]='9';
}
}
return st;
}
int openLock(vector<string>& deadends, string target) {
map<string,bool> flag;
queue<string> q;
if(indeadends("0000",deadends)) return -1;
q.push("0000");
flag["0000"]=true;
queue<int> level;
level.push(0);
while(!q.empty()){
string tmp=q.front();
q.pop();
int l=level.front();
level.pop();
if(tmp==target) return l;
flag[tmp]=true;
set<string> st=getChildren(tmp,deadends);
for(set<string>::iterator it=st.begin();it!=st.end();it++){
if(flag[*it]==false){
q.push(*it);
level.push(l+1);
flag[*it]=true;
}
}
}
return -1;
}
};
可惜的是,直接 bfs 超时,deadends.length = 500 时需要几秒才能得到正确答案。为了降低时间复杂度,引入 双向 bfs 的思路:同时从起点和终点两个方向开始搜索,一旦搜索到另一个方向已经搜索过的位置(或者说出现某个状态被两个方向均访问到了),就意味着找到了一条联通起点和终点的最短路径。 为了尽量让两个方向均匀搜索,即尽量保持两个方向搜索前进进度差不多,所以每次进行 bfs 搜索时,选择容量较少的队列进行 bfs 搜索,扩充该队列。双向 bfs 的终止条件为 某一个方向搜索过程中搜索到另一个方向搜索过的节点(相遇) 或者 某一个方向的队列为空(不连通)。
双向 bfs 适用于本题这种 开始节点和结束节点都确定 的问题,可以将时间复杂度由 O(nm+1) 降为 O(2*nm/2+1):
class Solution {
public:
bool indeadends(string s,vector<string>& deadends){
for(int i=0;i<deadends.size();i++){
if(deadends[i]==s) return true;
}
return false;
}
set<string> getChildren(string s,vector<string>& deadends){
set<string> st;
for(int i=0;i<4;i++){
char cur=s[i];
if(s[i]>='1' && s[i]<='8'){
s[i]=s[i]+1;
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]=s[i]-2;
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]=cur; // 恢复
}else if(s[i]=='0'){
s[i]='1';
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]='9';
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]='0';
}else if(s[i]=='9'){
s[i]='8';
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]='0';
if(!indeadends(s,deadends)){
st.insert(s);
}
s[i]='9';
}
}
return st;
}
int bfs(vector<string>& deadends,queue<string>& q1,queue<string>& q2,map<string,int>& level1,map<string,int>& level2){
string tmp=q1.front();
q1.pop();
set<string> st=getChildren(tmp,deadends);
for(set<string>::iterator it=st.begin();it!=st.end();it++){
if(level1[*it]==0){
if(level2[*it]>0){ // q2访问过*it
return level2[*it]+level1[tmp]-1;
}
q1.push(*it);
level1[*it]=level1[tmp]+1;
}
}
return -1;
}
int openLock(vector<string>& deadends, string target) {
if(target=="0000") return 0;
// "0000"->target
queue<string> q1;
if(indeadends("0000",deadends)) return -1;
q1.push("0000");
map<string,int> level1;
level1["0000"]=1; // 从1计数,0表示未访问
// target->"0000"
queue<string> q2;
if(indeadends(target,deadends)) return -1;
q2.push(target);
map<string,int> level2;
level2[target]=1;
// 双向bfs
while(!q1.empty() && !q2.empty()){
int ans=-1;
if(q1.size()<q2.size()){
ans=bfs(deadends,q1,q2,level1,level2);
}
else{
ans=bfs(deadends,q2,q1,level2,level1);
}
if(ans!=-1) return ans;
}
return -1;
}
};
Leetcode310.最小高度树
Leetcode310.最小高度树
树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
给你一棵包含 n 个节点的树,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。
可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。
请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。
树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。
示例 1:
输入:n = 4, edges = [[1,0],[1,2],[1,3]]
输出:[1]
解释:如图所示,当根是标签为 1 的节点时,树的高度是 1 ,这是唯一的最小高度树。
示例 2:
输入:n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]]
输出:[3,4]
提示:
1 <= n <= 2 * 104
edges.length = n - 1
0 <= ai, bi < n
ai != bi
所有 (ai, bi) 互不相同
给定的输入保证是一棵树,并且不会有重复的边
法一:广度优先搜索
本题不再是二叉树,而是普通的树,因此不再使用指针存储。题干给的树是以图的边的形式存储的,常规情况下一般使用 vector<vector<int>>
存储。因为整张图无环,因此可以遍历 n 个节点,将每个节点作为根节点构造树,然后进行搜索得到每棵树的高度,然后比较得到最小高度。
一开始采用的构造树的方法是遍历每一条边,然后将其压入各自的 vector,但这样相当于为图加入了双向的有向边,整张图出现了相当多的环路,dfs 陷入死循环。后来想到 bfs 时正好能够得到树的高度,还可以将构造树和搜索高度结合到一起:
class Solution {
public:
int bfs(int n, int root, vector<vector<bool>>& tree) { // 返回树的高度
vector<int> level(n);
for(int i=0;i<n;i++) level[i]=0;
level[root]=1;
queue<int> q;
q.push(root);
int h=0;
while(!q.empty()){
int tmp=q.front();
q.pop();
for(int i=0;i<tree[tmp].size();i++){
if(tree[tmp][i]==true && level[i]==0){
q.push(i);
level[i]=level[tmp]+1;
h=max(h,level[i]);
}
}
}
return h;
}
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
// 将edges[edgei[ai,bi]]转换为tree[nodei[...]]
vector<vector<bool> > tree;
vector<bool> tmp(n);
tree.resize(n,tmp);
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
tree[i][j]=false;
}
}
for(int i=0;i<edges.size();i++){
tree[edges[i][0]][edges[i][1]]=true;
tree[edges[i][1]][edges[i][0]]=true;
}
// 遍历节点作为根节点
vector<int> height(n);
int h=INT_MAX;
for(int i=0;i<n;i++){
// bfs
height[i]=bfs(n,i,tree);
// 求最小高度
h=min(h,height[i]);
}
// 求最小高度对应的根节点
vector<int> ans;
for(int i=0;i<n;i++){
if(h==height[i]) ans.push_back(i);
}
return ans;
}
};
遗憾的是,该方法的时间复杂度 O(n3),运行超时,没能通过测试。
法二:拓扑排序
其实把这道题就当成图来做就可以,想要找到具有最小高度的树的树根,一定是以图的最中心的节点为根节点得到的树。因此采用拓扑排序的思想,从度为 1 的节点(即叶节点)开始,向前回溯即可,这个过程本质上是一个从叶节点追溯根节点的过程,最后得到的节点就是最小高度树的根节点。
需要注意的是,每一次搜索时需要将所有度为 1 的节点同时压入队列,否则一对节点的度同时减 1 后会影响后面的结果。比如 1<-> 2 <-> 3,如果先将 1 压入后剩下 2 <-> 3,度都为 1,显然违背本意。
class Solution {
public:
bool isEnd(vector<bool>& isVisited){
for(int i=0;i<isVisited.size();i++){
if(isVisited[i]==false) return false;
}
return true;
}
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
// 将edges[edgei[ai,bi]]转换为图,为了方便删除边,所以使用相邻矩阵
vector<vector<bool> > tree;
vector<bool> tmp(n);
tree.resize(n,tmp);
vector<int> degree(n);
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
tree[i][j]=false;
}
}
for(int i=0;i<n;i++) degree[i]=0;
for(int i=0;i<edges.size();i++){
tree[edges[i][0]][edges[i][1]]=true;
tree[edges[i][1]][edges[i][0]]=true;
degree[edges[i][0]]++;
degree[edges[i][1]]++;
}
// 是否访问
vector<bool> isVisited(n);
for(int i=0;i<n;i++) isVisited[i]=false;
// 剪去度为 1 的节点
vector<int> ans;
while(!isEnd(isVisited)){
ans.clear();
for(int i=0;i<n;i++){
if(degree[i]==1){
ans.push_back(i);
isVisited[i]=true;
}
}
// 还有不少于两个节点没有访问到,那么 ans.size() 一定大于 2
for(int i=0;i<ans.size();i++){
degree[ans[i]]--;
for(int j=0;j<n;j++){
if(tree[ans[i]][j]){
degree[j]--;
tree[ans[i]][j]=false;
tree[j][ans[i]]=false;
}
}
}
// 还剩一个节点没被访问,特殊判断防止陷入死循环
if(ans.size()==0){
for(int i=0;i<n;i++){
if(isVisited[i]==false){
ans.push_back(i);
isVisited[i]=true;
}
}
}
}
return ans;
}
};
不幸的是,拓扑排序也没有改善时间复杂度,还是超时。看了官方题解,好像不在能力范围之内,先暂且搁置。