回溯-剪枝

回溯算法也可以认为是某种程度的暴力穷举,但是在暴力穷举的过程中,可以根据解的相关性质将不满足条件的解及时剪枝

目录

相关例题

 括号生成

 17. 电话号码的字母组合

39. 组合总和

46. 全排列

47. 全排列 II

78. 子集

139. 单词拆分

301. 删除无效的括号

40. 组合总和 II


相关例题

 括号生成

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

示例 1:

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

class Solution {
public:
    vector vec;
    void dfs(string str, int left, int right, int n){
        /*对此题的理解:在于以下两种情况严格保证了其为有效的括号
        str表示已经构成的字符串、left与right分别表示已经生成的左右括号数
        1.左括号数小于n,可以加'('
        2.右括号数小于左括号数,可以加')'
        */
        //递归结束
        if(left == n && right == n){
            vec.insert(vec.begin(), str);
            return;
        }
        if(left < n){
            str.push_back('(');
            dfs(str, left+1, right, n);
            str.pop_back();
        }
        if(right < left){
            str.push_back(')');
            dfs(str, left, right+1, n);
            str.pop_back();
        }  
    }
    vector generateParenthesis(int n) {
        dfs("", 0, 0, n);
        return vec;
    }
};

 17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
class Solution {
public:
    vector vec;
    void dfs(string res, string out, map digit_word){
        if(res.length() == 1){
            char c = res[0];
            for(int i = 0; i < digit_word[c].length(); i++){
                vec.insert(vec.end(), out+digit_word[c][i]);
            }
            return;
        }
        else{
            char c = res[0];
            for(int i = 0; i < digit_word[c].length(); i++){
                dfs(res.substr(1), out+digit_word[c][i], digit_word);
            }
        }
    }
    vector letterCombinations(string digits) {
        map digit_word;  
        digit_word.insert(pair('2', "abc"));
        digit_word.insert(pair('3', "def"));
        digit_word.insert(pair('4', "ghi"));
        digit_word.insert(pair('5', "jkl"));
        digit_word.insert(pair('6', "mno"));
        digit_word.insert(pair('7', "pqrs"));
        digit_word.insert(pair('8', "tuv"));
        digit_word.insert(pair('9', "wxyz"));
        dfs(digits, "", digit_word);
        return vec;
    }
};

39. 组合总和

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

candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。 

对于给定的输入,保证和为 target 的唯一组合数少于 150 个。

示例 1:

输入: candidates = [2,3,6,7], target = 7
输出: [[7],[2,2,3]]

class Solution {
public:
    vector> vec;
    void dfs(vector& candidates, int index, int target, vector& now){
        if(target == 0){
            vec.push_back(now);
            return;
        }
        else{
            for(int i = index; i < candidates.size(); i++){
                if(candidates[i] <= target){
                    now.push_back(candidates[i]);
                    dfs(candidates, i, target-candidates[i], now);
                    now.pop_back();
                }
            }
            /*不能找到满足插入条件的元素即回退,如candidates = [2,3,6,7], target = 7
            此时已经选择的元素为6,接下来无元素可选,直接return即可
            */
            return;
        }
    }
    vector> combinationSum(vector& candidates, int target) {
        vector now;
        dfs(candidates, 0, target, now);
        return vec;
    }
};

46. 全排列

46. 全排列

难度中等1878

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

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

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

class Solution {
public:
    vector> vec;
    void dfs(vector& now, vector nums, vector& vis){
        if(now.size() == nums.size()){
            //所有元素都已经被访问过,此时应该插入
            vec.push_back(now);
            return;
        }
        for(int i = 0; i < nums.size(); i++){
            if(!vis[i]){
                vis[i] = true;
                now.push_back(nums[i]);
                dfs(now, nums, vis);
                now.pop_back();
                vis[i] = false;
            }
        }
    }
    vector> permute(vector& nums) {
        vector vis;
        vector now;
        vis.resize(nums.size(), false);
        dfs(now, nums, vis);
        return vec;
    }
};

47. 全排列 II

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]
示例 2:

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

 遇到剪枝不清楚如何剪枝的可以手动去模拟即可。

class Solution {
public:
    vector> ans;
    void dfs(vector& nums, vector& vis, vector &now, int& count){
        if(count == nums.size()){
            ans.push_back(now);
            return;
        }
        for(int i = 0; i < nums.size(); i++){
            if(vis[i] || (i > 0 && nums[i] == nums[i-1] && !vis[i-1])){
                //当前元素已经被访问
                //或者当前到第二个重复元素,并且第一个重复元素没有被访问(剪)
                continue;
            }
            vis[i] = true;
            now.push_back(nums[i]);
            count += 1;
            dfs(nums, vis, now, count);
            count -= 1;
            now.pop_back();
            vis[i] = false;
        }
    }
    vector> permuteUnique(vector& nums) {
        /*
        参考:https://leetcode.cn/problems/permutations-ii/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liwe-2/
        回溯剪枝做法:如何去剪枝,可以画画递归执行的过程,方便解题
        对原数组进行排序,方便去重剪枝
        1a 1b 2
        排列:1a 1b 2 与 1b 1a 2为同一个排列
        怎样去剪枝呢:对于1a 1b处于递归的同一深度而言,现在面临着选1a还是1b
        如果我们先选了1a后续对应有了一个排列,构造排列完成之后,继续回到1a 1b那一层(1a 1b都没有被访问过,!vis[i-1] !vis[i])
        现在需要决定对1b进行选择,由前面递归状态可知,1b选择后和1a选择后的状态一样,要剪枝
        */
        sort(nums.begin(), nums.end());
        vector vis(nums.size(), false);
        int count = 0;
        vector now;
        dfs(nums, vis, now, count);
        return ans;
    }
};

78. 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

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

输入:nums = [0]
输出:[[],[0]]

还是按照深度优先搜索的思想,只不过在这里在搜索时,可以之间将中间搜索的过程直接作为结果。

class Solution {
public:
    vector> ans;
    void dfs(vector nums, vector& now, int index){
        //可以保存中间状态直接压入栈中
        ans.push_back(now);
        if(index == nums.size())
            return;
        for(int i = index; i < nums.size(); i++){
            now.push_back(nums[i]);
            dfs(nums, now, i + 1);
            now.pop_back();
        }
    }
    vector> subsets(vector& nums) {
        //另外一种解法直接可以认为该题直接对应枚举0-2^n之间所有数的二进制形式
        vector now;
        dfs(nums, now, 0);
        return ans;
    }
};

139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
     注意,你可以重复使用字典中的单词。
示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

class Solution {
public:
    bool dfs(string s, int start, vector wordDict, vector& isContained, vector& isVisited){
        //深度优先遍历,遍历开始坐标为start,该段子串是否能够被字典中的单词所拼接
        if(start == s.length()) return true;//已经遍历到结尾处,说明其肯定可以被拼接
        // 若已经被遍历过,直接返回即可
        if(isVisited[start]) return isContained[start];
        else{
            isVisited[start] = true;
            for(int i = start; i < s.length(); i++){
                string key = s.substr(start, i - start + 1);
                if (find(wordDict.begin(), wordDict.end(), key) != wordDict.end() && dfs(s, i+1, wordDict, isContained, isVisited)){
                    isContained[start] = true;
                    return true;
                } 
            }
            isContained[start] = false;
            return false;
        }
    }
    bool wordBreak(string s, vector& wordDict) {
        //使用dfs进行回溯,对字符串进行遍历,判断s[0:i]是否在wordDict中,而后对s[i+1: len-1]继续dfs遍历
        //注意保存中间状态,以防搜索空间爆炸
        vector isContained(s.length(), false);
        vector isVisited(s.length(), false);
        return dfs(s, 0, wordDict, isContained, isVisited);
    }
};

301. 删除无效的括号

给你一个由若干括号和字母组成的字符串 s ,删除最小数量的无效括号,使得输入的字符串有效。

返回所有可能的结果。答案可以按 任意顺序 返回。

示例 1:

输入:s = "()())()"
输出:["(())()","()()()"]
示例 2:

输入:s = "(a)())()"
输出:["(a())()","(a)()()"]
示例 3:

输入:s = ")("
输出:[""]

一些题解思路:我们可以得到需要剔除的最少的括号数以及目标子串的最终长度,以及左右括号再枚举过程中能否配对,形成一些剪枝条件。

class Solution {
public:
    string str;
    int remain;
    int max_len=0;
    vector vec;//结果
    vector removeInvalidParentheses(string s) {
        //使用回溯、剪枝算法实现
        str = s;
        string ans = "";
        //左右括号数用来剪枝
        isValid(str);
        max_len = str.length() - remain;
        dfs(0, ans, 0, 0, 0);
        set st(vec.begin(), vec.end());
        vec.assign(st.begin(), st.end());
        return vec;
    }
    void dfs(int i, string ans, int len, int left_count, int right_count){
        //按照左右括号数以及子串长度进行剪枝
        //对当前索引选取到i的这段字符串,进行判断,left_count左括号数量,right_count右括号数量
        if(i == str.length() || len == max_len){
            //已经遍历枚举结束或者已经达到最大长度,可以判断当前字符串是否满足情况
            // cout << ans << endl;
            if(len == max_len && isValid(ans))
                    vec.push_back(ans);
            return;
        }else{
            if(left_count < right_count)
            //剪枝条件:左括号数目已经少于右括号,后续左括号不可能继续满足配对条件
            //当前长度+剩余可选择长度小于最大长度或者大于最大长度,直接剪枝
                return;
            dfs(i+1, ans, len, left_count, right_count);//不选择当前第i个字符
            string now;
            now.push_back(str[i]);
            ans += now;
            if(str[i] == '(')
                dfs(i+1, ans, len+1, left_count+1, right_count);
            else if(str[i] == ')')
                dfs(i+1, ans, len+1, left_count, right_count+1);
            else
                dfs(i+1, ans, len+1, left_count, right_count);
        }
    }
    bool compare(char a, char b){
        //a为栈顶元素,b为要入栈的元素
        if(a == '(' && b == ')')
            return true;
        return false;
    }
    bool is_zimu(char a){
        //判断当前是否为字母,字母直接跳过
        if(a == '(' || a == ')')
            return false;
        else
            return true;
    }
    bool isValid(string s){
        //对字符串s判断其是否合法
        stack st;
        for(int i = 0; i < s.length(); i++){
            if(is_zimu(s[i]))
                continue;
            if(!st.empty() && compare(st.top(), s[i]))
            //栈非空且刚好配对
                st.pop();
            else
                st.push(s[i]);
        }
        if(st.empty())
            return true;
        else{
            remain = st.size();//记录多余的元素
            return false;
        }  
    }
};

40. 组合总和 II

 

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

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

注意:解集不能包含重复的组合。 

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

#include "bits/stdc++.h"

using namespace std;


class Solution {
public:
    vector> ans;
    void dfs(vector candidates, vector &vis, vector &now ,int index, int &target){
        if(target == 0){
            ans.push_back(now);
            return;
        }
        if(index == candidates.size())
            return;
        for(int i = index; i < candidates.size(); i++){
            if(i-1 >= 0 && candidates[i] == candidates[i-1] && !vis[i-1])
            //在递归遍历的同一层上,出现相等元素,且前一个元素还未被访问,此时会重复访问
                continue;
            if(target - candidates[i] < 0)
                break;
            now.push_back(candidates[i]);
            target -= candidates[i];
            vis[i] = true;
            dfs(candidates, vis, now, i+1, target);
            target += candidates[i];
            now.pop_back();
             vis[i] = false;
        }
    }
    vector> combinationSum2(vector& candidates, int target) {
        vector now;
        vector vis(candidates.size(), false);
        sort(candidates.begin(), candidates.end());//升序
        dfs(candidates, vis, now, 0, target);
        return ans;
    }
};

 

你可能感兴趣的:(leetcode,剪枝,算法,leetcode)