数据结构与算法-DFS/回溯

回溯backtracking

回溯法思路的简单描述是:把问题的解空间转化成了图或者树的结构表示,然后使用深度优先搜索策略进行遍历,遍历的过程中记录和寻找所有可行解或者最优解。
基本思想类同于:
1.图的深度优先搜索
2.二叉树的后序遍历
分支限界法:广度优先搜索
思想类同于:图的广度优先遍历
二叉树的层序遍历



1. leetcode 39. Combination Sum

题目描述:给一个数组nums和一个目标数target,找出所有数组nums中的数组合和为target的组合,nums数组中的数可以
使用无限次,
Given a set of candidate numbers (C) (without duplicates) and a target number (T), find all unique combinations in C where the candidate numbers sums to T.

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

Note:
All numbers (including target) will be positive integers.
The solution set must not contain duplicate combinations.
For example, given candidate set [2, 3, 6, 7] and target 7, 
A solution set is: 
[
  [7],
  [2, 2, 3]
]

code:

class Solution(object):
    def combinationSum(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        self.resList = []
        candidates = sorted(candidates)
        self.dfs(candidates,[],target,0)
        return self.resList
    def dfs(self, candidates, sublist, target, last):
        if target == 0:
            self.resList.append(sublist[:])
        if target< candidates[0]:
            return 
        for n in candidates:
            if n > target:
                return
            if n < last: # not go back 
                continue
            sublist.append(n)
            self.dfs(candidates,sublist,target - n, n)
            sublist.pop()

if __name__ == '__main__':
    nums = [2,3,6,7]
    Solution = Solution()
    target = 7
    print(Solution.combinationSum(nums,target))

上面的方法,DFS传参中需要传入sublist,候选集,每次需要append进当前的元素,因此最后需要pop()对数据出栈。

自己的code2: 更易懂但效率低,每次要对path求和,自然和为target则结果正确,添加到结果里,> target则return,否则继续DFS

class Solution(object):
    def combinationSum(self, candidates, target):
        candidates.sort()
        res = []
        index = 0 
        self.dfs(candidates,index,target,[],res)
        return res
    
    def dfs(self,candidates,index,target,path,res):
        if sum(path) == target:
            res.append(path)
        if sum(path) > target:
            return 
        for i in range(index,len(candidates)):
            self.dfs(candidates,i,target,path+[candidates[i]],res)
            

更简洁的写法:摘自leetcode discuss:

class Solution(object):
    def combinationSum(self, candidates, target):
        res = []
        candidates.sort()
        self.dfs(candidates, target, 0, [], res)
        return res
        
    def dfs(self, nums, target, index, path, res):
        if target < 0:
            return  # backtracking
        if target == 0:
            res.append(path)
            return 
        for i in xrange(index, len(nums)):
            self.dfs(nums, target-nums[i], i, path+[nums[i]], res)

2.leetcode 40. Combination Sum II

题目和上题leetcode 39 基本类似,唯一不同是数组中的所有元素不是无限次使用,而是有多少只能用多少次

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

Each number in C 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.
For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8, 
A solution set is: 
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]

code1: 完全暴力,不考虑任何优化,和leetcode 39的代码的区别只在于dfs递归里的i变成了i+1(跳过本身),同时leetcode 39中数组是没有重复元素的,leetcode 40里面可能有,所以需要判断重复,这里仅依靠一个字典d的O(1)判断
可以ac leetcode的tests,但是速度很低

class Solution(object):
    def combinationSum2(self, candidates, target):
        candidates.sort()
        res = []
        index = 0 
        self.d = {}
        self.dfs(candidates,index,target,[],res)
        return res
    
    def dfs(self,candidates,index,target,path,res):
        if target == 0 :
            if tuple(path) not in self.d:
                res.append(path)
                self.d[tuple(path)] = 1
            return 
        if target < 0:
            return 
        for i in range(index,len(candidates)):
            self.dfs(candidates,i + 1,target - candidates[i],path+[candidates[i]],res)

code2:
考虑优化:

  1. 当目前path为空,即即将添加第一个元素的时候,判断之前res里是否已有答案是该元素开头
比如target = 8, nums = [1, 1, 2, 5, 6, 7, 10]
则第一个1过完,到第二个1,path肯定已被第一个1的path所包含,就要跳过,否则
[1, 1, 6]
[1, 2, 5]
[1, 7]
[1, 2, 5]
[1, 7]
[2, 6]
可以看到出现了两次(1,2,5)和(1,7)

if i == index and nums[i] == res[-1][0]: # res[-1][0]是上一个答案的开头元素
  1. 当目前的下标不在开头,但是和之前的元素有重复,就直接跳过
比如 nums = [1,2,2,2,5] ,target = 5
答案会为
[1, 2, 2]
[1, 2, 2]
[1, 2, 2]
[5]
在遍历过第一个2的时候就不应该再去看后面的两个2了。
if i != index and candidates[i] == candidate[i-1]:
  continue

可以看出,优化2的代码其实也包含了优化1,同时由于不会重复,没有必要再用字典d去保存已经有的答案。

因此最终代码为

class Solution(object):
    def combinationSum2(self, candidates, target):
        candidates.sort()
        res = []
        index = 0 
        self.dfs(candidates,index,target,[],res)
        return res
    
    def dfs(self,candidates,index,target,path,res):
        if target == 0 :、
            res.append(path)
            return 
        if target < 0:
            return 
        for i in range(index,len(candidates)):
            if i != index and candidates[i] == candidates[i-1]:
                continue
            self.dfs(candidates,i + 1,target - candidates[i],path+[candidates[i]],res)


二维数组的深度遍历(参考leetcode17 Letter Combinations of a Phone Number)

给一个数组[['1','2','3'], ['4','5','6'], ['7','8','9']]
生成排列的组合:(顺序可以任意)
1,4,7,
1,4,8
1,4,9
1,5,7
1,5,8...
3,6,8,
3,6,9

def letter(nums):
    path = ''
    res = []
    index = 0
    dfs(nums,path,index,res)
    return res


def dfs(nums,path,index,res):
    if len(path) == len(nums):
        res.append(path)
        return

    for i in nums[index]:
        dfs(nums,path+i,index+1,res)


if __name__ == '__main__':
    nums = [['a','b','c'],['d','e','f'],['g','h','i','x']]
    # nums = ['a','']
    print(letter(nums))


leetcode22 Generate Parentheses

生成所有N个括号可能的组合
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:

[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
分析:

  1. DFS返回正确结果条件,当前添加的path长度等于2 * n,且左边括号数量要等于右边括号
  2. DFS中途返回的条件:左边或者右边已经添加的次数大于n
    或右边比左边添加的次数要多(如“())” 是不合法的)

方法1: 自己写的:

class Solution(object):
    def generateParenthesis(self, n):
        """
        :type n: int
        :rtype: List[str]
        """
        nums = ['(',')']
        path, index, l, r = '', n, 0, 0
        res = []
        self.dfs(nums,path,index,res,l,r)
        return res
    def dfs(self,nums,path,index,res,l,r):
        if l > index or r > index or r > l:
            return 
        if len(path) == 2 * index:
            res.append(path)
        for j in nums:
            if j == '(':
                self.dfs(nums,path+j,index,res,l+1,r)
            else:
                self.dfs(nums,path+j,index,res,l,r+1)

方法2 leetcode discuss
l和r分别是长度为n的两个栈,一个保存左括号,一个保存右括号,分别出栈,最后两个都为空的时候为结果
如果右边比左边数量还少(已经添加入path更多,illegal),则中途退出,faster

class Solution(object):
    def generateParenthesis(self, n):
        """
        :type n: int
        :rtype: List[str]
        """
        #if not n:
        #    return []
        path,left,right,res ='',n,n,[]
        self.dfs(path,left,right,res)
        return res
    
    def dfs(self,path,left,right,res):
        if right < left:
            return 
        if not left and not right:
            res.append(path[:])
            return 
        if left > 0:
            self.dfs(path + '(',left - 1,right,res)
        if right > 0:
            self.dfs(path + ')',left ,right - 1,res)

按照这个思路也可以给方法1中的dfs的for训练改成if

class Solution(object):
    def generateParenthesis(self, n):
        """
        :type n: int
        :rtype: List[str]
        """
        nums = ['(',')']
        path, index, l, r = '', n, 0, 0
        res = []
        self.dfs(nums,path,index,res,l,r)
        return res
    
    def dfs(self,nums,path,index,res,l,r):
        if r > l or l> index or r > index:
            return 
        if len(path) == 2 * index :
            res.append(path)
        if l <= index:
            self.dfs(nums,path+'(',index,res,l+1,r)
        if r <= index:
            self.dfs(nums,path+')',index,res,l,r+1)

全排列:

[1,2,3] 生成 123 132 213 231 312 321 六种全排列

  1. 网上常见的递归写法:交换与开头元素的位置
class Solution(object):
    def permute(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        end = len(nums)
        begin = 0
        global res 
        res = []
        self.perm(nums,begin,end)
        # res.append(perm(nums,begin,end))
        return res
    
    def perm(self,nums,begin,end):
    
        if begin >= end:
            tmp = nums[:]
            res.append(tmp) 
            
        else:
            for i in range(begin,end):
                nums[begin],nums[i] = nums[i],nums[begin]
                self.perm(nums,begin+1,end)
                nums[begin],nums[i] = nums[i],nums[begin]
  1. DFS写法
    DFS是树型结构
    即第一个节点搜索1,2,3,再每个节点下继续搜索1,2,3,判断return的原则一是碰到了重复元素(相当于剪枝),二是得到正确结果
class Solution(object):
    def permute(self, nums):
        index,path = 0,[]
        res = []
        d = {}
        self.dfs(nums,index,path,res,d)

    def dfs(self,nums,index,path,res,d):
        if len(set(path)) != len(path):
            return
        if len(path) == len(nums): 
            print(path)
            return 
        for i in nums:
            self.dfs(nums,index,path+[i],res,d)

上面的方法中需要将python中的list转化为set,比较长度来判断list是否有重复来完成剪枝,list转换set还是比较耗时的,因此另一种方法,直接在nums中去掉已经添加的元素,代码更简洁也更高效
nums = nums[:i] + nums[i+1:]

class Solution(object):
    def permute(self, nums):
        path = []
        res = []
        length = len(nums)
        self.dfs(nums,path,length,res)
        return res
    
    def dfs(self,nums,path,length,res):
        if len(path) == length:
            res.append(path)
        for i in range(len(nums)):
            self.dfs(nums[:i] + nums[i+1:],path + [nums[i]],length,res)

你可能感兴趣的:(数据结构与算法-DFS/回溯)