递归回溯分治+BFS汇总

回溯算法简介:

回溯算法是一种试探性算法,会对每一次试探结果进行评估。如果当前的情况已经满足要求,则没有必要继续试探,也就是可以避免走弯路。如果当前情况满足要求,则保存相应的方法。

回溯的特性是可以在出现非法情况时,算法可以回退到之前的情景,可以返回一步或多步。

 

1、子集、排列、组合问题 都可以使用回溯算法来解决:

子集、排列、组合问题
1)子集问题可以利用数学归纳思想,假设已知一个规模较小的问题的结果,思考如何推导出原问题的结果。也可以用回溯算法,要用 start 参数排除已 选择的数字。
2)组合问题利用的是回溯思想,结果可以表示成树结构,我们只要套用回溯算 法模板即可,关键点在于要用一个 start 排除已经选择过的数字。
3)排列问题是回溯思想,也可以表示成树结构套用算法模板,关键点在于使用 contains 方法排除已经选择的数字,前文有详细分析,这里主要是和组合问题作对比。
记住这几种树的形状,就足以应对大部分回溯算法问题了,无非就是 start 或者 contains 剪枝,也没啥别的技巧了。

 

回溯算法解题模板: 

// 回溯算法,解题模板
result = []
def backtrack(路径, 选择列表): 
    if 满足结束条件:
        result.add(路径)
        return
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

  

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: nums = [1,2,3]
输出:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

子集问题思路:

比如输入 nums = [1,2,3] ,你的算法应输出8个子集,包含空集和本身, 顺序可以不同:
[ [],[1],[2],[3],[1,3],[2,3],[1,2],[1,2,3] ]
第一个解法是利用数学归纳的思想:假设我现在知道了规模更小的子问题的 结果,如何推导出当前问题的结果呢?
具体来说就是,现在让你求 [1,2,3] 的子集,如果你知道了 [1,2] 的子 集,是否可以推导出 [1,2,3] 的子集呢?先把 [1,2] 的子集写出来瞅瞅:
[ [],[1],[2],[1,2] ]
你会发现这样一个规律:
subset( [1,2,3] ) - subset( [1,2] ) = [3],[1,3],[2,3],[1,2,3]

而这个结果,就是把 sebset( [1,2] ) 的结果中每个集合再添加上 3。 换句话说,如果 A = subset([1,2]) ,那么:
subset( [1,2,3] )
= A + [A[i].add(3) for i = 1..len(A)]
这就是一个典型的递归结构嘛, [1,2,3] 的子集可以由 [1,2] 追加得出, [1,2] 的子集可以由 [1] 追加得出,
base case 显然就是当输入集合 为空集时,输出子集也就是一个空集。

//递归方式

class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) { 
    // base case,返回一个空集
    if (nums.empty()) return {{}};
    // 把最后一个元素拿出来

    int n = nums.back();
    nums.pop_back();
    // 先递归算出前面元素的所有子集
    vector<vector<int>> res = subsets(nums);

    int size = res.size();
    for (int i = 0; i < size; i++) {
        // 然后在之前的结果之上追加
        res.push_back(res[i]);
        res.back().push_back(n);
    }
    return res;  
    }
};

 

关于复杂度的分析:

这个问题的时间复杂度计算比较容易坑人。我们之前说的计算递归算法时间 复杂度的方法,是找到递归深度,然后乘以每次递归中迭代的次数。
对于这 个问题,递归深度显然是 N,但我们发现每次递归 for 循环的迭代次数取决 于 res 的⻓度,并不是固定的。

根据刚才的思路, res 的⻓度应该是每次递归都翻倍,所以说总的迭代次 数应该是 2^N。
或者不用这么麻烦,你想想一个大小为 N 的集合的子集总 共有几个?2^N 个对吧,所以说至少要对 res 添加 2^N 次元素。
那么算法的时间复杂度就是 O(2^N) 吗?还是不对,2^N 个子集是 push_back 添加进 res 的,所以要考虑 push_back 这个操作的效率:

因为 res[i] 也是一个数组呀, push_back 是把 res[i] copy 一份然后添 加到数组的最后,所以一次操作的时间是 O(N)。
综上,总的时间复杂度就是 O(N*2^N),还是比较耗时的。 空间复杂度的话,如果不计算储存返回结果所用的空间的,只需要 O(N) 的
递归堆栈空间。如果计算 res 所需的空间,应该是 O(N*2^N)。

 

第二种通用方法就是回溯算法。回溯算法的模板:

result = []
def backtrack(路径, 选择列表): 
    if 满足结束条件:
        result.add(路径)
        return
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择



//所以只要改造回溯算法的模板就行了:

  

class Solution {
public:
    vector<vector<int>> res;
    vector<vector<int>> subsets(vector<int>& nums) { 
        // 记录走过的路径
        vector<int> track;
        backtrack(nums, 0, track);
        return res;
    }
    void backtrack(vector<int>& nums, int start, vector<int>& track) {
        res.push_back(track);
        for (int i = start; i < nums.size(); i++) {
            // 做选择 
            track.push_back(nums[i]);
            // 回溯
            backtrack(nums, i + 1, track); 
            // 撤销选择
            track.pop_back();
        }
    }
};

  

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: [1,2,2]
输出:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]

 

思路:这是之前子集的延伸,不过这次允许有重复项。拿题目中的例子[1 2 2]来分析,根据子集的分析,当处理到第一个2时,
此时的子集合为[], [1], [2], [1, 2],而这时再处理第二个2时,如果在[]和[1]后直接加2会产生重复,
所以只能在上一个循环生成的后两个子集合后面加2。所以我们用last来记录上一个处理的数字,然后判定当前的数字和上面的是否相同,
若不同,则循环还是从0到当前子集的个数(此时newsize-size=0);若相同,则新子集个数减去之前循环时子集的个数当做起点来循环,这样就不会产生重复了。

class Solution {
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        //初始化二维vector为:[[]]
        vector<vector<int> > ans(1);
        sort(nums.begin(),nums.end());
        int last=nums[0],size=1;
        for(int i=0;i<nums.size();++i)
        {
            if(last!=nums[i])
            {
                last=nums[i];
                size=ans.size();
            }
            int newsize=ans.size();
            // 限制相等的元素出现重叠情况,确定需要追加新元素的vector
            for(int j=newsize-size;j<newsize;++j)
            {
                //在上一轮的集合中加入新的元素
                ans.push_back(ans[j]);
                ans.back().push_back(nums[i]);
            }
        }
        return ans;
        
        
    }
};

  

 

22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

 

示例:

输入:n = 3
输出:[
       "((()))",
       "(()())",
       "(())()",
       "()(())",
       "()()()"
     ]

 

1、一个「合法」括号组合的左括号数量一定等于右括号数量,这个很好理 解。
2、对于一个「合法」的括号字符串组合 p ,必然对于任何 0 <= i < len(p) 都有:子串 p[0..i] 中左括号的数量都大于或等于右括号的数量。


方法一:暴力法

生成所有的可能的括号序列,然后判断序列是否满足条件;
为了检查序列是否有效,我们遍历这个序列,并使用一个变量 balance 表示左括号的数量减去右括号的数量。
如果在遍历过程中 balance 的值小于零,或者结束时 balance 的值不为零,那么该序列就是无效的,否则它是有效的。

 

方法二:回溯算法
方法一还有改进的余地:我们可以只在序列仍然保持有效时才添加 '(' or ')',而不是像 方法一 那样每次添加。
我们可以通过跟踪到目前为止放置的左括号和右括号的数目来做到这一点,

如果左括号数量不大于 n,我们可以放一个左括号。如果右括号数量小于左括号的数量,我们可以放一个右括号。

 

class Solution {
    void backtrack(vector<string>& ans, string& cur, int open, int close, int n) {
        // 回溯算法套路,路径、选择列表、结束条件
        
        //满足结束条件
        if (cur.size() == n * 2) {
            ans.push_back(cur);
            return;
        }
    
        if (open < n) {
            //做出选择
            cur.push_back('(');
            //回溯
            backtrack(ans, cur, open + 1, close, n);
            //撤销选择
            cur.pop_back();
        }
        if (close < open) {
            cur.push_back(')');
            backtrack(ans, cur, open, close + 1, n);
            cur.pop_back();
        }
    }
public:
    vector<string> generateParenthesis(int n) {
        vector<string> result;
        string current;
        backtrack(result, current, 0, 0, n);
        return result;
    }
};

  

 

 

 

39. 组合总和

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。
解集不能包含重复的组合。 
示例 1:

输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:

输入:candidates = [2,3,5], target = 8,
所求解集为:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

回溯法解题步骤:

判断当前情况是否非法,如果非法就立即返回;
当前情况如果满足,则保存该结果或方法;
当前情况下,遍历所有可能出现的情况并进行下一步尝试;
递归完毕后进行回溯,回溯的方法是取消上一步的尝试。

 

class Solution {
private:
    vector<vector <int>> results;
    vector<int> solution;
public:
    void backtracking(vector<int> candidates, int target, int start){
        if(target < 0)
            return;
        if(target == 0){
            results.push_back(solution);
            return;
        }
        for(int i=start; i<candidates.size() && candidates[i]-target<=0; i++){
            solution.push_back(candidates[i]);
            backtracking(candidates, target-candidates[i], i);
            solution.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        // 先进性排序
        std::sort(candidates.begin(), candidates.end());
        // 回溯
        backtracking(candidates, target, 0);
        return results;        
    }
};

  

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

说明:

所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。 
示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
  [1,2,2],
  [5]
]

 与39. 组合总和/C++不同的地方有2点:

1)要去重
2)递归时,要用i+1,因为已经使用过candidates[i]了,不能重复使用
以下介绍两种方法去重:
第一种方法是在重复组合生成前就处理掉,这种方法效率较高。先对原数组排序,使得相同元素聚集在一起。如果发现2个相邻元素相同,则直接跳过。

class Solution {
private:
    vector<vector <int>> results;
    vector<int> solution;
public:
    void backtracking(vector<int> candidates, int target, int start){
        if(target < 0)
            return;
        if(target == 0){
            results.push_back(solution);
            return;
        }
        for(int i=start; i<candidates.size() && candidates[i]-target<=0; i++){
            if(i>start){
                if(candidates[i-1]==candidates[i]) continue;
            }
            solution.push_back(candidates[i]);
            backtracking(candidates, target-candidates[i], i+1);
            solution.pop_back();
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        // 先进性排序
        std::sort(candidates.begin(), candidates.end());
        // 回溯
        backtracking(candidates, target, 0);
        return results;        
    }
};

  

 

216. 组合总和 III

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

所有数字都是正整数。
解集不能包含重复的组合。
示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]

思路:可以初始化一个候选数组,然后将问题转化成组合总和 II 类似的问题,只需要再增加一个约束条件,判断产出的组合的元素数量为K

 

class Solution {
private:
    vector<vector <int>> results;
    vector<int> solution;
public:
    void backtracking(vector<int> candidates, int target, int start, int k){
        if(target < 0 )
            return;
        if(target == 0 && solution.size()==k){
            results.push_back(solution);
            return;
        }
        for(int i=start; i<candidates.size() && candidates[i]-target<=0; i++){
            if(i>start){
                if(candidates[i-1]==candidates[i]) continue;
            }
            solution.push_back(candidates[i]);
            backtracking(candidates, target-candidates[i], i+1,k);
            solution.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum3(int k, int n) {
        vector<int> candidates={1,2,3,4,5,6,7,8,9};
        backtracking(candidates, n, 0,k);
        return results; 

    }
};

  

 

377. 组合总和 Ⅳ

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

nums = [1, 2, 3]
target = 4

所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。
进阶:
如果给定的数组中含有负数会怎么样?
问题会产生什么变化?
我们需要在题目中添加什么限制来允许负数的出现?

 

思路:这里其实是找出所有可能的排列,所以组合总和问题I只需要修改start起始位置即可;
这里不需要用 start 参数排除已选择的数字。

但这种方法,因为存在大量子问题的重复计算,所以复杂度高,运行超时;

 

class Solution {
private:
    vector<vector <int>> results;
    vector<int> solution;
public:
    void backtracking(vector<int> candidates, int target, int start){
        if(target < 0)
            return;
        if(target == 0){
            results.push_back(solution);
            return;
        }
        for(int i=start; i<candidates.size() && candidates[i]-target<=0; i++){
            solution.push_back(candidates[i]);
            backtracking(candidates, target-candidates[i], 0);
            solution.pop_back();
        }
    }
    int combinationSum4(vector<int>& nums, int target) {
        // 先进行排序
        std::sort(nums.begin(), nums.end());
        // 回溯
        backtracking(nums, target, 0);
        return results.size();        
    }
};

  

动态规划方法:

因为题目不需要列出所有组合,只需要计算出组合数。因此不需要使用递归来进行搜索。
本题其实可以看做是一个完全背包问题。
关于01背包与完全背包可以参考此处。
背包问题详解

此题的状态转移方程为dp[i] = dp[i-nums[0]]+dp[i-nums[1]]+...dp[i-nums[len-1]],条件为i>=nums[j];
dp[0] = 1,dp[0]表示组成0,一个数都不选就可以了,所以dp[0]=1
举个例子。假设nums={1,2,3}; target = 4

dp[4] = dp[4-1]+dp[4-2]+dp[4-3] = dp[3]+dp[2]+dp[1]

dp[1] = dp[0] = 1;
dp[2] = dp[1]+dp[0] = 2;
dp[3] = dp[2]+dp[1]+dp[0] = 4;
dp[4] = dp[4-1]+dp[4-2]+dp[4-3] = dp[3]+dp[2]+dp[1] = 7

 【零钱兑换问题中dp[i]为dp[i-nums[j]]的最小值,而这道题中dp[i]为dp[i-nums[j]]之和,这是它们的区别。】

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        int len = nums.size();
        if(len==0)
            return 0;
        vector<unsigned long long> dp(target+1,0);
        dp[0] = 1;
        for(int i = 1;i<=target;i++){
            for(int j = 0;j<len;j++){
                //当i==nums[j]时,以nums[j]为结尾的所有排列就一个啊,所以可知直接使dp[0]=1实现这一目的
                if(i-nums[j]>=0){
                    dp[i] +=  dp[i-nums[j]];
                }
            }
        }
        return dp[target];
    }
};

  

 

 

79. 单词搜索
给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

 

示例:

board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]

给定 word = "ABCCED", 返回 true
给定 word = "SEE", 返回 true
给定 word = "ABCB", 返回 false

给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

思路:利用回溯法,从二维网格的每一个字符出发,从上下左右四个方向看是否能够找到最后组成word的相邻字母,注意的就是一点,
用过的字母我们将其设置为空格,如果此路不通,那么回溯的时候记得将用过的字母从空格设置回去,具体的看代码注释。

一道搜索的题目,有点类似走迷宫,只不过按照给定单词字母顺序来寻找,遍历board中每一个元素,判断与word中的第一个字母是否相同,
如果相同则在当前位置上去搜索上下左右相邻的单元格的元素是否和当前字母的下一个字母相同,不在搜索范围内或者字母不同就返回false,
当搜索的字母数等于word的长度时,也就表明在board找到了这个word。注意每次判断一个字母要标记当前位置以搜索过,以防止字母重复利用。
我选择直接更改board元素,以便在后续的判断中不会重复判断此位置,在搜索结束后改回来就可以了。

 

class Solution {
public:
    bool exist(vector<vector<char>>& board, string word) {
        h = board.size();
        w = board[0].size();
        for(int i = 0; i < h; i++){
            for(int j = 0; j < w; ++j){
                //尝试从每一个字母开始回溯
                if(searchexist(board, word, 0, i, j)) return true;
            }
        }
        return false;
    }
    int searchexist(vector<vector<char>>& board, string &word, int n, int x, int y){
        if(x < 0 || x > h-1 || y < 0 || y > w-1 || word[n] != board[x][y])
            return 0;
        //达成要求返回1
        if(n == word.length()-1)
            return 1;
        //将用过的保存起来
        char temp = board[x][y];
        board[x][y] = 0;
        //四个方回溯
        int flag = searchexist(board, word, n+1, x+1, y)
                 ||searchexist(board, word, n+1, x-1, y)
                 ||searchexist(board, word, n+1, x, y+1)
                 ||searchexist(board, word, n+1, x, y-1);
        //回溯失败,将其重置
        board[x][y] = temp;
        return flag;
    }
private:
    int h, w;
};

  

 

 

 

2、解题套路:

常见的背包问题有

1、组合问题。

2、True、False问题。

3、最大最小问题。

 

例如:

1、组合问题:
377. 组合总和 Ⅳ
494. 目标和
518. 零钱兑换 II


2、True、False问题:
139. 单词拆分
416. 分割等和子集


3、最大最小问题:
474. 一和零
322. 零钱兑换

 

组合问题公式:

dp[i] += dp[i-num]

 

True、False问题公式:

dp[i] = dp[i] or dp[i-num]

 

最大最小问题公式

dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)

以上三组公式是解决对应问题的核心公式。

 

当然拿到问题后,需要做到以下几个步骤:
1.分析是否为背包问题。
2.是以上三种背包问题中的哪一种。
3.是0-1背包问题还是完全背包问题。也就是题目给的nums数组中的元素是否可以重复使用。
4.如果是组合问题,是否需要考虑元素之间的顺序。需要考虑顺序有顺序的解法,不需要考虑顺序又有对应的解法。

接下来讲一下背包问题的判定
背包问题具备的特征:给定一个target,target可以是数字也可以是字符串,再给定一个数组nums,nums中装的可能是数字,也可能是字符串,

问:能否使用nums中的元素做各种排列组合得到target。

 

 

背包问题技巧:
1.如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序;

for num in nums:
  for i in range(target, nums-1, -1):


2.如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。

for num in nums:
  for i in range(nums, target+1):


3.如果组合问题需考虑元素之间的顺序,需将target放在外循环,将nums放在内循环。

for i in range(1, target+1):
  for num in nums:

 

组合总和 Ⅳ

代码

class Solution:
  def combinationSum4(self, nums: List[int], target: int) -> int:
    f not nums:
      return 0
    dp = [0] * (target+1)
    dp[0] = 1
    for i in range(1,target+1):
      for num in nums:
        if i >= num:
          dp[i] += dp[i-num]
    return dp[target]

作者:Jackie1995
链接:https://leetcode-cn.com/problems/combination-sum-iv/solution/xi-wang-yong-yi-chong-gui-lu-gao-ding-bei-bao-wen-/

 

BFS 思想及解题框架:

其实 DFS 算法就是回溯算法,解题框架参考回溯算法😊
BFS 的核心思想应该不难理解的,就是把一些问题抽象成图,从一个点开 始,向四周开始扩散。
一般来说,我们写 BFS 算法都是用「队列」这种数 据结构,每次将一个节点周围的所有节点加入队列。
BFS 相对 DFS 的最主要的区别是:BFS 找到的路径一定是最短的,但代价 就是空间复杂度比 DFS 大很多,


要说框架的话,我们先举例一下 BFS 出现的常⻅场景好吧,问题的本质就是:
让你在一幅「图」中找到从起点 start 到终点 target 的最近距离,
这个例子听起来很枯燥,但是 BFS 算法问题其实都是在干这个事儿;

 

BFS 解题框架:

// 计算从起点 start 到终点 target 的最近距离 
int BFS(Node start, Node target) {
    // 核心数据结构
    Queue<Node> q;  
    

    // 避免走回头路
    Set<Node> visited; 

    // 将起点加入队列 visited.add(start);
    q.offer(start); 

    // 记录扩散的步数
    int step = 0; 
    while (q not empty) {
        int sz = q.size();
        /* 将当前队列中的所有节点向四周扩散 */ 
        for (int i = 0; i < sz; i++) {
            Node cur = q.poll();

            /* 划重点:这里判断是否到达终点 */
            if (cur is target)
                return step;
            /* 将 cur 的相邻节点加入队列 */ 
            for (Node x : cur.adj())
                if (x not in visited) { 
                    q.offer(x);
                    visited.add(x); 
                }
        }
        /* 划重点:更新步数在这里 */
        step++; 
    }
}

  

队列 q 就不说了,BFS 的核心数据结构;
cur.adj() 泛指 cur 相邻的节 点,比如说二维数组中, cur 上下左右四面的位置就是相邻节点;
visited 的主要作用是防止走回头路,大部分时候都是必须的,但是 像一般的二叉树结构,没有子节点到父节点的指针,
不会走回头路就不需要visited ;

 

例子:

1、二叉树的最小深度

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明: 叶子节点是指没有子节点的节点。

示例:

给定二叉树 [3,9,20,null,null,15,7],

3
/ \
9 20
/ \
15 7
返回它的最小深度  2.


//二叉树的最小深度

解题思路:怎么套到 BFS 的框架里呢?
首先明确一下起点 start 和终点 target 是什 么,怎么判断到达了终点?
显然起点就是 root 根节点,终点就是最靠近根节点的那个「叶子节点」 嘛,叶子节点就是两个子节点都是 null 的节点;
那么,按照我们上述的框架稍加改造来写解法即可:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int minDepth(TreeNode* root) {
        if(root==NULL){
            return 0;
        }
        queue<TreeNode*> q;
        q.push(root);
        // root 本身就是一层,depth 初始化为 1
        int depth = q.size();

        while(!q.empty()){
            int s = q.size();
            /* 将当前队列中的所有节点向四周扩散 */
            for(int i =0;i<s;i++){
                /* 判断是否到达终点 */
                TreeNode* temp=q.front();
                q.pop();
                if(temp->left==NULL && temp->right==NULL){
                    return depth;
                }

                /* 将相邻节点加入队列 */
                if(temp->left!=NULL){
                    q.push(temp->left);
                }
                if(temp->right!=NULL){
                    q.push(temp->right);
                }
            }

            /* 这里增加步数 */
            depth++;
        }
        return depth;
    }
};

1、为什么 BFS 可以找到最短距离,DFS 不行吗?
首先,你看 BFS 的逻辑, depth 每增加一次,队列中的所有节点都向前迈一步,这保证了第一次到达终点的时候,走的步数是最少的。
DFS 不能找最短路径吗?其实也是可以的,但是时间复杂度相对高很多。
DFS 实际上是靠递归的堆栈记录走过的路径,你要找到最短路径,肯定得把二叉树中所有树杈都探索完才能对比出最短的路径有多⻓,
而 BFS 借助队列做到一次一步「⻬头并进」,是可以在不遍历完整棵 树的条件下找到最短距离的。

形象点说,DFS 是线,BFS 是面;DFS 是单打独斗,BFS 是集体行动。这个应该比较容易理解吧。

2、既然 BFS 那么好,为啥 DFS 还要存在?

BFS 可以找到最短距离,但是空间复杂度高,而 DFS的空间复杂度较低。
还是拿刚才我们处理二叉树问题的例子,假设给你的这个二叉树是满二叉 树,节点数为 N ,对于DFS算法来说,空间复杂度无非就是递归堆栈,
最坏情况下顶多就是树的高度,也就是 O(logN) 。
但是你想想 BFS 算法,队列中每次都会储存着二叉树一层的节点,这样的 话最坏情况下空间复杂度应该是树的最底层节点的数量,
也就是 N/2 ,用 BigO表示的话也就是 O(N) 。由此观之,BFS 还是有代价的,一般来说在找最短路径的时候使用 BFS,
其他时候还是 DFS 使用得多一些(主要是递归代码好写)。

 

 

752. 打开转盘锁
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有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
解释:
无法旋转到目标数字且不被锁定。
示例 4:

输入: deadends = ["0000"], target = "8888"
输出:-1

class Solution {
public:

    string plusOne(string str,int i)//数字+1
    {
        str[i] = str[i]=='9'?'0':str[i]+1;
        return str;
    }

    string downOne(string str,int i)//数字-1
    {
        str[i] = str[i]=='0'?'9':str[i]-1;
        return str;
    }


    int openLock(vector<string>& deadends, string target) {
        unordered_set<string>deadset(deadends.begin(),deadends.end());//死亡密码

        unordered_set<string>visited;//走过的密码
        visited.insert("0000");

        queue<string>q;//当前的密码
        q.push("0000");

        int step=0;

        while(!q.empty())
        {
            int size = q.size();//当前这一批次,广度优先遍历,一批(一层)一批的走
            for(int i=0;i<size;i++)
            {
                string cur = q.front();//这一批次,当前密码
                q.pop();

                if(deadset.count(cur)) continue; //在死亡密码中,换下一个
                if(cur==target) return step;    //到达目标,结束

                for(int i=0;i<4;i++)    //每次转动,有(上、下)*4 种可能
                {
                    string up = plusOne(cur,i);  //向上
                    if(!visited.count(up))       //没有走过
                    {
                        q.push(up);              //放入当前密码(下一批次计算)
                        visited.insert(up);      //放入走过密码
                    }
                    string down = downOne(cur,i); //向下
                    if(!visited.count(down))      //没有走过
                    {
                        q.push(down);
                        visited.insert(down);
                    }
                }
            }
            step++;
        }
        return -1;
    }
};

  

 解法2:双向BFS

BFS 算法还有一种稍微高级一点的优化思路:双向 BFS,可以进一步提高算法的效率。
传统的 BFS 框架就是从起点开始向四周扩散,遇到终点时停止;
而双向 BFS 则是从起点和终点同时开始扩散,当两边有交集的时候停止。

 

class Solution {
public:
    int openLock(vector<string>& deadends, string target) {
        // 记录需要跳过的死亡密码
        unordered_set<string> deads(deadends.begin(), deadends.end());
        // 记录已经穷举过的秘密,防止走回头路
        unordered_set<string> visited;
        // 用集合不用队列,可以快速判断元素是否存在
        // queue<string> q;
        unordered_set<string> q1;
        unordered_set<string> q2;

        int step = 0;
        q1.insert("0000");
        q2.insert(target);

        while(!q1.empty() && !q2.empty()) {
            // 
            unordered_set<string> q;
            int sz = q1.size();
            // 将当前队列中的所有节点向四周扩散
            for (string str:q1) {
                // 判断是否到达终点
                if (deads.count(str)) continue;
                if (q2.count(str)) return step;

                visited.insert(str);

                // 将一个节点的未遍历相邻结点加入队列
                for (int i = 0; i < 4; i++) {
                    string up = plusOne(str, i);
                    if (!visited.count(up)) {
                        q.insert(up);
                    }
                    string down = minusOne(str, i);
                    if (!visited.count(down)) {
                        q.insert(down);
                    }
                }
            }

            // q 相当于 q1
            // 这里交换 q1 q2,下一轮 while 就是扩散 q2 

            q1 = q2;
            q2 = q;

            // 在这里增加步数
            step++;
        }
        // 如果穷举完都没有找到目标密码,那就是找不到了
        return -1;
    }
private:
    string plusOne(string s, int j) {
        if (s[j] == '9') {
            s[j] = '0';
        } else {
            s[j] = s[j] + 1;
        }
        return s;
    }

    string minusOne(string s, int j) {
        if (s[j] == '0') {
            s[j] = '9';
        } else {
            s[j] = s[j] - 1;
        }
        return s;
    }
};

  

 

 

 

  

 

posted @ 2020-09-21 17:01  静悟生慧  阅读(319)  评论(0编辑  收藏  举报