LeetCode回溯算法章节 回溯模板 1 2 3 4 5 6 7 8 9 10 void backtrace (参数){if (终止条件)for (每层中的元素)backtrace ();调用回溯
组合 [组合](77. 组合 - 力扣(LeetCode) )
回溯 从[1,N]中找到凑成K个数的所有组合,将[i,N](i=1,2,3…N)看做一棵树的不同结点,每取出一个数就把剩下的数看做该结点的子结点,叶子结点则为符合要求的结果。因此,for循环中的是不断取出一个数进入递归,结束递归后删除,再递增取下一个数,直到遇到结束条件退出递归。即for循环会遍历[1,N]中所有的元素,每添加一个元素进入数组就进入一层递归,但进入递归后若符合终止条件则会跳出,避免了N次方的时间复杂度。也相当于枚举法,但会及时止损
确定回溯终止条件
1 2 3 4 5 if(vec.size()= = k)
当数组大小符合要求时,也就是遇到了叶子结点,这一次递归就终止,并更新结果集
确定回溯返回值
1 2 3 4 void  search (int  n,int  k,vector<vector<int >>& res,vector<int >& vec,int  start) 
确定单层逻辑
1 2 3 4 5 6 for (int i = start;i<=n;i++).push (i);search (n,k,res,vec,start+1 );.pop_back ();
 
整体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class  Solution  {public :void  search (int  n, int  k, vector<vector<int >>& res, vector<int >& vec, int  start)      {if  (vec.size () == k)push_back (vec);return ;for  (int  i = start; i <= n; ++i)push_back (i);search (n, k, res, vec, i + 1 );pop_back ();int >> combine (int  n, int  k) {int >> res;int > vec;search (n, k, res, vec, 1 );return  res;
回溯优化 在回溯算法当中,由于是回溯的本质就是枚举,对枚举的优化只在于剪枝,通过给定限定条件减少无必要的递归,本题也同样如此,以上版本中将start赋予到了i,却并没有考虑到start的位置要求,在某些分支中,start过于太大,导致剩下的元素不能满足k的数量要求,因此,可以对回溯进行剪枝优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class  Solution  {public :void  search (int  n, int  k, vector<vector<int >>& res, vector<int >& vec, int  start)      {if  (vec.size () == k)push_back (vec);return ;for  (int  i = start; i <= n - (k - vec.size ()) + 1 ; ++i)push_back (i);search (n, k, res, vec, i + 1 );pop_back ();int >> combine (int  n, int  k) {int >> res;int > vec;search (n, k, res, vec, 1 );return  res;
剪枝的原因在于i不必从过于大的start中取值,因此将对for循环中i的循环结束语句修改,从进入递归之前剪枝
总结 回溯算法难点在于将题意转换成树形结构,对于树形结构可以更好的理解回溯,以及剪枝的操作
组合总和III 回溯 题目思路类似于上一题,只不过将取值范围固定在1~9之间,其余都一样
整体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class  Solution  {public :void  traceback (int  k, int  n, vector<vector<int >>& res, vector<int >& vec, int  index)      {if  (vec.size () == k)if  (accumulate (vec.begin (), vec.end (), 0 ) == n)push_back (vec);return ;for  (int  i = index; i <= 9 ; i++)push_back (i);traceback (k, n, res, vec, i + 1 );pop_back ();int >> combinationSum3 (int  k, int  n) {int >> res;int > vec;traceback (k, n, res, vec, 1 );return  res;
回溯优化 在以上版本代码中,容易看出需要优化的地方在于取值的范围是递增的,一旦有满足总和的vec数组,后续的遍历中的数组总和只会更大,因此可以忽略掉后续的遍历。类似地,同样在递归进入条件判断语句中增加限制,考虑到是用数组总和来做判断是否需要剪枝,则增加一个形参sum表示当前vec数组的总和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class  Solution  {public :void  traceback (int  k, int  n, vector<vector<int >>& res, vector<int >& vec, int  index, int & sum)      {if  (vec.size () == k)if  (sum == n)push_back (vec);return ;for  (int  i = index; i <= 9  && (n - sum) >= i; i++)push_back (i);traceback (k, n, res, vec, i + 1 , sum);pop_back ();int >> combinationSum3 (int  k, int  n) {int >> res;int > vec;int  sum = 0 ;traceback (k, n, res, vec, 1 , sum);return  res;
总结 在回溯法中,如果想要不重复地访问元素,则需要traceback(k,n,res,vec,i+1)而不是traceback(k,n,res,vec,index+1),后者会在i遍历时,与index+1有重复值,且重复值只从2开始
本题是与上题相同的回溯思路,但不同的剪枝思路。
电话号码的字母组合 [电话号码的字母组合](17. 电话号码的字母组合 - 力扣(LeetCode) )
回溯 与前些题目不同的是,没有直接能获取到要进入递归的范围,即递归树的结点,需要借助容器来获取数值范围,这里使用数组保存0~9所分别对应的字母。
1 2 3 4 5 6 7 8 9 10 11 vector<vector<string>> hashmap{"a" ,"b" ,"c" },"d" ,"e" ,"f" },"g" ,"h" ,"i" },"j" ,"k" ,"l" },"m" ,"n" ,"o" },"p" ,"q" ,"r" ,"s" },"t" ,"u" ,"v" },"w" ,"x" ,"y" ,"z" } };
这里保存0,1两个空数组是为了方便索引访问直接对应
确定递归结束条件
1 2 3 4 5 if(digits.size()= = str.size())
确定递归返回值
1 void  traceback (vector<string >& res, string & str,const  string  &digits,int  index )
确定单层逻辑
1 2 3 4 5 6 for (int  i = 0 ;i<hashmap[digit].size();i++)str  += hashmap[digit][i];str ,digits,index +1 );str .pop_back();
 
整体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class  Solution  {public :"a" ,"b" ,"c" },"d" ,"e" ,"f" },"g" ,"h" ,"i" },"j" ,"k" ,"l" },"m" ,"n" ,"o" },"p" ,"q" ,"r" ,"s" },"t" ,"u" ,"v" },"w" ,"x" ,"y" ,"z" }};void  traceback(vector<string>& res,string& str ,const  string &digits,int  index )if (str .size()==digits.size())str );return ;int  digit = digits[index ] - '0' ;for (int  i = 0 ;i<hashmap[digit].size();i++)str  += hashmap[digit][i];str ,digits,index +1 );str .pop_back();str ;if (digits.empty())  return  res;str ,digits,0 );        return  res;
总结 由回溯过程可知,这个版本的回溯代码不需要剪枝优化。
在本题中,传入递归函数的index形参用来获取digits中的下一位数字,保证每次for循环的都是不同进位上的数字,也是这道题的最大变化点。并且不需要传入其余参数用于表示开始遍历的结点索引,因为需要遍历的是对应数字上的所有字母
组合总和 [组合总和](39. 组合总和 - 力扣(LeetCode) )
回溯 这道题给定一个数组和目标值,要求从数组中找到不限数量的数组元素,令其和等于目标值,并且能够重复用数组中的元素。最大的不同点在于能够重复使用数组元素,首先能想到的就是不需要index形参,不能让递归每次都从下一个元素开始遍历。
确定递归结束条件
1 2 3 4 5 if(target = =  sum)
容易想到只要让当前数组的和等于目标值即可结束该层递归,即到达叶子结点。但是这样就会导致了死循环,因为如果一旦数组和大于目标值,则递归永远不会退出。因此需要加上对sum值的限制(在进入递归前判断)
确定递归返回值
1 void  traceback(vector <int >& candidates, vector <vector <int >> &res,vector <int > &vec,int  target ,int  &sum ,int  startindex)
确定单层逻辑
1 2 3 4 5 6 7 8 for (int  i = startindex; i < candidates.size () && target  > sum ; i++)sum  += candidates[i];target , sum , i);sum  -= candidates[i];
在这,startindex作为每次遍历时开始的索引下标,与[III](# 组合总和III)不同,传入到下一次递归的是i而不是i+1,这是因为允许重复元素的存在 
 
整体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution {void  traceback(vector <int >& candidates, vector <vector <int >>& res, vector <int >& vec, int  target , int & sum , int  startindex)if  (sum  == target )return ;for  (int  i = startindex; i < candidates.size () && target  》 sum ; i++)sum  += candidates[i];target , sum , i);sum  -= candidates[i];vector <vector <int >> combinationSum(vector <int >& candidates, int  target ) {vector <vector <int >> res;vector <int > vec;int  sum  = 0 ;target , sum , 0 );return  res;
回溯优化 可以想到,如同[III](# 组合总和III),剪枝优化的思路在于当数组递增 时,一旦有满足目标值的数组元素,往后的元素再进入递归,vec数组的和都会大于目标值,因此可以剪掉,所以先给数组进行排序 ,然后在递归进入前加以条件的限制 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution {void  traceback(vector <int >& candidates, vector <vector <int >>& res, vector <int >& vec, int  target , int & sum , int  startindex)if  (sum  == target )return ;for  (int  i = startindex; i < candidates.size () && (target  - sum ) >= candidates[i]; i++)sum  += candidates[i];target , sum , i);sum  -= candidates[i];vector <vector <int >> combinationSum(vector <int >& candidates, int  target ) {vector <vector <int >> res;vector <int > vec;int  sum  = 0 ;target , sum , 0 );return  res;
总结 当允许重复元素出现时,在进入下一层递归时就可以传入i而不是i+1。
因为给出的数组非递增,需要先对数组进行排序,优化效果不明显
组合总和II [组合总和II](40. 组合总和 II - 力扣(LeetCode) )
回溯 该题与[II](# 组合总和II)的区别就在于输入的candidates数组允许有重复元素的出现,但每个元素只能使用一次 ,因此传递到下一层递归中的索引就必须是index+1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution {void  traceback(vector <int >& candidates, vector <vector <int >>& res, vector <int >& vec, int  target , int & sum , int  startindex)if  (sum  == target )return ;for  (int  i = startindex; i < candidates.size () && target  > sum ; i++)sum  += candidates[i];target , sum , i);sum  -= candidates[i];vector <vector <int >> combinationSum(vector <int >& candidates, int  target ) {vector <vector <int >> res;vector <int > vec;int  sum  = 0 ;target , sum , 0 );return  res;
此外,如果candidates中有重复元素,会导致同层的结点中可能会选取一样的值进入到下一层递归中,就会导致最终结果出现有相同元素的分组 ,如以上版本的代码,仅修改了上一题的index传递值。原因在于,可能会出现保存到res中的元素在回溯删除之后,因为相同值的元素存在,在遍历剩余元素时又被重新添加进去。
可以针对这种情况设立一个tmp变量,先对candidates进行排序,tmp记录刚被删除的元素的值,再下一个元素被添加时判断是否等于tmp,只有不相等时才会被添加进去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution {int  tmp = 0 ;void  traceback(vector <int >& candidates, vector <vector <int >>& res, vector <int >& vec, int  target , int & sum , int  startindex)if  (sum  == target )return ;for  (int  i = startindex; i < candidates.size () && (target -sum ) >= candidates[i]; i++)if (tmp == candidates[i])continue ;sum  += candidates[i];target , sum , i+1 );sum  -= candidates[i];vector <vector <int >> combinationSum2(vector <int >& candidates, int  target ) {vector <vector <int >> res;vector <int > vec;int  sum  = 0 ;target , sum , 0 );return  res;
另一种方法
1 2 3 4 5 6 7 8 9 10 for (int  i = startindex; i < candidates.size () && (target -sum ) >= candidates[i]; i++)if (i > startindex && candidates[i] == candidates[i-1 ])continue ;sum  += candidates[i];target , sum , i+1 );sum  -= candidates[i];
回溯优化 剪枝优化过程同上题 ,以上版本代码已剪枝
总结 candidates没有重复元素,但结果输出可以有重复元素,则修改index
candidates有重复元素,但结果输出不能有重复元素,即相同元素的分组,则通过添加限制条件防止重复元素进入递归
分割回文串 [分割回文串](131. 分割回文串 - 力扣(LeetCode) )
回溯 相比较于之前的题目,该题变化点主要集中在于递归结束判断条件,在满足回文的条件的同时,还需要将整个字符串完全分割 
确定递归结束条件
1 2 3 4 5 6 if (index >=  s.size())
由于不能添加重复元素进入递归,所以需要index形参,需要在对整个字符串完成分割的时候才停止递归,并且要满足回文的条件才能保存结果
确定递归返回值
1 void  traceback(string  s, int  index)
确定单层逻辑
1 2 3 4 5 6 for  (int  i = index; i < s.size() ; ++i)_back(s .substr (index ,i +1-index ) );1 );_back() ;
 
整体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class  Solution  {public :bool  judge ()      {for (auto  sustr:vec)int  i = 0 , j = sustr.size ()-1 ;while (i<=j)if (sustr[i]!=sustr[j])return  false ;return  true ;void  traceback (string s, int  index)      {if  (index>=s.size ())if (judge ())push_back (vec);return ;for  (int  i = index; i < s.size (); ++i)push_back (s.substr (index,i+1 -index));traceback (s, i + 1 );pop_back ();partition (string s) {if  (s.size () == 1 )return  vector<vector<string>>{{s}};traceback (s, 0 );return  res;
此外,还可以在进入递归前完成对回文串的判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class  Solution  {public :bool  judge (int  st, int  en,string s)      {while (st<=en)if (s[st]!=s[en])return  false ;return  true ;void  traceback (string s, int  index)      {if  (index>=s.size ())push_back (vec);return ;for  (int  i = index; i < s.size (); ++i)if (!judge (index,i,s))continue ;push_back (s.substr (index,i+1 -index));traceback (s, i + 1 );pop_back ();partition (string s) {if  (s.size () == 1 )return  vector<vector<string>>{{s}};traceback (s, 0 );return  res;
总结 难点在于
复原IP地址 本题也属于切割问题,但切割完成的判断更为复杂,切割的IP地址分为4段,需要直接在原字符串中加上'.'表示分段,分段中的IP地址不能有前导0,不能大于255,否则都将是无效的IP。
转换成树形结构
首先,确定判断IP段是否有效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bool  ok(const  string& s, int  st, int  en)if (st > en)return  false ;if (s[st] == 0  && st!=en)return  false ;int  num  = 0 ;for (int  i = st; i <= en; i++)num  = num *10  + (s[i] - '0' );if (num  > 255 )return  false ;return  true ;
如果仅仅是首元素为0还不一定无效,IP地址段可以只有一个0,但不能是前导0
计算IP地址段可以用for循环,逐位相加,一旦某一位已经超过255则直接返回false
回溯1 
递归结束条件
1 2 3 4 5 6 if (comma == 3 )if (ok(s,index,s.size()-1 )).push_back (s);
因为是直接在源字符串中做分割,在加了3个'.'的情况下,还需要对最后一段IP进行判断是否有效才能加入到结果集中
递归返回值
1 void traceback(vector<string> & res , int  comma, int  index , string & s)
因为直接在源字符串上操作,并且需要找到所有有效的IP地址,因此不需要返回值。传入参数为引用,不用拷贝并且允许直接修改、删除等操作
单层逻辑
1 2 3 4 5 6 7 8 for (int  i = index ; i < s.size(); i++)insert (s.begin () + i + 1 , '.' );2 , s);begin () + i + 1 );
insert函数将'.'插在输入的迭代器之前,即取代输入的迭代器位置,erase则是删除输入迭代器当前位置上的元素。进入递归由于插入多了一位,则下一层递归中需要从index = i+2开始
 
整体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class  Solution  {public :bool  ok (const  string & s, int  st, int  enif  (st > en)return  false ;if  (s[st] == '0'  && st != en)return  false ;int  num = 0 ;for  (int  i = st; i <= en; i++) 10  + (s[i] - '0' );if  (num > 255 ) return  false ;return  true ;void  traceback (vector<string >& res, int  index, int  comma, string & s )if  (comma == 3 )if  (ok(s, index, s.size() - 1 ))return ;for  (int  i = index; i < s.size(); i++)if  (ok(s, index, i))1 , '.' );2 , comma, s);1 );vector<string > restoreIpAddresses (string  s  {string > res;0 , 0 , s);return  res;
回溯2 另外一种解法是, 不在源字符串上修改,而是将4段IP地址一段一段的加入一个空字符串中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class  Solution  {public :void  backtrace(string& s,int  cnt,int  index ,string& str ){if (cnt==4  || index ==s.size() ){if (cnt==4  && index ==s.size())str .substr(0 ,str .size()-1 ));return ;for (int  i=1 ;i<=3 ;i++){if (index +i>s.size()) return ;if (s[index ]=='0'  && i!=1 ) return ;if (i==3  && s.substr(index ,i)>"255" ) return ;str +=s.substr(index ,i);str .push_back('.' );1 ,index +i,str );str  = str .substr(0 ,str .size()-i-1 );str  ="" ;0 ,0 ,str );return  ans;
总结 回溯1是从一整段IP地址直接做分割,回溯2则是将各段分割好的小段最终才拼凑成完整的IP地址,这样做的好处是对于各段统一判断是否有效,而不用像回溯1中在添加了最后一个'.'之后,添加到结果集之前还要做一次有效判断。并且在回溯2中,判断有效更加简洁,可以利用加入的子段最多只有3位直接与”255”进行判断,而回溯1中可能会出现不止3位的IP地址,因此不能直接与”255”判断,但可以修改成if(en - st == 2 && s.substr(s.begin() + st, 3) > "255") return false;以及if(en - st > 2)	return false;
子集 回溯 子集问题中,依旧还是回溯的总体思路,不同在于之前结束条件的判断,仅仅只是遍历到数组结尾就退出
整体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class  Solution  {public :void  traceback (vector<int >&nums,vector<int >&vec,vector<vector<int >>&res,int  index)      {if (index>=nums.size ())return ;for (int  i = index; i<nums.size ();i++)push_back (nums[i]);push_back (vec);traceback (nums,vec,res,i+1 );pop_back ();int >> subsets (vector<int >& nums) {int > vec;int >> res;push_back (vec);traceback (nums,vec,res,0 );return  res;
总结 子集问题不需要有判断条件,就直接存入结果集中,相当于将树型结构上的每个结点 ;而组合和切割问题则是保留叶子结点 
子集II 回溯 给定输入数组中含有重复元素,但要求输出结果中不能有重复子数组,与[组合总和II](# 组合总和II)思路一致,且其余同子集 的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class  Solution  {public :void  traceback (vector<int >&nums,vector<int >&vec,vector<vector<int >>&res,int  index)      {if (index>=nums.size ())return ;for (int  i = index; i<nums.size ();i++)if (i>index&&nums[i]==nums[i-1 ])continue ;push_back (nums[i]);push_back (vec);traceback (nums,vec,res,i+1 );pop_back ();int >> subsetsWithDup (vector<int >& nums) {int > vec;int >> res;push_back (vec);sort (nums.begin (),nums.end ());traceback (nums,vec,res,0 );return  res;
递增子序列 [递增子序列](491. 递增子序列 - 力扣(LeetCode) )
回溯 这道题需要理解的几个关键点:
要求是子序列,则不能对其排序,有点类似于分割问题但可以只取两边不取中间,即[1,2,3]选[1,3] 
要求是非严格递增,则在进入递归前应先进行判断, 
输入数组中可能会有重复元素,但输出不能包含重复子数组 
 
可知,需要在不能排序 的情况下,还要在递归过程中排除重复项,回溯的时候应该借助容器来帮助辨别重复项 ,容易想到用unordered_set容器储存结果用于去除重复项,然后遍历容器将没有重复的元素依次存放到vector中,但是,不能直接定义unordered_set<vector<int>>,还要有自定义的哈希函数。因此可以转换思路,在每一层的结点循环中设置一个unordered_set,即单单不允许这一层有重复被选数 ,这就避免重复项进入递归,因为前一个所选就囊括了后面所有相同数的子序列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class  Solution  {public :void  traceback (vector<int >& nums,vector<vector<int >> &res, vector<int >& vec,int  index)      {int > uset;if (index>=nums.size ())return ;for (int  i = index;i<nums.size ();i++)if (i>index&&nums[i]==nums[i-1 ])continue ;if (!vec.empty () && nums[i]<vec.back ())continue ;if (uset.find (nums[i])!=uset.end ())continue ;push_back (nums[i]);insert (nums[i]);if (vec.size ()>1 )push_back (vec);traceback (nums,res,vec,i+1 );pop_back ();            int >> findSubsequences (vector<int >& nums) {int >> res;int > vec;traceback (nums,res,vec,0 );return  res;
回溯优化1 以上版本的代码,用到了unordered_set,又因为总数据量较小,可以用vector替代unordered_set以减少开销
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class  Solution  {public :void  traceback (vector<int >& nums,vector<vector<int >> &res, vector<int >& vec,int  index)      {vector<int > used (201 ,0 )  ;if (index>=nums.size ())return ;for (int  i = index;i<nums.size ();i++)if (i>index&&nums[i]==nums[i-1 ])continue ;if (!vec.empty () && nums[i]<vec.back ())continue ;if (used[nums[i]+100 ]==1 )continue ;push_back (nums[i]);100 ] = 1 ;if (vec.size ()>1 )push_back (vec);traceback (nums,res,vec,i+1 );pop_back ();            int >> findSubsequences (vector<int >& nums) {int >> res;int > vec;traceback (nums,res,vec,0 );return  res;
回溯优化2 另一种回溯模板的解法,不需要用到额外的容器来记录,但较难理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class  Solution  {public :int > vec;int >> res;void  traceback (vector<int >&nums,int  cur,int  last)      {if (cur>=nums.size ())if (vec.size ()>1 )push_back (vec);return ;if (nums[cur]>=last)push_back (nums[cur]);traceback (nums,cur+1 ,nums[cur]);pop_back ();if (nums[cur]!=last)traceback (nums,cur+1 ,last);int >> findSubsequences (vector<int >& nums) {traceback (nums,0 ,-101 );return  res;
选择合法项:只有cur值大于等于last,才会选择该元素(添加到vec中)
不借助容器就能排除重复项:只有last和cur指向的值不相同时,才会不选择该结点(不添加到vec中),也就是说当有多个重复元素时,只选最后一个以避免重复 
拓展 另一种回溯模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 vector<vector<int >> ans;int > temp;void  dfs (int  cur, vector<int >& nums)  if  (cur == nums.size ()) {if  (isValid () && notVisited ()) {push_back (temp);return ;push_back (nums[cur]);dfs (cur + 1 , nums);pop_back ();dfs (cur + 1 , nums);
[子集](# 子集)新解法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class  Solution  {public :int >> res;int > vec;void  traceback (int  cur, vector<int >&nums)      {if (cur>=nums.size ())push_back (vec);return ;push_back (nums[cur]);traceback (cur+1 ,nums);pop_back ();traceback (cur+1 ,nums);int >> subsets (vector<int >& nums) {traceback (0 ,nums);return  res;
[子集II](# 子集II)新解法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class  Solution  {public :int >> res;int > vec;void  traceback (int  cur, vector<int >&nums)      {if (cur>=nums.size ())push_back (vec);return ;push_back (nums[cur]);traceback (cur+1 ,nums);pop_back ();while (cur < nums.size ()-1  && nums[cur]==nums[cur+1 ])traceback (cur+1 ,nums);int >> subsetsWithDup (vector<int >& nums) {sort (nums.begin (),nums.end ());traceback (0 ,nums);return  res;
总结 该题最关键是在不排序的前提下,排除重复项进入递归,需要借助容器,并且是在每层递归中重新定义一个容器,从而避免同层相同元素进入递归 
另一种回溯模板,不对应树形结构 ,因而更难理解;优先处理更多元素的情况,但因其一层两次递归,时间复杂度更高,但指针使用更加灵活,减少对容器的使用,降低空间复杂度
全排列 回溯 排列问题,容易得知,每次递归时需要从0开始遍历数组,这就意味着不能像组合总和一样代入index对递归进行限制。并且,需要借助容器来记录遍历的当前值是否有被使用过。将全排列转化成树形结构,则同一个树枝上不能够有重复值,那么就可以借助全局的vector进行记录,与vec同步增删元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class  Solution  {public :int >> res;int > vec;void  traceback (vector<int >& nums, int  index,vector<int > &used)      {if  (vec.size () == nums.size ())push_back (vec);return ;for  (int  i = 0 ; i < nums.size (); ++i)if  (used[nums[i] + 10 ] == 1 )continue ;push_back (nums[i]);10 ] = 1 ;traceback (nums, index + 1 ,used);10 ] = 0 ;pop_back ();int >> permute (vector<int >& nums) {vector<int > used (21 , 0 )  ;traceback (nums, 0 ,used);return  res;
与[递增子序列](# 递增子序列)一样,可以通过unordered_set以及vector来记录,vector在数组样本小的情况下性能更加优越
总结 
组合之和:只能往后选取(排列顺序不影响结果),故需要用index记录每次递归开始选取的位置;去重则通过排序后再对当前值进行是否与前值重复判断 
分割:与组合之和差别在于终止条件,分割必须要字符串全部分割 
子集:与组合之和差别在于子集的每个结点都是结果集 
全排列:不需要index记录每次递归开始选取的位置,因为有排列顺序要求;去重因为没有index,需要借助容器来记录是否被选取过,通常选vector 
 
全排列II 回溯1 在[全排列](# 全排列)基础上,输入数组可能会有重复值,即不仅在树枝上不能够有重复值,在树层上也不允许有重复值的存在,但是不能直接加上排序后再判断当前值与前一值是否相等的 去重操作。因为重复值的存在,当在同一条路径上,即树枝上允许有重复值的存在(来源于不同的等值元素),针对树枝上的重复值,可以借助vector先遍历nums记录每个数值的个数,用来判断这个树枝上是否还能使用该数值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class  Solution  {public :int >> res;int > vec;void  traceback (vector<int >& nums, int  index,vector<int > &used)      {if  (vec.size () == nums.size ())push_back (vec);return ;for  (int  i = 0 ; i < nums.size (); ++i)if  (used[nums[i] + 10 ] == 0 )continue ;if (i>0 &&nums[i]==nums[i-1 ])continue ;push_back (nums[i]);10 ];traceback (nums, index + 1 ,used);10 ];pop_back ();int >> permuteUnique (vector<int >& nums) {vector<int > used (21 , 0 )  ;sort (nums.begin (),nums.end ());for (int  i:nums)10 ];traceback (nums, 0 ,used);return  res;
关键语句在于if(i>0 && nums[i]==nums[i-1])	continue;,既做到了对同一层上的结点去重,也做到在一棵树枝上对取过的值的去重(全排列的要求,相当于全排列中的used数组) 
回溯2 另一种做法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class  Solution  {public :int >> res;int > vec;void  traceback (vector<int >& nums,vector<bool > &used)      {if  (vec.size () == nums.size ())push_back (vec);return ;for  (int  i = 0 ; i < nums.size (); ++i)if (i>0 &&nums[i]==nums[i-1 ]&&used[i-1 ]==false )continue ;if (used[i]==false )true ;push_back (nums[i]);traceback (nums,used);false ;pop_back ();int >> permuteUnique (vector<int >& nums) {vector<bool > used (nums.size(), false )  ;sort (nums.begin (),nums.end ());traceback (nums,used);return  res;
直观的用used数组表示原始数组中的每一个值是否使用过,是对全排列的进一步改进。不只是加上直接加上排序后再判断当前值与前一值是否相等的 去重操作,为了防止树枝上的多余去重,额外增加了&&used[i-1]==false,限定了只在树层上去重,而不影响树枝上对等值元素的取用
总结 在全排列的基础上,只增加if(i>0 && nums[i]==nums[i-1])	continue;语句会导致竖向上首元素会被重复加入(因为i从0开始),而语句本意是跳过横向重复结点和跳过已加入的竖向重复结点,因此,要么选择再增加对跳过竖向结点的限制,如[回溯2](# 回溯2)用额外used数组记录每个元素是否被访问过,如果前一个元素未被访问nums[i-1]==false,且满足当前元素与前一个元素相同i>0 && nums[i]==nums[i-1],证明这是在横向上的重复结点(树层),应当跳过,而竖向上如果再次访问访问过的结点used[i]==true,则会执行跳过,不重复加入结果中(另一个角度,如果在满足当前元素与前一个元素相同i>0 && nums[i]==nums[i-1]时,前一个元素被访问过nums[i-1]==true,此时当前元素也可以被跳过,保留的是横向上的结点,而竖向重复结点被跳过);要么在if(i>0 && nums[i]==nums[i-1])	continue;语句不能跳过已加入的竖向重复结点和横向重复结点的情况下,限制加入的重复结点数量如[回溯1](# 回溯1)用used数组记录每一个结点的数量,那么当i从开始时,只会最多加入原数组数量的元素,一旦超过就会被跳过。以上两种算法过程都如下图,
重新安排行程 回溯 本题中,关键点在于
记录出发机场和目标机场的容器,同时还要记录是否访问过,防止死循环 
回溯的终止条件,因为一定存在合理路径,所以机票数+1 = 机场数 
容器按字典序排列目标机场,若访问字典序较高的目标机场时,该机场是死胡同,则应返回或者回溯 
回溯的返回值应该是bool类型,用返回值来判断是否应该回溯(删除死胡同元素) 
 
递归终止条件
1 2 3 4 if (tickets.size ()==res.size ()-1 )return  true ;
递归返回值
1 bool traceback(vector <string >&res,vector <vector <string >>& tickets)
当返回值为true时,意味着已添加到结果集的元素符合要求,应该直接返回,而不应回溯(删除);但当返回值为false时,代表着根据字典序优先访问到的这个机场无法再访问其他机场,是个死胡同,应该回溯(pop_back)
递归单层逻辑
1 2 3 4 5 6 7 8 9 10 11 12 for (auto const & ticket:tickets[res[res.size()-1 ]])if (ticket.second >0 )first );second if (traceback(res,tickets))	return  true ;second ++;return  false ;
上述代码在取目的机场的map时,使用范围for循环,auto &ticket:tickets[res[res.size()-1]]等价于pair<const string,int> &ticket:tickets[res[res.size()-1]],其中因为map的key值不能改变,所以一定要用const引用,或者只是用副本pair<string,int>ticket:tickets[res[res.size()-1]]。而当不使用范围for循环时,类型符就要更改为map<const string, int>& ticket = tickets[res[res.size()-1]],因为赋值需要同样的类型或者可转变的类型,而pair不能转变为map
 
整体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class  Solution  {public :int >> targets;bool  traceback (vector<string>& res, vector<vector<string>>& tickets)      {if  (res.size () == tickets.size () + 1 )return  true ;for (auto & target:targets[res[res.size ()-1 ]])if (target.second>0 )push_back (target.first);if (traceback (res,tickets))return  true ;pop_back ();return  false ;vector<string> findItinerary (vector<vector<string>>& tickets)   {push_back ("JFK" );for  (auto  ticket : tickets)0 ]][ticket[1 ]]++;traceback (res, tickets);return  res;
special解法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class  Solution  {public :void  dfs (const  string& curr)  while  (vec.count (curr) && vec[curr].size () > 0 ) {top ();pop ();dfs (move (tmp));emplace_back (curr);vector<string> findItinerary (vector<vector<string>>& tickets)   {for  (auto & it : tickets) {0 ]].emplace (it[1 ]);dfs ("JFK" );reverse (stk.begin (), stk.end ());return  stk;
按字典序从priority_queue中访问目标机场,对新访问的机场进行深度有限搜索,并删除访问过的机场,直到该机场是最后一个机场,则添加到数组当中,再不断返回,将之前访问过的机场也添加到数组中,最后将数组逆序即为结果集
总结 该题巧妙选择容器,记录复杂的信息,用unordered_map记录出发机场和目标机场,是因为不需要对出发机场进行排序,为了减少开销,用map<string,int>按字典序记录目标机场及机票数,不使用multiset单纯记录目标机场,就是为了防止同一张机票的重复访问,防止死循环
在第二种解法中,使用不用的容器来记录,通过优先队列来按字典序记录目标机场,每次访问只需要p.top(),访问时间复杂度是O(1),删除的时间复杂度是O(logm)
破解保险箱 N皇后 [N皇后](51. N 皇后 - 力扣(LeetCode) )
要求在n*n的棋盘上放置n个皇后,并且皇后互相不能攻击,即同行同列包括斜角线上不能有两个皇后
逐行遍历棋盘,因为只需要在一行中摆放一个皇后,则不需要两层循环遍历,同时还要有判断是否冲突的函数
首先,根据规则判断在该位置上放置皇后是否会被攻击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 bool  noattack(vector<string>& chessboard, int  n, int  index , int  row )for (int  i = 0 ;i < n;i++)if (chessboard[i][index ] == 'Q' )return  false ;for (int  i = row  - 1 , j = index  - 1 ; i >= 0  && j >= 0 ; iif (chessboard[i][j] == 'Q' )return  false ;for (int  i = row  - 1 , j = index  + 1 ; i >= 0  && j < n; iif (chessboard[i][j] == 'Q' )return  false ;return  true ;
因为是每一行遍历,因此不需要判断同行是否还有其他皇后;也因为是逐行往下遍历,不需要判断下方是否还有其他皇后,仅判断左上和右上方即可
回溯 
确定递归结束条件
1 2 3 4 5 if(chessboard.size()= = n)
不断添加单行到棋盘中,当棋盘中数量达到n时,就是一个符合要求的棋盘摆法
确定递归返回值
因为可能有多种不同符合规则的摆法,因此返回值为void,遍历所有可能值再返回
确定单层逻辑
1 2 3 4 5 6 7 8 9 for (int  i = 0 ; i < n ; i++)if  (noattack(chessboard, n ,index ,i))index ][i] = 'Q';n , chessboard, index  + 1 );index ][i] = '.';
对单行棋盘中皇后的摆放位置进行枚举,再递归进入下一行,如果发生相互攻击(遍历完所有值都不符合要求)则进行回溯,不发生攻击则继续递归下一行,直到满足要求
 
整体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class  Solution  {public :bool  noattack (const  vector<string>& chessboard, int  n, int  row, int  column)      {for  (int  i = 0 ; i <= row; i++)if  (chessboard[i][column] == 'Q' )return  false ;for  (int  i = row - 1 , j = column - 1 ; i >= 0  &&j >= 0 ; --i, --j)if  (chessboard[i][j] == 'Q' )return  false ;for  (int  i = row - 1 , j = column + 1 ; i >= 0  && j < n; --i, ++j)if  (chessboard[i][j] == 'Q' )return  false ;return  true ;void  traceback (vector<vector<string>>& res, int  n, vector<string>& chessboard, int  index)      {if  (index >= n)push_back (chessboard);return ;for  (int  i = 0 ; i < n; i++)if  (noattack (chessboard, n,index,i))'Q' ;traceback (res, n, chessboard, index + 1 );'.' ;solveNQueens (int  n) {vector<string> chessboard (n, string(n, '.' ))  ;traceback (res, n, chessboard, 0 );return  res;
总结 该题的关键点在于
不需要两层循环,虽然是二维棋盘,但N皇后中不需要在二维棋盘的每一个点上都要放置皇后,仅是判断当前的点是否能放,因此使用单层循环  
回溯不需要返回值,是因为要寻找所有可行的棋盘摆放 ,应当用全局变量或者引用返回 ,同时要遍历所有情况;若只需要找到一种可行的棋盘 ,则返回值应为**bool,一旦找到则 返回true退出递归** 
不需要对同行皇后进行判断,是因为对同行位置进行遍历时,肯定不会发生同一行同时有两个皇后出现 ,一旦某一个位置能够摆放皇后,就会马上进入到下一层递归 ,即到下一行的遍历,而后面回溯回本行时,又会将原位置的皇后删除,再从下一位进行遍历 
 
解数独 [解数独](37. 解数独 - 力扣(LeetCode) )
与上题N皇后 相比,解数独也是在一个二维棋盘上,但不仅要判断该点是否会有冲突,还要在遍历时在每个点上都要填充数字,也就是说,不能像上题一样,如果是单层循环,填完该位置上的数字就进入递归,遍历下一行。因此需要两层循环,填完数字后进入递归,还是从刚填过的位置开始继续填。
同样,首先需要根据规则判断是否有冲突
同行不能有重复数字 
同列不能有重复数字 
同一个3x3格子不能有重复数字 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 bool  ok (vector<vector<char >> & chessboard,int  row, int  col, int  tmp) for (int  i = row;i<9 ;i++)if (chessboard[i][col]==tmp+'0' )return  false ;for (int  j = 0 ;j<9 ;j++)if (chessboard[row][j]==tmp+'0' )return  false ;int  subrow = (row / 3 ) * 3 ;	int  subcol = (col / 3 ) * 3 ;	for (int  i = subrow; i < subrow + 3 ; i++)for (int  j = subcol; j < subcol + 3 ; j++)if (board[i][j]==tmp+'0' )return  false ;return  true ;
可知,需要多次的循环来完成一次的判断。则可以用哈希表的方法,先储存已有的数字,然后在一次循环完成冲突的判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 //全局变量9 ][10 ] {0 };9 ][10 ] {0 };9 ][10 ] {0 };0 ; i < 9 ; i++)0 ; j < 9 ; j++)'.' )'0' ;1 ;1 ;3  + (i/3 )*3 ][tmp] = 1 ;1 )1 )3 +(i/3 )*3 ][tmp]==1 )1 ;1 ;3 +(i/3 )*3 ][tmp] = 1 ;
将三种规则分别替换为三个用二维数组记录的哈希表,行表(row)用第一维表示对应的行数,用第二维表示该行上出现过的数字0-9,如果出现过则置为1,列表(col)同理;由于9宫格中刚好有九个数字,也可以用同样的方法来记录,将棋盘上行列号映射为9个不同的9宫格序号,再记录出现过的数字
回溯 
确定递归结束条件
因为结束时,总是需要遍历棋盘上所有位置,当遍历结束也就是递归的结束,因此不需要递归的结束条件
确定递归返回值
1 bool  traceback (vector<vector<char >>& chessboard, int  row, int  col) 
因为只需要寻找到一种符合的答案,一旦遇到有,则应立即退出递归并返回
递归单层逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 for(int i = 0 ;i<9 ;i++)0 ;j<9 ;j++)'.' )1 ;tmp<10 ;tmp++)'0' ;'.' ;0 ;0 ;3 +(i/3 )*3 ][tmp] = 0 ;
 
整体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 class Solution  {9 ][10 ] = {0 },column[9 ][10 ] = {0 }, subbox[9 ][10 ] = {0 };1 )1 )3 +(i/3 )*3 ][tmp]==1 )1 ;1 ;3 +(i/3 )*3 ][tmp] = 1 ;0 ;i<9 ;i++)0 ;j<9 ;j++)'.' )1 ;tmp<10 ;tmp++)'0' ;'.' ;0 ;0 ;3 +(i/3 )*3 ][tmp] = 0 ;0 ;i<9 ;i++)0 ;j<9 ;j++)'.' )'0' ;1 ;1 ;3 +(i/3 )*3 ][tmp] = 1 ;
总结 该题的关键点在于
递归中需要用双层循环,如果单层循环,则在该行填入一个数字之后进入递归,不管传入怎样的参数都不能使继续填充该行并且完成换行继续填充  
递归不需要结束条件,因为题目需要对棋盘上所有点都进行一次遍历,递归结束时就是得到结果时,即遍历结束即为递归结束 
递归需要返回值,题目保证能找到一个有效的解决方案,且最多只有一个解,因此返回bool值,当找到时,即遍历完棋盘,就返回true;当在棋盘上某一个点遍历完9个数字都没有合适结果时,应当返回false,令上一层递归进行回溯。也就是说,有返回值如bool类型,可以通过判断返回值(如果为真)可以避免进一步递归,同时还能让进入多余分支的递归及时退出,防止了死循环(如本题,在没有递归结束条件的情况下,如果不用返回值限制则会导致死循环);而无返回值的通常是要遍历所有分支,从中找到所有解 
一次循环即可完成对数独是否有效的判断,借助哈希表,将一维的数据通过二维的哈希表进行重复性判断 
 
该题要求从必选数组base中选择一份基料,以及从可选数组topping中选择0份、1份或者2份配料,组成的冰淇淋总价格最接近target且成本最低
回溯 容易想到,给定数据量较少,可以用回溯算法对数组进行遍历,用set保存可行的方案,并最终选择最合适的一个价格返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class Solution {int > vec;void  traceback(vector <int >& toppingCosts, int  target , int  index, int  res)if  (index == toppingCosts.size ())return ;for  (int  i = index; i < toppingCosts.size (); i++)target , i + 1 , res);target , i + 1 , res);2 ;int  closestCost(vector <int >& baseCosts, vector <int >& toppingCosts, int  target ) {int  res = 0 ;for  (int  i = 0 ; i < baseCosts.size (); i++)target , 0 , res);int  ans = INT_MAX;for  (auto it: vec)if  (abs (target  - it) < abs (ans - target ))else  if (abs (target  - it == abs (ans-target )))return  ans;
因为最多可以添加两份配料,则将原数组topping中的元素都复制一遍,再进行回溯,但会超时
则可以对数组topping中的同一个元素进行两次回溯递归,分别将一份和两份配料的价格传递到递归当中
无论价格是多少,都会被存入到set当中,因为难以在存入结果之前对结果的合法性进行判断,所以只能被动地存入所有结果,最后在遍历所有结果的时候,选取最合适的结果
回溯优化 优化在只需要用O(1)的空间存储结果,存储的不再是所有结果,而是最合适的结果,并对其进行维护
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class  Solution {public :int  ans = INT_MAX;int >& toppingCosts, int  target , int  index , int  res)if  (abs (target -ans) > abs (res-target ))if (abs (target -ans) == abs (res-target ) && res < ans )if (res > target )return ;int  i = index ; i < toppingCosts.size (); i++)target , i + 1 , res + toppingCosts[i]);target , i + 1 , res+ 2  * toppingCosts[i]);int  closestCost(vector<int >& baseCosts, vector<int >& toppingCosts, int  target ) {int  res = 0 ;int  i = 0 ; i < baseCosts.size (); i++)target , 0 , res);return  ans;
总结 回溯算法的题目要注意回溯参数i与 index,如果是i,则是允许元素重复加入结果集,在本题中,使用i作为参数传递而不是i+1,则意味着可以使用任意数量的配料topping,不符合题意;添加元素到结果集时,向递归函数传递的参数永远都是i而不是index 
因为有必选数组和可选数组的存在,对必选数组进行for循环遍历,再进行回溯,保证结果集中有必选数组的元素;而必选数组中又可以对不同数量的元素重复使用(同一个元素最多两次),则可以通过两次回溯调用来实现