回溯算法及题目
Posted 两片空白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了回溯算法及题目相关的知识,希望对你有一定的参考价值。
目录
前言
本博客参考代码随想录的题目来编写,大家可以去关注一下。主要加了自己的想法,便于复习。
一.什么是回溯算法
1.1 概念
回溯算法也叫回溯搜索法,是一种搜索方式。
回溯算法与递归相辅相成,意识就是回溯里面蕴含着递归。
由于有递归,所以就会有递归终止条件,按题意得到。递归前搜集结果,递归后释放收集的结果(构成回溯)。
1.2 理解回溯
回溯算法可以描述成一个树形结构,树的深度是递归的深度,树的宽度是搜集的结果,结构的收集是从左到右收集的。
1.3 模板
一般回溯问题可以简化成三个步骤:
- 确定回溯函数的参数和返回值。返回值一般是void
- 递归终止条件
- 单层的递归逻辑,收集结果和释放结果。
//伪代码
void backtracking(参数)//回溯函数
{
if(终止条件){
收集结果
return;
}
for(遍历所有集合的元素){
处理结点,处理单个元素
递归函数
回溯操作(撤销处理的结点)
}
}
for循环是树里的横向遍历树,递归则是纵向遍历树。
递归回溯返回上一层,再收集下一个元素进行递归,直到返回。
结合下面的题目来理解。会更加清楚。
1.4 回溯算法可以解决的问题
- 组合问题: N个数⾥⾯按⼀定规则找出k个数的集合
- 切割问题:⼀个字符串按⼀定规则有⼏种切割⽅式
- ⼦集问题:⼀个N个数的集合⾥有多少符合条件的⼦集
- 排列问题: N个数按⼀定规则全排列,有⼏种排列⽅式
- 棋盘问题: N皇后,解数独等等
组合问题并不注重集合元素中的顺序如[1,2]和[2,1],是一样的。排列注重集合元素的顺序[1,2]和[2,1]是两个集合
组合问题
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
抽象成的树结构:
1.确定回溯函数的参数和返回值
参数一个是集合n,一个是组合个数k,还需要一个startindex来控制 范围。
void backtracking(int startindex,int n,int k);
2.递归终止条件
当收集的元素个数达到k时,收集结果返回。
//终止条件
if(path.size()==k){
//搜集组合结果
result.push_back(path);
return;
}
3.单层的递归逻辑,收集结果和释放结果。
for(int i=startindex;i<=n;i++){
//操作,将单个元素放到数组中
path.push_back(i);
backtracking(i+1,n,k);
//回溯,释放单个元素
path.pop_back();
}
for循环每次从startindex开始,for循环确定了树的宽度。
由图可知,每次递归下一层从i+1开始。
path收集单个元素。递归完成回溯,释放收集的元素。
递归回溯返回上一层,再进行递归收集下一个元素。
class Solution {
public:
vector<int> path;//收集结点,收集单个元素
vector<vector<int>> result;//收集结果
void backtracking(int startindex,int n,int k){
//终止条件
if(path.size()==k){
//搜集组合结果
result.push_back(path);
return;
}
for(int i=startindex;i<=n;i++){
//操作,将单个元素放到数组中
path.push_back(i);
backtracking(i+1,n,k);
//回溯,释放单个元素
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(1,n,k);
return result;
}
};
剪枝:
剪枝思路:当后面的元素个数已经不够达到k值,时可以不用继续往下遍历了。例如:n=4,k=4。从n=2开始后面就不需要继续往下遍历了。
此时收集的元素个数path.size();
剩下要收集的元素个数:k-path.size()
最大起始位置:n-(k-path.size())+1,从n-(k-path.size())+1开始到最后才正好收集K个。
为什么要加1?因为n时从1开始的。
class Solution {
public:
vector<int> path;//收集结点,收集单个元素
vector<vector<int>> result;//收集结果
void backtracking(int startindex,int n,int k){
//终止条件
if(path.size()==k){
//搜集组合结果
result.push_back(path);
return;
}
for(int i=startindex;i<=n-(k-parh.size())+1;i++){
//操作,将单个元素放到数组中
path.push_back(i);
backtracking(i+1,n,k);
//回溯,释放单个元素
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(1,n,k);
return result;
}
};
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:输入: k = 3, n = 7
输出: [[1,2,4]]
这一题与上面一题如出一辙,只是增加了一个条件,需要和等于n。
参数:
这里依旧用path来收集单个元素,res来收集结果。
k元素个数,n元素目标和
num每层收集的元素和
startindex开始位置(确定范围),len最后位置(固定)
void backtraking(int startindex,int len,int k,int n,vector<int>& path,vector<vector<int>>& res,int num)
递归终止:
当元素个数超过k时,此时不需要继续往下递归,立马返回
当元素和超过n时,此时不需要继续往下递归,立马返回
当元素个数等于k且和等于n时,搜集结果,返回。
//如果和大于n返回
if(num>n){
return;
}
//如果元素个数大于k,返回
if(path.size()>k){
return;
}
//收集结果
if(num==n&&path.size()==k){
res.push_back(path);
return ;
}
单层递归逻辑:
path收集单个元素,num求和,递归,最后回溯
//单层递归逻辑
for(int i=startindex;i<=len-(k-path.size())+1;i++){
num+=i;//求和
path.push_back(i);//搜集单个元素
backtraking(i+1,len,k,n,path,res,num);//递归
num-=i;//回溯
path.pop_back();//回溯
}
注意回溯:上面时加和push,回溯就要减和pop。
class Solution {
public:
void backtraking(int startindex,int len,int k,int n,vector<int>& path,vector<vector<int>>& res,int num){
//如果和大于n返回
if(num>n){
return;
}
//如果元素个数大于k,返回
if(path.size()>k){
return;
}
//收集结果
if(num==n&&path.size()==k){
res.push_back(path);
return ;
}
//单层递归逻辑
for(int i=startindex;i<=len-(k-path.size())+1;i++){
num+=i;//求和
path.push_back(i);//搜集单个元素
backtraking(i+1,len,k,n,path,res,num);//递归
num-=i;//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum3(int k, int n) {
vector<vector<int>> res;
vector<int> path;
backtraking(1,9,k,n,path,res,0);
return res;
}
};
给定一个仅包含数字
2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
确定回溯函数参数:
path搜集单个元素,res搜集结果,digits按键,start第几个个按键,len按键总个数
void backtracing(string digits,int start,int len,string& path,vector<string>& res)
确定递归返回:
当path元素个数等于按键个数时,收集结果,返回
if(path.size()==len){
res.push_back(path);
return;
}
单层递归逻辑
先求出是哪个按键,遍历按键的所有字母情况。收集元素。
//用数组或者map可以保存按键和字母的对应关系
//下标对应按键的字母情况
string str[10]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
//注意递归不是增加的循环起始,而是走到下一个按键
int x=digits[start]-'0';//start按键字符转整形
for(int j=0;str[x][j]!=0;j++){//变量按键里字母的情况
path.push_back(str[x][j]);//收集元素
backtracing(digits,start+1,len,path,res);
path.pop_back();//回溯
}
注意几个细节点:
1.用数组或者map保存按键余字母的所有情况
2.递归更新的不是循环的起始值,而是按键的下一个。题意就是收集不同按键的字母的所有情况。
class Solution {
public:
//下标对应按键的字母情况
string str[10]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
void backtracing(string digits,int start,int len,string& path,vector<string>& res){
if(path.size()==len){//按键个数等于收集元素个数
res.push_back(path);
return;
}
//注意递归不是增加的循环起始,而是走到下一个按键
int x=digits[start]-'0';//start按键字符转整形
for(int j=0;str[x][j]!=0;j++){//变量按键里字母的情况
path.push_back(str[x][j]);//收集元素
backtracing(digits,start+1,len,path,res);
path.pop_back();//回溯
}
}
vector<string> letterCombinations(string digits) {
int len=digits.size();
vector<string> res;
if(len==0){
return res;
}
string path;
backtracing(digits,0,len,path,res);
return res;
}
};
给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。
对于给定的输入,保证和为 target 的唯一组合数少于 150 个。
示例 1:
输入: candidates = [2,3,6,7], target = 7
输出: [[7],[2,2,3]]
这上面的组合问题有点不同,是可以重复获取数字,但是,由于是组合问题,不考虑顺序,只能从一个数后面位置开始取数,不能从前面取数。
函数参数:
void backtracing(vector<int>& candidates,int target,int num,int start,int len,
vector<vector<int>>& result,vector<int>& path)
candidates为集合,target为目标和,num为当前求的和,start从集合Negev元素开始,len集合长度,result收集结果,path收集单个元素。
递归终止条件:
if(num>target){//当前和大于目标和,不需要继续往下算
return;
}
if(num==target){//等于目标和,搜集结果
result.push_back(path);
return;
}
单层递归逻辑:
收集结点和回溯
重复体现在每次循环开始不是i+1,而是i。
//重复体现在start,不是i+1,而是从i开始
for(int i=start;i<len;i++){
path.push_back(candidates[i]);//收i结果
num+=candidates[i];
backtracing(candidates,target,num,i,len,result,path);
path.pop_back();//回溯
num-=candidates[i];
}
class Solution {
public:
void backtracing(vector<int>& candidates,int target,int num,int start,int len,vector<vector<int>>& result,vector<int>& path){
if(num>target){//当前和大于目标和,不需要继续往下算
return;
}
if(num==target){//等于目标和,搜集结果
result.push_back(path);
return;
}
//重复体现在start,不是i+1,而是从i开始
for(int i=start;i<len&&num+candidates[i]<=target;i++){
path.push_back(candidates[i]);//收i结果
num+=candidates[i];
backtracing(candidates,target,num,i,len,result,path);
path.pop_back();//回溯
num-=candidates[i];
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> result;
vector<int> path;
int len=candidates.size();
backtracing(candidates,target,0,0,len,result,path);
return result;
}
};
这里有一个剪枝:
回溯剪枝一般在for循环中,如果num + candidates [i] >target 不继续往下回溯。但是要先排序,如果后面又小的数,符合条件也不会去回溯了。
class Solution {
public:
void backtracing(vector<int>& candidates,int target,int num,int start,int len,vector<vector<int>>& result,vector<int>& path){
if(num>target){//当前和大于目标和,不需要继续往下算
return;
}
if(num==target){//等于目标和,搜集结果
result.push_back(path);
return;
}
//重复体现在start,不是i+1,而是从i开始
for(int i=start;i<len&&num+candidates[i]<=target;i++){
path.push_back(candidates[i]);//收i结果
num+=candidates[i];
backtracing(candidates,target,num,i,len,result,path);
path.pop_back();//回溯
num-=candidates[i];
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
//剪枝要先排序,不然后面有小的,符合条件也不回溯了。
sort(candidates.begin(),candidates.end());
vector<vector<int>> result;
vector<int> path;
int len=candidates.size();
backtracing(candidates,target,0,0,len,result,path);
return result;
}
};
给定一个数组 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]
]
该题重点:1.元素不可以重复获取
2.给的集合candidates 中有相同元素,但是结果集合中不能出现组合相同的组合
元素不可以重复获取:可以调整每次回溯for循环中,i开始的范围。
给的集合candidates 中有相同元素,但是结果集合中不能出现组合相同的组合:一开始我想的是重复元素利用set去重,在去回溯。但是会忽略一些结果,比如: [10,1,2,7,6,1,5]就忽略了[1,1,6]。
那么问题来了,我们是要同⼀树层上使⽤过,还是统⼀树枝上使⽤过呢?
回看⼀下题⽬,元素在同⼀个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
所以我们要去重的是同⼀树层上的“使⽤过”,同⼀树枝上的都是⼀个组合⾥的元素,不⽤去重。强调⼀下,树层去重的话,需要对数组排序!
排序后,回溯同一层中取到相同元素就不要继续往下回溯了,因为会出现组合相同的情况。
解决:先排序,如果该开始收集的数和之前感慨是搜集的数相同,就不回溯了。看图好理解。
注意:for循环时树的宽度,递归式树的深度。去重,是在for循环里去重。同一层用到同一个数。会有同样的组合,而不是同一路径。比如下图,同一层用到同一个数就会有[1...],[1...]一样的组合。同一路径用到同一个数,就是[1,1...]。
函数参数:
candidates给的集合,target目标和,num当前和,start确定开始遍历位置,len结合长度,result收集结果,path收集单个结点。
backtracing(vector<int>& candidates,int target,int start,int len,int num,
vector<vector<int>>& result,vector<int>& path)
递归终止条件:
//大于就返回
if(num > target){
return ;
}
//收集结果
if(num == target){
result.push_back(path);
return ;
}
单层递归逻辑:
这里有一个剪枝:
回溯剪枝一般在for循环中,如果num + candidates [i] >target 不继续往下回溯。
//剪枝 num + candidates [i] >target 不继续往下回溯。
for(int i=start; i<len && num+candidates[i]<=target; i++){
//一层中有相同的就不回溯(会有相同结果,之前已经遍历过这种情况),不是同一路径,
if(i>start&&candidates[i]==candidates[i-1]){
continue;
}
num+=candidates[i];
path.push_back(candidates[i]);
backtracing(candidates,target,i+1,len,num,result,path);
num-=candidates[i];
path.pop_back();
}
class Solution {
public:
void backtracing(vector<int>& candidates,int target,int start,int len,int num,vector<vector<int>>& result,vector<int>& path){
//大于就返回
if(num > target){
return ;
}
//收集结果
if(num == target){
result.push_back(path);
return ;
}
for(int i=start; i<len && num+candidates[i]<=target; i++){
//一层中有相同的就不回溯(会有相同结果,之前已经遍历过这种情况),不是同一路径,
if(i>start&&candidates[i]==candidates[i-1]){
continue;
}
num+=candidates[i];
path.push_back(candidates[i]);
backtracing(candidates,target,i+1,len,num,result,path);
num-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
//要先排序
sort(candidates.begin(),candidates.end());
int len=candidates.size();
vector<vector<int>> result;
vector<int> path;
backtracing(candidates,target,0,len,0,result,path);
return result;
}
};
分割问题
分割问题注意切割线在哪,或者说时哪个。切割线一般是循环开始startindex。
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
函数参数:
字符串s,循环开始位置startindex,字符串长度,收集结果result,收集字符串path。
void backtracing(string& s,int startindex,int len,vector<vector<string>>& result,vector<string>& path)
上一次切割线位置为startindex。
如图
递归终止条件:
如例子,当切割线到最后,就收集结果
//当切割线到最后时,递归结束
if(startindex==len){
result.push_back(path);
return;
}
单层递归逻辑:
先取得子串,由于上一次切割线位置为startindex,子串长度为i-startindex+1。获取子串用substr函数,第一个参数为起始位置,第二个参数为子串长度。
判断子串是否为回文串,是就回溯并且收集子串。
//分割线在startindex位置
for(int i=startindex;i<len;i++){
//获取子串
string tmp=s.substr(startindex,i-startindex+1);
//判断子串是否是回文串
if(JudgePal(tmp)){
path.push_back(tmp);
backtracing(s, i+1, len, result, path);
path.pop_back();
}
}
class Solution {
public:
//判断一个字符串是否是字符串
bool JudgePal(string& str){
int left=0;
int right=str.size()-1;
while(left<right){
if(str[left]!=str[right]){
return false;
}
left++,right--;
}
return true;
}
void backtracing(string& s,int startindex,int len,vector<vector<string>>& result,vector<string>& path){
//当切割线到最后时,递归结束
if(startindex==len){
result.push_back(path);
return;
}
//分割线在startindex位置
for(int i=startindex;i<len;i++){
//获取子串
string tmp=s.substr(startindex,i-startindex+1);
//判断子串是否是回文串
if(JudgePal(tmp)){
path.push_back(tmp);
backtracing(s, i+1, len, result, path);
path.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
vector<vector<string>> result;
vector<string> path;
int len=s.size();
backtracing(s,0,len,result,path);
return result;
}
};
给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。
示例 1:
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
该题题时切割问题,'.'逗号位置就是切割位置。限制条件要将字符串分为4段,每段字符对应的整数要求大于等于0,小于等于255。并且每段长度大于1时,第一个不能为0。每段之间以'.'逗号隔开。
图片取至代码随想录
注意:由题意,IP地址为4段,每段转化为整数后处于0~255之间。假设每段都是255,字符串长度最多为12个,当超过12时,肯定不能转化为IP地址。
函数参数:
s为字符串,startindex为字符起始位置,也是上一次的切割位置。len字符串长度,result收集IP字符串,path单个符合条件IP字符串。
void backtracing(string& s,int startindex,int len,vector<string>& result,string& path)
递归终止条件:
单层逻辑,我在每个符合条件的子串后面都加了一个'.'逗号。当IP长度等于s长度+4时,并且切割到最后,收集结果返回。未切割到最后,直接返回。
//我在每段后面都加了一个'.',
//当IP长度等于s+4,但是没有切割到最后
//IP不符合条件,直接返回
if(path.size()==len+4&&startindex<len){
return;
}
//当IP长度等于s+4,切割到最后
//IP符合条件,收集结果后,返回
if(path.size()==len+4&&startindex==len){
path.pop_back();
result.push_back(path);
return;
}
单层递归逻辑:
先获取子串,注意startindex是上一次的切割位置。判断子串是否在0~255之间。如果子串长度大于1,子串第一个字符不能为0。
回溯,要将子串全部pop出来。
for (int i = startindex; i<len; i++){
//切割子串
string tmp = s.substr(startindex, i - startindex + 1);
//子串转为C字符串
const char* str = tmp.c_str();
//转化成整数
int x = atoi(str);
//在范围内
if ((x <= 255 && x >= 0)){
//子串长度大于1,第一个不能为0
if (tmp[0] == '0'&&tmp.size()>1){
}
//收集子串
else{
//形成IP
path += str;
path += '.';
backtracing(s, i + 1, len, result, path);
//取出'.'
path.pop_back();
//将收集的子串pop
while (path.size()>0 && path[path.size() - 1] != '.'){
path.pop_back();
}
}
}
}
总代码:
class Solution {
public:
void backtracing(string& s,int startindex,int len,vector<string>& result,string& path){
if(path.size()==len+4&&startindex<len){
return;
}
if(path.size()==len+4&&startindex==len){
path.pop_back();
result.push_back(path);
return;
}
for(int i=startindex;i<len;i++){
//切割子串
string tmp=s.substr(startindex,i-startindex+1);
//子串转为C字符串
const char* str=tmp.c_str();
//转化成整数
int x=atoi(str);
//在范围内
if((x<=255&&x>=0)){
//子串长度大于1,第一个不能为0
if(tmp[0]=='0'&&tmp.size()>1){
}
//收集子串
else{
//形成IP
path+=str;
path+='.';
backtracing(s,i+1,len,result,path);
//取出'.'
path.pop_back();
//将收集的子串pop
while(path.size()>0&&path[path.size()-1]!='.'){
path.pop_back();
}
}
}
}
}
vector<string> restoreIpAddresses(string s) {
vector<string> result;
string path;
int len=s.size();
//长度大于12.不能形成IP
if(len>12){
return result;
}
backtracing(s,0,len,result,path);
return result;
}
};
求子集问题
子集问题也适用于回溯算法,与组合和切割问题不同的是,组合和切割问题都是在叶节点收集结果,子集问题是收集每一个结点的结果。
子集问题也是一种组合问题,不考虑顺序。所以按照模板,for循环从startindex开始,startindex递归更新是从i+1开始。
给你一个整数数组
nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
按照例子,我们发现每一个结点,就是我们需要的结果。
函数参数:
nums数组集合,start开始位置,从nums中的哪个位置开始,len数组长度,result收集结点结果,path收集每一个数字,相当于结点。
void backtracing(vector<int>& nums,int start,int len,
vector<vector<int>>& result,vector<int>& path)
递归终止条件:
nums中遍历开始位置后面已经没有元素时,返回。
开始位置时start,当start==len时,后面没有元素了。
其实这里也不要写递归返回,因为for循环循环条件是i<len,当start>len时不会进入循环,直接返回。
if(start>=len){
return ;
}
单层递归逻辑:
收集元素,递归和回溯,path相当于结点。
for(int i=start;i<len;i++){
path.push_back(nums[i]);//收集子集元素
backtracing(nums,i+1,len,result,path);//递归
path.pop_back();//回溯
}
由于树的每一个结点都要收集:每次递归进入循环,都要收集结果。
class Solution {
public:
void backtracing(vector<int>& nums,int start,int len,vector<vector<int>>& result,vector<int>& path){
//收集结点
result.push_back(path);
//可以不写
// if(start>=len){
// return ;
// }
for(int i=start;i<len;i++){
path.push_back(nums[i]);//收集子集元素
backtracing(nums,i+1,len,result,path);//递归
path.pop_back();//回溯
}
}
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
vector<int> path;
int len=nums.size();
backtracing(nums,0,len,result,path);
return result;
}
};
给你一个整数数组
nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7] 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
根据示例发现,该结果求的是子集,但是需要去重。比如:不去重,[4,6,7]会出现两次。
去重不能像上面的一个组合问题一样,先排序后去重,排序后,就不能得到和想在相同顺序的递增子序列了。
并且去重是去除同一层相同的值,排序后可以比较前后,但是这里不能排序,可以用哈希表或者是一个数组记录,同一层是否已经使用过该元素。
函数参数:
由于同一元素不能重复取,取元素范围从startindex开始,每次递归从i+1开始。
void backtracing(vector<int>& nums,int startindex,int len,
vector<vector<int>>& result,vector<int>& path)
递归返回:
当startindex到数组最后,没有元素,递归返回。递归返回条件不用写,for循环,循环条件是再最后,会直接返回。
单层递归逻辑:
当path中没有元素要插入元素,当path最后一个元素比nums的第i个元素大于或者等于,插入元素。
还需要用哈希表或者建立一个数组。判断同一层是否使用相同元素。
for(int i=startindex;i<len;i++){
//当path元素不是一个并且小于path的值
//当同一层有相同的
if(path.size()>0&&nums[i]<path[path.size()-1]||map[nums[i]+100]==1){
continue;
}
//元素只有一个,和nums[i]>path最后一个
//user.insert(nums[i]);
map[nums[i]+100]=1;
path.push_back(nums[i]);
backtracing(nums,i+1,len,result,path);
path.pop_back();
}
}
难点主要是同一层不能使用相同元素,并且不能排序。用数组或者哈希表记录。
同一层是在for循环里。递归会重新定义。
class Solution {
public:
void backtracing(vector<int>& nums,int startindex,int len,vector<vector<int>>& result,vector<int>& path){
if(path.size()>1){
result.push_back(path);
}
//去除同一层里相同的
//同一层相同的是树的宽度,for循环里
//每一次递归,是树的高度,都会重新定义
//unordered_set<int> user;//去重
int map[201]={0};
for(int i=startindex;i<len;i++){
//当path元素不是一个并且小于path的值
//当同一层有相同的
if(path.size()>0&&nums[i]<path[path.size()-1]||map[nums[i]+100]==1){
continue;
}
//元素只有一个,和nums[i]>path最后一个
//user.insert(nums[i]);
map[nums[i]+100]=1;
path.push_back(nums[i]);
backtracing(nums,i+1,len,result,path);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
int len=nums.size();
vector<vector<int>> result;
vector<int> path;
backtracing(nums,0,len,result,path);
return result;
}
};
排列问题
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
排列时有序的,元素相同顺序不同的集合是不同的集合。
所以排列不能像组合一样,每次循环从startindex开始,而是每次从0 开始。集合[1,2],[1,2]和[2,1]是两个不同的集合。1使用完,选2的时候还会要使用1,所以每次循环从0开始。
还需要用一个数组记录元素是否使用过,不然会出现重复元素的情况。
抽象成树形结构:
函数参数:
result收集结果,path收集单个元素,key元素是否使用的标志。
vector<vector<int>> result;
vector<int> path;
void backtracing(vector<int>& nums,int len,vector<int>& key)
递归出口:
全排列,当收集元素个数等于数组元素个数时,收集结果,返回。
if(path.size()==len){
result.push_back(path);
return;
}
单层递归逻辑:
判断是否使用过,未使用,push进path,将该元素标记为使用过。再递归
回溯:pop出元素,将该元素回溯为未使用。
//全排列开始从0开始,因为注重顺序
for(int i=0; i<len; i++){
//未使用
if(key[i]==0){
path.push_back(nums[i]);
//表示未使用过
key[i]=1;
backtracing(nums,len,key);
//回溯
path.pop_back();
//回溯为未使用
key[i]=0;
}
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracing(vector<int>& nums,int len,vector<int>& key){
if(path.size()==len){
result.push_back(path);
return;
}
//全排列开始从0开始,因为注重顺序
for(int i=0; i<len; i++){
//未使用
if(key[i]==0){
path.push_back(nums[i]);
//表示未使用过
key[i]=1;
backtracing(nums,len,key);
//回溯
path.pop_back();
//回溯为未使用
key[i]=0;
}
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
//记录是否使用过,0未使用,1使用过
vector<int> key;
int len=nums.size();
key.resize(len);
backtracing(nums,len,key);
return result;
}
};
给定一个可包含重复数字的序列
nums
,按任意顺序 返回所有不重复的全排列。示例 1:
输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]
因为有重复的元素,这里涉及到需要去重的操作。
去重的操作和组合去重的操作类似,由于这里没有严格规定连个数之间的相对位置。可以先排序后去重。同一层元素相同,不进行排列。
如果前后两元素相等,并且,前面这个元素没有使用,说明还会拿前面的元素,会有相同的结果。此时不进行排列。
//如果前一个没使用过,并且还和现在一样,会出现相同结果
if(i>0&&usered[i-1]==0&&nums[i]==nums[i-1]){
continue;
}
其它排列情况和上面相同
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracing(int len,vector<int>& nums,vector<int>& usered){
if(path.size()==len){
result.push_back(path);
return;
}
for(int i=0; i<len; i++){
//如果前一个没使用过,并且还和现在一样,会出现相同结果
if(i>0&&usered[i-1]==0&&nums[i]==nums[i-1]){
continue;
}
if(usered[i]==0){
usered[i]=1;
path.push_back(nums[i]);
backtracing(len,nums,usered);
usered[i]=0;
path.pop_back();
}
}
}
static bool cmp(int a, int b){
return a<b;
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
//先排序,将相同的元素放一起
sort(nums.begin(),nums.end(),cmp);
int len=nums.size();
//标记元素是否使用
vector<int> usered;
usered.resize(len);
backtracing(len,nums,usered);
return result;
}
};
重新安排行程
给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。示例 1:
输入:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
输出:["JFK","MUC","LHR","SFO","SJC"]
这道题⽬有⼏个难点:
1. ⼀个⾏程中,如果航班处理不好容易变成⼀个圈,成为死循环
2. 有多种解法,字⺟序靠前排在前⾯,让很多同学望⽽退步,如何该记录映射关系呢 ?
3. 使⽤回溯法(也可以说深搜) 的话,那么终⽌条件是什么呢?
4. 搜索的过程中,如何遍历⼀个机场所对应的所有机场。
1. ⼀个⾏程中,如果航班处理不好容易变成⼀个圈,成为死循环?
为什么是这样?比如 ["JFK","NRT"],["NRT","JFK"]。起始只有两个航班,两个航班起点和重点是相反的,这样会形成一个循环,最终递归返回导致错误的结果。
所以我们每次用完一个航班就需要删除这个航班或者标记。
2..如何确定映射关系?
这里要确定起始位置和终止位置的映射关系,并且出现的起始位置会有重叠。
如果直接用multimap<string,string>,或者unordered_map<string,multiset<string>> 在删除时,会导致迭代器失效。(multimap<起始位置,终止位置>,unordered_map<起始位置,multiset<终止位置>>
我们可以使用unordered_map<string,map<string,int>>,含义:unordered_map<起始位置,map<终止位置,起始位置相同的航班数>>,用航班个数记录是否还能用该航班,不用做删除操作。
//key<起始位置,<终止位置,航班次数>>
unordered_map<string,map<string,int>> key;
3. 终止条件
每次都会要走一个航班,但是起始还有一个"JFK",所以当收集结果个数等于航班数加1,返回。
if(result.size()==ticketsnum+1){
return true;
}
4. 通过回溯法来遍历各机场
- 递归参数
ticketsnum表示航班数
vector<string> result;//保持结果
//返回值bool,因为我们只需要找一条线
bool backtracing(unordered_map<string,map<string,int>>& key, int ticketsnum)
返回值为bool,由题意,至少存在一种合理行程,我们只要找到该行程再一直返回就好了。
result和key都需要初始化:
由题意,行程由"JKF"出发。
key记录内一个航班起始和终止,并且相同起点,的航班数。
for(int i=0; i<ticketsnum; i++){
key[tickets[i][0]][tickets[i][1]]++;
}
result.push_back("JFK");
- 递归终止条件
每次都会要走一个航班,但是起始还有一个"JFK",所以当收集结果个数等于航班数加1,返回。
if(result.size()==ticketsnum+1){
return true;
}
- 单层递归逻辑
这里有一个很隐晦的条件,通过result里的值作为起点再key中找终点。得到c。
再判断该位置是否还能用。
//key[result[result.size()-1]是通过起点找终点,
for(pair<const string,int>& c : key[result[result.size()-1]]){
//看是否还能用
if(c.second>0){
c.second--;
result.push_back(c.first);
if(backtracing(key,ticketsnum)==true){
return true;
}
c.second++;
result.pop_back();
}
}
return false;
}
注意:pairstring⾥要有const,因为map中的<key,value>的key是不可修改的,所以是 pair<const string, int> 。
class Solution {
vector<string> result;
//返回值bool,因为我们只需要找一条线
bool backtracing(unordered_map<string,map<string,int>>& key, int ticketsnum){
if(result.size()==ticketsnum+1){
return true;
}
//key[result[result.size()-1]是通过起点找终点,
for(pair<const string,int>& c : key[result[result.size()-1]]){
//看是否还能用
if(c.second>0){
c.second--;
result.push_back(c.first);
if(backtracing(key,ticketsnum)==true){
return true;
}
c.second++;
result.pop_back();
}
}
return false;
}
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {
//key<起始位置,<终止位置,航班次数>>
unordered_map<string,map<string,int>> key;
int ticketsnum=tickets.size();
for(int i=0; i<ticketsnum; i++){
key[tickets[i][0]][tickets[i][1]]++;
}
result.push_back("JFK");
backtracing(key, ticketsnum);
return result;
}
};
以上是关于回溯算法及题目的主要内容,如果未能解决你的问题,请参考以下文章