【ps】本篇有 9 道 leetcode OJ。
目录
一、算法简介
二、相关例题
1)全排列
.1- 题目解析
.2- 代码编写
2)子集
.1- 题目解析
.2- 代码编写
3)找出所有子集的异或总和再求和
.1- 题目解析
.2- 代码编写
4)全排列 II
.1- 题目解析
.2- 代码编写
5)电话号码的字母组合
.1- 题目解析
.2- 代码编写
6)括号生成
.1- 题目解析
.2- 代码编写
7)组合
.1- 题目解析
.2- 代码编写
8)目标和
.1- 题目解析
.2- 代码编写
9)组合总和
.1- 题目解析
.2- 代码编写
一、算法简介
回溯算法是一种经典的递归算法,通常⽤于解决组合问题、排列问题和搜索问题等。
回溯算法的基本思想:从一个初始状态开始,按照⼀定的规则向前搜索,当搜索到某个状态无法前进时,回退到前一个状态,再按照其他的规则搜索。回溯算法在搜索过程中维护一个状态树,通过遍历状态树来实现对所有可能解的搜索。
回溯算法的核心思想:“试错”,即在搜索过程中不断地做出选择,如果选择正确,则继续向前搜索,否则,回退到上一个状态,重新做出选择。回溯算法通常用于解决具有多个解,且每个解都需要搜索才能找到的问题。
// 回溯算法的模板
void dfs(vector<int>& path, vector<int>& choice, ...)
{
// 满⾜结束条件
if (/* 满⾜结束条件 */)
{
// 将路径添加到结果集中
res.push_back(path);
return;
}
// 遍历所有选择
for (int i = 0; i < choices.size(); i++)
{
// 做出选择
path.push_back(choices[i]);
// 做出当前选择后继续搜索
dfs(path, choices);
// 撤销选择
path.pop_back();
}
}
其中, path 表示当前已经做出的选择, choices 表示当前可以做的选择。在回溯算法中,我们需要做出选择,然后递归地调用回溯函数。如果满足结束条件,则将当前路径添加到结果集中。
否则,我们需要撤销选择,回到上一个状态,然后继续搜索其他的选择。回溯算法的时间复杂度通常较高,因为它需要遍历所有可能的解。但是,回溯算法的空间复杂度较低,因为它只需要维护一个状态树。在实际应用中,回溯算法通常需要通过剪枝等方法进行优化,以减少搜索的次数,从而提高算法的效率。
回溯算法是一种非常重要的算法,可以解决许多组合问题、排列问题和搜索问题等。回溯算法的核心思想是搜索状态树,通过遍历状态树来实现对所有可能解的搜索。回溯算法的模板非常简单,但是实现起来需要注意一些细节,比如何做出选择、如何撤销选择等。
二、相关例题
1)全排列
46. 全排列
.1- 题目解析
全排列的过程,其实可以画成一棵决策树,而找出全排列的结果,其实就是对这棵决策树进行 DFS。
DFS 的思路是,循环模仿遍历树的结点,到叶子结点就返回,不是就进入循环。
在下面 for 循环里要考虑这个位置要填哪个数。根据题目要求,我们肯定不能填已经填过的数,因此很容易想到的一个处理手段就是,定义一个标记数组来标记已经填过的数,那么在填这个数的时候,我们遍历题目给定的所有数,如果这个数没有被标记过,我们就尝试填入,并将其标记,继续尝试填下一个位置。而回溯的时候,要撤销这一个位置填的数以及标记,并继续尝试其他没被标记过的数。
.2- 代码编写
class Solution {
vector<vector<int>> ret;
vector<int> path;
bool check[7];
public:
vector<vector<int>> permute(vector<int>& nums) {
dfs(nums);
return ret;
}
void dfs(vector<int>& nums)
{
if(path.size()==nums.size())
{
ret.push_back(path);
return ;
}
for(int i=0;i<nums.size();i++)//枚举每一个在排列开头的数
{
if(check[i]==false)
{
//记录结果并遍历下一层
path.push_back(nums[i]);
check[i]=true;
dfs(nums);
//回到这一层再恢复现场
path.pop_back();
check[i]=false;
}
}
}
};
2)子集
78. 子集
.1- 题目解析
本题有两种解法。
第一种与上一道题类似,根据某一个元素选或不选,将所有的子集穷举出来,然后统计结果即可。
第二种解法则是根据子集中有多少个元素,来将所有的子集穷举出来。
.2- 代码编写
//解法一
class Solution {
vector<vector<int>> ret;
vector<int> path;
public:
vector<vector<int>> subsets(vector<int>& nums) {
dfs(nums,0);
return ret;
}
void dfs(vector<int>& nums,int i)
{
if(i==nums.size())
{
ret.push_back(path);
return;
}
//不选
dfs(nums,i+1);
//选
path.push_back(nums[i]);//记录结果
dfs(nums,i+1);
path.pop_back();//恢复现场
}
};
//解法二
class Solution {
vector<vector<int>> ret;
vector<int> path;
public:
vector<vector<int>> subsets(vector<int>& nums) {
dfs(nums,0);
return ret;
}
void dfs(vector<int>& nums,int pos)
{
ret.push_back(path);//每次进到下一层,都是新结果,都要记录
for(int i=pos;i<nums.size();i++)//枚举下一层
{
path.push_back(nums[i]);//记录结果
dfs(nums,i+1); //进入下一层
path.pop_back(); //回到当前层,恢复现场
}
}
};
3)找出所有子集的异或总和再求和
1863. 找出所有子集的异或总和再求和
.1- 题目解析
这道题只需要在上一道的基础上,稍微改变统计结果的方式即可。
.2- 代码编写
class Solution {
int sum;
int path;
public:
int subsetXORSum(vector<int>& nums) {
dfs(nums,0);
return sum;
}
void dfs(vector<int>& nums,int pos)
{
sum+=path;
for(int i=pos;i<nums.size();i++)
{
path^=nums[i];
dfs(nums,i+1);
path^=nums[i];
}
}
};
4)全排列 II
47. 全排列 II
.1- 题目解析
我们可以直接在上文《全排列》的基础上用 set 对结果去重。
或者,在上文《全排列》的基础上,加入剪枝操作。由于题目不要求返回的排列顺序,因此我们可以对初始状态排序,将所有相同的元素放在各自相邻的位置,方便之后操作。
.2- 代码编写
//解法一:set去重
class Solution {
set<vector<int>> ret;
vector<int> path;
bool cheak[8] = {false};
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
dfs(nums);
vector<vector<int>> tmp(ret.begin(), ret.end());
return tmp;
}
void dfs(vector<int> nums)
{
if(nums.size() == path.size())
{
// if(find(ret.begin(), ret.end(), path) == ret.end())
// ret.push_back(path);
ret.insert(path);
return;
}
for(int i = 0; i < nums.size(); ++i)
{
if(cheak[i] == false) // 如果没有用过
{
path.push_back(nums[i]);
cheak[i] = true;
dfs(nums); // 此时路径已经加上一个了,在让其进入递归
path.pop_back(); // 回溯,恢复现场,(递归往回走了)
cheak[i] = false;
}
}
}
};
//解法二:剪枝,关心不合法的分支
class Solution {
vector<int> path;
vector<vector<int>> ret;
bool check[9];
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
dfs(nums);
return ret;
}
void dfs(vector<int>& nums)
{
if(path.size()==nums.size())
{
ret.push_back(path);
return;
}
for(int i=0;i<nums.size();i++)
{
if(check[i]==true
|| (i!=0 && nums[i]==nums[i-1] && check[i-1]==false))
{
continue;
}
path.push_back(nums[i]);
check[i]=true;
dfs(nums);
path.pop_back();
check[i]=false;
}
}
};
//解法三:剪枝,关心合法的分支
//解法二:剪枝
class Solution {
vector<int> path;
vector<vector<int>> ret;
bool check[9];
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
dfs(nums);
return ret;
}
void dfs(vector<int>& nums)
{
if(path.size()==nums.size())
{
ret.push_back(path);
return;
}
for(int i=0;i<nums.size();i++)
{
if(check[i]==false && (i==0 || nums[i]!=nums[i-1] || check[i-1]==true))//剪枝
{
path.push_back(nums[i]);
check[i]=true;
dfs(nums);
path.pop_back();
check[i]=false;
}
}
}
};
5)电话号码的字母组合
17. 电话号码的字母组合
.1- 题目解析
每一个数字都对应了一串字符,我们可以由此用一个哈希表建立数字和字符之间的映射,以便通过数字找到相应的字符。
而其他过程同上文中的题目,通过画决策树 + DFS 来解决。
.2- 代码编写
class Solution {
string hash[10]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
vector<string> ret;
string path;
public:
vector<string> letterCombinations(string digits) {
if(digits.size()==0)return ret;
dfs(digits,0);
return ret;
}
void dfs(string& digits,int pos)
{
if(pos==digits.size())
{
ret.push_back(path);
return;
}
for(auto ch:hash[digits[pos]-'0'])
{
path.push_back(ch);
dfs(digits,pos+1);
path.pop_back();
}
}
};
6)括号生成
22. 括号生成
.1- 题目解析
.2- 代码编写
class Solution {
int left,right,n;
string path;
vector<string> ret;
public:
vector<string> generateParenthesis(int _n) {
n=_n;
dfs();
return ret;
}
void dfs()
{
if(right==n)
{
ret.push_back(path);
return ;
}
if(left<n)
{
path.push_back('(');left++;
dfs();
path.pop_back();left--;
}
if(right<left)
{
path.push_back(')');right++;
dfs();
path.pop_back();right--;
}
}
};
7)组合
77. 组合
.1- 题目解析
本题是上一道题的变形,画决策树穷举出所有情况即可。
.2- 代码编写
class Solution {
vector<int> path;
vector<vector<int>> ret;
int n,k;
public:
vector<vector<int>> combine(int _n, int _k) {
n=_n,k=_k;
dfs(1);
return ret;
}
void dfs(int start)
{
if(path.size()==k)
{
ret.push_back(path);
return;
}
for(int i=start;i<=n;i++)
{
path.push_back(i);
dfs(i+1);
path.pop_back();
}
}
};
8)目标和
494. 目标和
.1- 题目解析
.2- 代码编写
class Solution {
int ret,aim;
public:
int findTargetSumWays(vector<int>& nums, int target) {
aim=target;
dfs(nums,0,0);//参数:原始数组、当前下标位置、决策树某一条路径之和
return ret;
}
void dfs(vector<int>& nums,int pos,int path)
{
if(pos==nums.size())
{
if(path==aim)ret++;//统计结果
return;
}
dfs(nums,pos+1,path+nums[pos]);//穷举加
dfs(nums,pos+1,path-nums[pos]);//穷举减
}
};
9)组合总和
39. 组合总和
.1- 题目解析
.2- 代码编写
//解法一:枚举每个值之和
class Solution {
int aim;
vector<int> path;
vector<vector<int>> ret;
public:
vector<vector<int>> combinationSum(vector<int>& nums, int target) {
aim=target;
dfs(nums,0,0);
return ret;
}
void dfs(vector<int>& nums,int pos,int sum)
{
if(sum==aim)
{
ret.push_back(path);
return;
}
if(sum>aim || pos==nums.size())return;//回溯
for(int i=pos;i<nums.size();i++)
{
path.push_back(nums[i]);
dfs(nums,i,sum+nums[i]);
path.pop_back();
}
}
};
//解法二:枚举每个值的个数
class Solution {
int aim;
vector<int> path;
vector<vector<int>> ret;
public:
vector<vector<int>> combinationSum(vector<int>& nums, int target) {
aim=target;
dfs(nums,0,0);
return ret;
}
void dfs(vector<int>& nums,int pos,int sum)
{
if(sum==aim)
{
ret.push_back(path);
return;
}
if(sum>aim || pos==nums.size())return;//回溯
for(int k=0;k*nums[pos]<=aim;k++) //枚举个数
{
if(k)path.push_back(nums[pos]);
dfs(nums,pos+1,sum+k*nums[pos]);
}
for(int k=1;k*nums[pos]<=aim;k++)
{
path.pop_back();
}
}
};