LeetCode算法练习——回溯&&深搜(一)

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯在迷宫搜索中使用很常见,就是这条路走不通,然后返回前一个路口,继续下一条路。回溯算法说白了就是穷举法。不过回溯算法使用剪枝函数,剪去一些不可能到达 最终状态(即答案状态)的节点,从而减少状态空间树节点的生成。

回溯法是一个既带有系统性又带有跳跃性的的搜索算法。它在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。 

解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:

  • 路径:也就是已经做出的选择。
  • 选择列表:也就是你当前可以做的选择。
  • 结束条件:也就是到达决策树底层,无法再做选择的条件。

代码框架如下:

    vector<vector<int>> res;
    vector<int> path;
    void dfs(路径, 选择列表) {
        if (满足条件结束) {
            res.push_back(path);
            return;
        }
        for (循环条件) {
            if (!isValid(参数))    continue;        //剪枝去重
            path.push_back(参数);                   //做选择
            dfs(路径, 选择列表);
            path.pop_back();                       //回溯
        }
    }

LeetCode77. 组合

给定两个整数 nk,返回 1 ... n 中所有可能的 k 个数的组合。

示例:

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

此题完全套用模板即可,无需做多余的判断,k 限制了树的高度,n 限制了树的宽度,值得注意的是dfs(nums, i + 1, tmp)中的i + 1确保了数组中的元素不会被重复选择,代码如下:

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> combine(int n, int k) {
        if (k <= 0 || n <= 0) return res;
        dfs(n, k, 1);
        return res;
    }

    void dfs(int n, int k, int start) {
        if (path.size() == k) {
            res.push_back(path);
            return;
        }
        for (int i = start; i <= n; i++) {
            path.push_back(i);      // 做选择
            dfs(n, k, i + 1);
            path.pop_back();        // 撤销选择
        }
    }
};

LeetCode39. 组合总和

给定一个无重复元素的数组 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]
]

此题中的元素是可以重复被选择的,而我们每次计算时,需要用目标数的和减去数组内的数值,并不停地深搜遍历直至差为0结束递归,关键递归函数为:dfs(i, target - candidates[i], candidates)。

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
public:
    void dfs(int start, int target, vector<int>& candidates){
        if (target == 0) {
            res.push_back(path);
            return;
        }
        for(int i = start; i < candidates.size() && target - candidates[i] >= 0; i++){
            path.push_back(candidates[i]);
            dfs(i, target - candidates[i], candidates);
            path.pop_back();
        }
    }
    
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
	sort(candidates.begin(), candidates.end());
        dfs(0, target, candidates);
        return res;
    }
};

LeetCode40. 组合总和 II

此题条件改变了:candidates 数组中的每个数字在每个组合中只能使用一次,同时数组内包含了相同元素。我们只需要增加两条语句进行处理即可。

        for(int i = start; i < candidates.size() && target - candidates[i] >= 0; i++){
            if(i > start && candidates[i] == candidates[i - 1])  continue;  
            //candidate容器内相同值的处理
            path.push_back(candidates[i]);
            dfs(i + 1, target - candidates[i], candidates);                 
            //元素不可重复利用,使用下一个即i+1  
            path.pop_back();
        }

LeetCode216. 组合总和 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]]

此题candidates需要自己定义,同时也多了很多边界条件,即k和n的取值是否合理以及递归结束条件,代码如下:

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

LeetCode17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例:

输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

此题的思路与组合1类似,只不过引入了电话号码与字符的映射,代码如下:

class Solution {
public:
    //1. 用map记录每个数字按键对应的所有字母
    map<char, string> maps = {
        {'2', "abc"}, {'3', "def"}, {'4', "ghi"}, {'5', "jkl"}, {'6', "mno"},
        {'7', "pqrs"}, {'8', "tuv"}, {'9', "wxyz"}
    };
    //2. 存储最终结果和临时结果的变量
    vector<string> res;
    string path;
    vector<string> letterCombinations(string digits) {
        if(digits.size() == 0) {
            return res;
        }
        dfs(0, digits);
        return res;
    }
    //3. dfs函数递归遍历
    void dfs(int n, string digits) {
        if(n == digits.size()) {
            res.push_back(path);
            return;
        }
        for(int i = 0; i < maps[digits[n]].size(); i++) {
            path.push_back(maps[digits[n]][i]);    
            dfs(n + 1, digits);                         
            //以在当前位置压入该字母这一“情况”为前提,构造此“分支”的后续结果
            path.pop_back();  //状态还原,例如临时结果从 "ab" -> "a",下一次循环尝试"ac" 
        }
    }
};

LeetCode78. 子集 && LeetCode90. 子集 II

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

子集1示例:                        子集2示例:                

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

此题的不同点在于当输入数组有重复数字时,子集1会出现重复的子集,而子集2则不会。解题思路套用模板,代码如下:

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        if(nums.size() == 0)    return res;
        sort(nums.begin(),nums.end());
        dfs(nums, 0);
        return res;
    }
    void dfs(vector<int>& nums, int start){
        res.push_back(path);
        for(int i = start; i < nums.size(); i++){
            if(i > start && nums[i] == nums[i - 1]) continue;
            //剪枝去重,去掉上述语句即为子集1的答案   
            path.push_back(nums[i]);         //选择
            dfs(nums, i + 1);                //遍历
            path.pop_back();                 //回溯
        }
    }
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值