【Leetcode刷题】广度优先搜索

本篇文章为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;
    }
};

不幸的是,拓扑排序也没有改善时间复杂度,还是超时。看了官方题解,好像不在能力范围之内,先暂且搁置。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值