回溯算法也可以认为是某种程度的暴力穷举,但是在暴力穷举的过程中,可以根据解的相关性质将不满足条件的解及时剪枝
目录
相关例题
括号生成
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;
}
};
给定一个仅包含数字 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;
}
};
给定一个无重复元素的正整数数组 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. 全排列
难度中等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;
}
};
给定一个可包含重复数字的序列 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;
}
};
给你一个整数数组 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;
}
};
给你一个字符串 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);
}
};
给你一个由若干括号和字母组成的字符串 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;
}
}
};
给定一个候选人编号的集合 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;
}
};