算法与数据结构(三):回溯法理论、应用及模板(排列、组合)

1. 入门

有时会遇到这样一类题目,它的问题可以分解,但是又不能得出明确的动态规划或是递归解法,此时可以考虑用回溯法解决此类问题。回溯法的优点 在于其程序结构明确,可读性强,易于理解,而且通过对问题的分析可以大大提高运行效率。但是,对于可以得出明显的递推公式迭代求解的问题,还是不要用回溯法,因为它花费的时间比较长。

回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。可以认为回溯算法一个”通用解题法“,这是由他试探性的行为决定的,就好比求一个最优解,我可能没有很好的概念知道怎么做会更快的求出这个最优解,但是我可以尝试所有的方法,先试探性的尝试每一个组合,看看到底通不通,如果不通,则折回去,由最近的一个节点继续向前尝试其他的组合,如此反复。这样所有解都出来了,在做一下比较,能求不出最优解吗?

2. 基本定义和概念

回溯法中,首先需要明确下面三个概念:

  • 约束函数:约束函数是根据题意定出的。通过描述合法解的一般特征用于去除不合法的解,从而避免继续搜索出这个不合法解的剩余部分。因此,约束函数是对于任何状态空间树上的节点都有效、等价的。
  • 状态空间树:刚刚已经提到,状态空间树是一个对所有解的图形描述。树上的每个子节点的解都只有一个部分与父节点不同。
  • 扩展节点、活结点、死结点:所谓扩展节点,就是当前正在求出它的子节点的节点,在DFS中,只允许有一个扩展节点。活结点就是通过与约束函数的对照,节点本身和其父节点均满足约束函数要求的节点;死结点反之。由此很容易知道死结点是不必求出其子节点的(没有意义)。

3. 为什么用DFS

深度优先搜索(DFS)和广度优先搜索(FIFO)在分支界限法中,一般用的是FIFO或最小耗费搜索;其思想是一次性将一个节点的所有子节点求出并将其放入一个待求子节点的队列。通过遍历这个队列(队列在 遍历过程中不断增长)完成搜索。而DFS的作法则是将每一条合法路径求出后再转而向上求第二条合法路径。而在回溯法中,一般都用DFS。为什么呢?这是因 为可以通过约束函数杀死一些节点从而节省时间,由于DFS是将路径逐一求出的,通过在求路径的过程中杀死节点即可省去求所有子节点所花费的时间。FIFO 理论上也是可以做到这样的,但是通过对比不难发现,DFS在以这种方法解决问题时思路要清晰非常多。

回溯法可以被认为是一个有过剪枝的DFS过程,利用回溯法解题的具体步骤如下:

首先,要通过读题完成下面三个步骤:

  1. 描述解的形式,定义一个解空间,它包含问题的所有解;
  2. 构造状态空间树;
  3. 构造约束函数(用于杀死节点);

然后就要通过DFS思想完成回溯,完整过程如下:

  1. 设置初始化的方案(给变量赋初值,读入已知数据等)。
  2. 变换方式去试探,若全部试完则转(7)。
  3. 判断此法是否成功(通过约束函数),不成功则转(2)。
  4. 试探成功则前进一步再试探。
  5. 正确方案还未找到则转(2)。
  6. 已找到一种方案则记录并打印。
  7. 退回一步(回溯),若未退到头则转(2)。
  8. 已退到头则结束或打印无解。

4. 回溯方法的步骤

回溯方法的步骤如下:

  1. 定义一个解空间,它包含问题的解。
  2. 用适于搜索的方式组织该空间。
  3. 用深度优先法搜索该空间,利用限界函数避免移动到不可能产生解的子空间。

5. 回溯模板

首先我们来看一道题目:

Combinations:Given two integers n and k,return all possible combinations of k numbers out of 1 … n. For example, If n = 4 and k =2, a solution is:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
即:给你两个整数 n和k,从1-n中选择k个数字的组合。比如n=4,那么从1,2,3,4中选取两个数字的组合,包括图上所述的四种。

然后我们看看题目给出的框架:

public class Solution {
    public List<List<Integer>> combine(int n, int k) {
       
    }
}

要求返回的类型是List<List> 也就是说将所有可能的组合list(由整数构成)放入另一个list(由list构成)中。
现在进行套路教学:要求返回List<List>,那我就给你一个List<List>,因此

  1. 定义一个全局List<List> result=new ArrayList<List>();
  2. 定义一个辅助的方法(函数)public void backtracking(int n,int k, Listlist){}
    n, k 总是要有的吧,加上这两个参数,前面提到List<Integer> 是数字的组合,也是需要的吧,这三个是必须的,没问题吧。(可以尝试性地写参数,最后不需要的删除)
  3. 接着就是我们的重头戏了,如何实现这个算法?对于n=4,k=2,1,2,3,4中选2个数字,我们可以做如下尝试,加入先选择1,那我们只需要再选择一个数字,注意这时候k=1了(此时只需要选择1个数字啦)。当然,我们也可以先选择2,3 或者4,通俗化一点,我们可以选择 1~n 的所有数字,这个是可以用一个循环来描述?每次选择一个加入我们的链表list中,下一次只要再选择k-1个数字。那什么时候结束呢?当然是 k<0 的时候啦,这时候都选完了。
    有了上面的分析,我们可以开始填写
    public void backtracking(int n,int k, List<Integer> list){}
    中的内容。
publicvoid backtracking(int n,int k,int start,List<Integer> list){
        if(k<0)        return;
        else if(k==0){
            //k==0表示已经找到了k个数字的组合,这时候加入全局result中
            result.add(new ArrayList(list));
 
        }else{
            for(int i=start;i<=n;i++){
                list.add(i);    //尝试性的加入i
                //开始回溯啦,下一次要找的数字减少一个所以用k-1,i+1见后面分析
                backtracking(n,k-1,i+1,list);
                //(留白,有用=。=)
            }
        }
    }

观察一下上述代码,我们加入了一个start变量,它是i的起点。为什么要加入它呢?比如我们第一次加入了1,下一次搜索的时候还能再搜索1了么?肯定不可以啊!我们必须从他的下一个数字开始,也就是2 、3或者4啦。所以start就是一个开始标记这个很重要啦!
这时候我们在主方法中加入backtracking(n,k,1,list);调试后发现答案不对啊!为什么我的答案比他长那么多?
在这里插入图片描述

回溯回溯当然要退回再走啦,你不退回,当然又臭又长了!所以我们要在刚才代码注释留白处加上退回语句。仔细分析刚才的过程,我们每次找到了1,2这一对答案以后,下一次希望2退出然后让3进来,1 3就是我们要找的下一个组合。如果不回退,找到了2 ,3又进来,找到了3,4又进来,所以就出现了我们的错误答案。正确的做法就是加上:list.remove(list.size()-1);他的作用就是每次清除一个空位 让后续元素加入。寻找成功,最后一个元素要退位,寻找不到,方法不可行,那么我们回退,也要移除最后一个元素。
所以完整的程序如下:

public class Solution {
   List<List<Integer>> result=new ArrayList<List<Integer>>();
   public List<List<Integer>> combine(int n, int k) {
       List<Integer> list=new ArrayList<Integer>();
       backtracking(n,k,1,list);
       return result;
    }
   public void backtracking(int n,int k,int start,List<Integer>list){
       if(k<0) return ;
       else if(k==0){
           result.add(new ArrayList(list));
       }else{
           for(int i=start;i<=n;i++){
                list.add(i);
                backtracking(n,k-1,i+1,list);
                list.remove(list.size()-1);
            }
       }
    }
}

是不是有点想法了?那么我们操刀一下。

6. 应用实例

6.1 LeetCode 39. Combination Sum(组合总数)

Given a set of candidate numbers (candidates) (without duplicates) and a target number (target), find all unique combinations in candidates where the candidate numbers sums to target.

The same repeated number may be chosen from candidates unlimited number of times.

Note:

  • All numbers (including target) will be positive integers.
  • The solution set must not contain duplicate combinations.

题目:
给定一组候选数字 (候选) (不带重复项) 和目标数字 (目标), 可以在候选数字总和为目标的候选项中查找所有唯一的组合。

同样重复的数字可以从候选者无限次数中选择。

Example 1:

Input: candidates = [2,3,6,7], target = 7,
A solution set is:
[
  [7],
  [2,2,3]
]

Example 2:

Input: candidates = [2,3,5], target = 8,
A solution set is:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

按照前述的套路走一遍:

public class Solution {
   List<List<Integer>> result=new ArrayList<List<Integer>>();
   public List<List<Integer>> combinationSum(int[] candidates,int target) {
       Arrays.sort(candidates);
       List<Integer> list=new ArrayList<Integer>();
       return result;
    }
   public void backtracking(int[] candidates,int target,int start,){        
    }
}
  1. 全局List<List> result先定义
  2. 回溯backtracking方法要定义,数组candidates 目标target 开头start 辅助链表List list都加上。
  3. 分析算法:以[2,3,6,7] 每次尝试加入数组任何一个值,用循环来描述,表示依次选定一个值
for(inti=start; i<candidates.length; i++){

           list.add(candidates[i]);

       }

接下来回溯方法再调用。比如第一次选了2,下次还能再选2是吧,所以每次start都可以从当前i开始(ps:如果不允许重复,从i+1开始)。第一次选择2,下一次要凑的数就不是7了,而是7-2,也就是5,一般化就是remain = target - candidates[i],所以回溯方法为:

backtracking(candidates, target-candidates[i], i, list);

然后加上退回语句:list.remove(list.size()-1);
那么什么时候找到的解符合要求呢?自然是remain(注意区分初始的target)= 0 了,表示之前的组合恰好能凑出target。如果 remain < 0 表示凑的数太大了,组合不可行,要回退。当remain>0 说明凑的还不够,继续凑。
所以完整方法如下:

publicclass Solution {
    List<List<Integer>> result=newArrayList<List<Integer>>();
    public List<List<Integer>>combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);//所给数组可能无序,排序保证解按照非递减组合
        List<Integer> list=newArrayList<Integer>();
        backtracking(candidates,target,0,list);//给定target,start=0表示从数组第一个开始
        return result;//返回解的组合链表
    }
    public void backtracking(int[]candidates,int target,int start,List<Integer> list){
       
            if(target<0)    return;//凑过头了
            else if(target==0){
               
                result.add(newArrayList<>(list));//正好凑出答案,开心地加入解的链表
               
            }else{
                for(inti=start;i<candidates.length;i++){//循环试探每个数
                    list.add(candidates[i]);//尝试加入
		   //下一次凑target-candidates[i],允许重复,还是从i开始
                   backtracking(candidates,target-candidates[i],i,list);                   
		   list.remove(list.size()-1);//回退
                }
            }
       
    }
}

其对应的python版本如下:

class Solution:
    def combinationSum(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        candidates.sort()
        Solution.res = []
        self.DFS(candidates, target, 0, [])
        
        return Solution.res
    
    def DFS(self, candidates, target, start, temp_res):
        if target == 0:
            Solution.res.append(temp_res[:])
            return
        for i in range(start, len(candidates)):
            if candidates[i] > target:
                return
            self.DFS(candidates, target - candidates[i], i, temp_res + [candidates[i]])
            

这里一定很迷惑,为什么转到了python版本之后就不用后退了呢?(对应list.remove(list.size()-1);
事实上,问题出在了
self.DFS(candidates, target - candidates[i], i, temp_res + [candidates[i]])

由于python解决方案中直接将temp_res + [candidates[i]]放在了递归语句中,则在递归遇到不满足条件跳出时,对应的temp_res也会将之前输入时加上的[candidates[i]]去掉;而上面java程序采用的是先将[candidates[i]]加到了temp_res,再传入递归程序DFS中,故而即便是跳出递归,temp_res并不会去除之前加上的temp_res,需要手动再加上后退程序:temp_res.pop().

故而也可看与java对应的python版本:

class Solution:
    def combinationSum(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        candidates.sort()
        Solution.res = []
        self.DFS(candidates, target, 0, [])

        return Solution.res

    def DFS(self, candidates, target, start, temp_res):
        if target == 0:
            Solution.res.append(temp_res[:])
            return
        for i in range(start, len(candidates)):
            if candidates[i] > target:
                return
            temp_res.append(candidates[i])
            self.DFS(candidates, target - candidates[i], i, temp_res)
            temp_res.pop()
            

注:还是推荐采用下面这种方式,因为直接将对temp_res的操作放在递归程序的输入函数中,容易出现一些问题;本题之所以成果是因为temp_res + [candidates[i]]的妥当使用,事实上,如果将其换为temp_res.append(candidates[i]),程序就会出现错误:
在这里插入图片描述

从debug的过程发现,出错原因在使用temp_res + [candidates[i]]会立刻对temp_res进行转换;而采用temp_res.append(candidates[i])temp_res并不会立刻发生变化,而是直到下次达到此语句时候才进行了变化,与希望过程不符。

6.2 LeetCode 46. Permutations(排列)

Given a collection of distinct integers, return all possible permutations.

Example:

Input: [1,2,3]
Output:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

对这种排列题,则无需再设定起始点 start,不然排列总数不够(因为加上起始点后,每次再从 nums 挑选数字到 temp_res 时,不能再回过去挑了,如上例,当 start = 1 时,只能从 input[2, 3] 中挑选,不能再回过去选 1),但排列的情况则是输入中的任意数字均可作为开始起点,故而,每次,均是从 start = 0 开始遍历;
其次,因为这道题输入无重复数字,故而每次遍历时候只需要判定一次该数字是否已经遍历过,可直接使用 if nums[i] not in temp_res: 进行判定,无需再设置标记数组(用于处理输入含重复数字情况)便可得到结果。

再提供一种递归法,比较容易理解与操作:

 class Solution(object):
    def permute(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """ 
        if not nums:
            return []
        self.res = []
        temp_res = []
        
        res = []
        self.dfs(nums, res, [])
        return res
   
        
    def dfs(self, nums, res, path):
        if not nums:
            res.append(path)
        else:
            for i in range(len(nums)):
                self.dfs(nums[:i] + nums[i+1:], res, path + [nums[i]])
    
        

6.3 LeetCode 47. Permutations II(排列2)

Given a collection of numbers that might contain duplicates, return all possible unique permutations.

Example:

Input: [1,1,2]
Output:
[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]

这道题与上道题最大区别点就在于输入含有重复数字,如果还按照上述题解法,则不可能得到结果(对应重复数字只能处理一次);

  • 但设想,是否直接去掉重复判定 if nums[i] not in temp_res: 就可以了,答案依旧是不行,去掉了重复判定,每次都从 0 开始重新遍历,会出现每个元素直接重复 len(nums) 结果,如 [2,2,2], [1,1,1] 之类;
  • 再设想,是否可以通过添加 for 循环中遍历开始点 start,通过 for i in range(start, nums) 每次遍历过程 start + 1 来处理呢,结果依旧是不行,问题同上一个例题所述,这种情况无法出现输入后面元素作为起始位置的排列,如本题的 [2,1,1]
  • 因此,需要借助标记列表 mark 用于标记一个输入元素是否已经遍历

下面先据此思想,写出程序如下:

错误版本:

class Solution(object):
    def permuteUnique(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        if not nums: return []
        self.res = []
        # self.permute(nums, res, [])
        # self.helper(nums, [])
        mark = [False] * len(nums)
        self.dfs(nums, [], mark)
        return self.res
    
    def dfs(self, nums, temp_res, mark):
        if len(temp_res) > len(nums):
            return 
        if len(temp_res) == len(nums):
            self.res.append(temp_res)
            return 
        for i in range(len(nums)):
            if mark[i]: continue
            mark[i] = True
            self.dfs(nums, temp_res + [nums[i]], mark)
            mark[i] = False

结果如下:
在这里插入图片描述

通过检查结果不难得知,即便加了 mark 列表,只能保证元素不管是否重复,都会完成全排列(因此,这道题的结果完全可以对应上一道题的答案,因为上一道题输入不重复,全遍历就是正确结果);

此时,需要在判定过程中排除出重复的 temp_res,排除方法有两种:

  • 一种是在添加 temp_res 时,判定 and temp_res not in self.res 的被动后期结果过滤法:
  • 一种是 for 训练中直接提前过滤,即先对数组进行排序,保证相等的数字放在一起,然后当我们遇到的不是第一个数字,并且现在的数字和前面的数字相等,同时前面的数字还没有访问过,我们是不能搜索的,需要直接返回。原因是,这种情况下,必须是由前面搜索到现在的这个位置,而不能是由现在的位置向前面搜索。

方式一:

class Solution(object):
    def permuteUnique(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        if not nums: return []
        self.res = []
        # self.permute(nums, res, [])
        # self.helper(nums, [])
        mark = [False] * len(nums)
        self.dfs(nums, [], mark)
        return self.res
    
    def dfs(self, nums, temp_res, mark):
        if len(temp_res) > len(nums):
            return 
        if len(temp_res) == len(nums) and temp_res not in self.res:
            self.res.append(temp_res)
            return 
        for i in range(len(nums)):
            if mark[i]: continue
            mark[i] = True
            self.dfs(nums, temp_res + [nums[i]], mark)
            mark[i] = False

方式二:

class Solution(object):
    def permuteUnique(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        if not nums: return []
        self.res = []
        # self.permute(nums, res, [])
        # self.helper(nums, [])
        mark = [False] * len(nums)
        nums.sort()
        self.dfs(nums, [], mark)
        return self.res
       
       
    def dfs(self, nums, temp_res, mark):
        if len(temp_res) > len(nums):
            return 
        if len(temp_res) == len(nums):
            self.res.append(temp_res)
            return 
        for i in range(len(nums)):
            # if mark[i]: continue
            if mark[i] or (i > 0 and nums[i] == nums[i-1] and mark[i-1]): continue
                
            mark[i] = True
            self.dfs(nums, temp_res + [nums[i]], mark)
            mark[i] = False

这种主动过滤方式效率更高,运行时间快了十倍,但是代码相对麻烦;

最后,再提供一种简便思路,依旧采用后验式,代码如下:

class Solution(object):
    def permuteUnique(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        if not nums: return []
        self.res = []
        # self.permute(nums, res, [])
        # self.helper(nums, [])
        mark = [False] * len(nums)
        self.dfs(nums, [], mark)
        return self.res
        
        
    def permute(self, nums, res, temp_res):
        if not nums and temp_res not in res:
            res.append(temp_res[:])
            
        for i in range(len(nums)):
            self.permute(nums[:i] + nums[i+1:], res, temp_res + [nums[i]])

LeetCode 77. Combinations(组合)

Given two integers n and k, return all possible combinations of k numbers out of 1 … n.

Example:

Input: n = 4, k = 2
Output:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

这道题自以为按照 java 那种格式直接在 i 遍历时候加上 start 起始变量即可,事实上却走不通

递归法:

这个题要找到组合,组合和排列的不同之处在于组合的数字出现是没有顺序意义的。

剑指offer的做法是找出n个数字中m的数字的组合方法是,把n个数字分成两部分:第一个字符和其他的字符。如果组合中包括第一个字符,那么就在其余字符中找到m-1个组合;如果组合中不包括第一个字符,那么就在其余字符中找到m个字符。所以变成了递归的子问题。

我的做法如下,这个之中用到了if k > len(array)的做法,防止数组过界陷入死循环(其作用主要是对第二个递归而言的)。

class Solution(object):
    def combine(self, n, k):
        """
        :type n: int
        :type k: int
        :rtype: List[List[int]]
        """
        nums = [i+1 for i in range(n)]
        self.res = []
        temp_res = []
        # self.dfs(nums, k, 0, temp_res)
        self.recusion(nums, k, temp_res)
        return self.res
    
    def recusion(self, nums, k, temp_res):
        if k > len(nums):
            return
        if k == 0:
            self.res.append(temp_res)
            return 

        self.recusion(nums[1:], k - 1, temp_res + [nums[0]])
        self.recusion(nums[1:], k, temp_res)

回溯法:

class Solution(object):
    def combine(self, n, k):
        """
        :type n: int
        :type k: int
        :rtype: List[List[int]]
        """
        
        nums = [i+1 for i in range(n)]
        self.res = []
        temp_res = []
        self.dfs(nums, k, temp_res)
        # self.recusion(nums, k, temp_res)
        return self.res
    
    
    def dfs(self, nums, k, temp_res):
        if k < 0:
            return
        if k == 0:
            self.res.append(temp_res[:])
            return
        for i in range(len(nums)):
            self.dfs(nums[i+1:], k-1, temp_res + [nums[i]])

回溯法中值得注意的是,每次回溯,修改的是 array 的输入长度,以及 k,没有设置 start 变量,实验了很多次,不好使,上述写法更实用!

上述约束中,使用 if k > len(nums) 要比使用 if k < 0: 约束更强,回溯更快!

LeetCode 39. Combination Sum

Given a set of candidate numbers (candidates) (without duplicates) and a target number (target), find all unique combinations in candidates where the candidate numbers sums to target.

The same repeated number may be chosen from candidates unlimited number of times.

Note:

  • All numbers (including target) will be positive integers.
  • The solution set must not contain duplicate combinations.

Example 1:

Input: candidates = [2,3,6,7], target = 7,
A solution set is:
[
  [7],
  [2,2,3]
]

Example 2:

Input: candidates = [2,3,5], target = 8,
A solution set is:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

这道题也是一种变形,很值得注意的是,这道题就需要 start 变量,不然最终的结果会出现重复,如下:

错误版本:

class Solution(object):
    def combinationSum(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        self.res = []
        self.dfs(candidates, target, [])
        return self.res
        
        
    def dfs(self, candidates, target, temp_res):
        if target == 0:
            self.res.append(temp_res)
            return
        if target < 0:
            return
        for i in range(len(candidates)):
            if candidates[i] > temp_res:
                continue
            self.dfs(candidates, target - candidates[i], temp_res + [candidates[i]])

在这里插入图片描述

从错误中不难发现,错误原因在于出现重复项,对于这种情况,明显是希望当遍历一项之后,不再取前面的项,但可以取本项,故而要加上 start 来约束遍历范围,正确代码如下:

正确版本:

class Solution(object):
    def combinationSum(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        self.res = []
        self.dfs(candidates, 0, target, [])
        # ret =  set(map(lambda x: sorted(x), self.res))
        return self.res
        
        
    def dfs(self, candidates, start, target, temp_res):
        if target == 0:
            self.res.append(temp_res)
            return
        if target < 0:
            return
        for i in range(start, len(candidates)):
            if candidates[i] > temp_res:
                continue
            self.dfs(candidates, i, target - candidates[i], temp_res + [candidates[i]])

LeetCode 40. Combination Sum II

Given a collection of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sums to target.

Each number in candidates may only be used once in the combination.

Note:

  • All numbers (including target) will be positive integers.
  • The solution set must not contain duplicate combinations.

Example 1:

Input: candidates = [10,1,2,7,6,1,5], target = 8,
A solution set is:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]

Example 2:

Input: candidates = [2,5,2,1,2], target = 5,
A solution set is:
[
  [1,2,2],
  [5]
]

这道题与上一道类似,但是又有不同,区别点在于不允许出现过的词再出现,故而,需要 start 每次 +1,但实验会发现,这样依旧存在问题,错误代码如下:

错误版本:

class Solution(object):
    def combinationSum2(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        self.res = []
        candidates.sort()
        self.dfs(candidates, 0, target, [])
        return self.res
        
        
    def dfs(self, candidates, start, target, temp_res):
        if target == 0:
            self.res.append(temp_res)
            return
        if target < 0:
            return
        for i in range(start, len(candidates)):
            # if i > start and candidates[i] == candidates[i-1]:
            #     continue
            self.dfs(candidates, i + 1, target - candidates[i], temp_res + [candidates[i]])

在这里插入图片描述

分下发现,其错误的主要原因是出现重复结果,这时又是跟上面一样,有两种解决思路,一种先找再过滤;一种是直接在遍历过程中直接忽略掉;

先找再过滤版本:

class Solution(object):
    def combinationSum2(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        self.res = []
        candidates.sort()
        self.dfs(candidates, 0, target, [])
        return self.res
        
        
    def dfs(self, candidates, start, target, temp_res):
        if target == 0:
            if temp_res not in self.res:
                self.res.append(temp_res)
            return
        if target < 0:
            return
        for i in range(start, len(candidates)):
            # if i > start and candidates[i] == candidates[i-1]:
            #     continue
            self.dfs(candidates, i + 1, target - candidates[i], temp_res + [candidates[i]])

直接在回溯过程中过滤:

这时候,要注意的是,过滤条件的确定,由答案可知,过滤是想过滤掉每次,以后回溯时,后面出现跟前面重复的情况(对应 i > start and candidates[i] == candidates[i-1]),尤其注意是 i > start,不是 i > 1,因为是向后遍历的!

代码如下:

class Solution(object):
    def combinationSum2(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        self.res = []
        candidates.sort()
        self.dfs(candidates, 0, target, [])
        return self.res
        
        
    def dfs(self, candidates, start, target, temp_res):
        if target == 0:
            if temp_res:
                self.res.append(temp_res)
            return
        if target < 0:
            return
        for i in range(start, len(candidates)):
            if i > start and candidates[i] == candidates[i-1]:
                continue
            self.dfs(candidates, i + 1, target - candidates[i], temp_res + [candidates[i]])

LeetCode 216. Combination Sum III

Find all possible combinations of k numbers that add up to a number n, given that only numbers from 1 to 9 can be used and each combination should be a unique set of numbers.

Note:

  • All numbers will be positive integers.
  • The solution set must not contain duplicate combinations.

Example 1:

Input: k = 3, n = 7
Output: [[1,2,4]]

Example 2:

Input: k = 3, n = 9
Output: [[1,2,6], [1,3,5], [2,3,4]]

依据上面思路不难分析,结果如下:

class Solution(object):
    def combinationSum3(self, k, n):
        """
        :type k: int
        :type n: int
        :rtype: List[List[int]]
        """
        self.res = []
        nums = [i + 1 for i in range(9)]
        self.dfs(nums, 0, k, n, [])
        return self.res
    
        
    def dfs(self, nums, start, k, target, temp_res):
        if target == 0 and len(temp_res) == k:
            self.res.append(temp_res)
            return
        if target < 0 or len(temp_res) > k:
            return
        for i in range(start, len(nums)):
            self.dfs(nums, i + 1, k, target - nums[i], temp_res + [nums[i]])

LeetCode 22. Generate Parentheses(括号生成)

原题

Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.

For example, given n = 3, a solution set is:

题目:
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。

Example:

[
  "((()))",
  "(()())",
  "(())()",
  "()(())",
  "()()()"
]

Solution:

class Solution:
    def generateParenthesis(self, n):
        """
        :type n: int
        :rtype: List[str]
        """
        Solution.res = []
        n, start, temp_res, len_left, len_right, candidate_list = n, 0, "", 0, 0, ["(", ")"]
        self.backtracking(n, len_left, len_right, temp_res, candidate_list)
        return Solution.res

    def backtracking(self, n, len_left, len_right, temp_res, candidate_list):

        # len_left = len_right = 0

        if len_left == n and len_right == n:
            Solution.res.append(temp_res[:])

        if len_left > n or len_right > n or len_right > len_left:
            return

        if len(temp_res) < 2 * n:
            for i in range(len(candidate_list)):
                temp_res += candidate_list[i]
                if candidate_list[i] == "(":
                    self.backtracking(n, len_left + 1, len_right, temp_res, candidate_list)
                else:
                    self.backtracking(n, len_left, len_right + 1, temp_res, candidate_list)
                temp_res = temp_res[: len(temp_res) - 1]

依旧采用的回溯法,只是针对字符串进行操作需要注意:

  1. 对于字符串进行回溯,一个很好的方法是直接写入回溯表达式(对于列表格式慎用,append不支持这种事实上赋值),如上述for循环表达式,可以替换为:
        if len(temp_res) < 2 * n:
            for i in range(len(candidate_list)):
                # temp_res += candidate_list[i]
                if candidate_list[i] == "(":
                    self.backtracking(n, len_left + 1, len_right, temp_res + candidate_list[i], candidate_list)
                else:
                    self.backtracking(n, len_left, len_right + 1, temp_res + candidate_list[i], candidate_list)
                # temp_res = temp_res[: len(temp_res) - 1]
  1. 对于回溯法字符串删除,可采用 temp_res = temp_res[: len(temp_res) - 1] 曲线救国。

参考文献:

回溯详解及其应用:Leetcode 39 combination sum
[python]回溯法模板
手把手教你中的回溯算法——多一点套路

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值