DFS/回溯专题

DFS&&回溯法

写在前面的话:感谢liweiwei大佬的解析
回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:

  • 找到一个可能存在的正确的答案;
  • 在尝试了所有可能的分步方法后宣告该问题没有答案。

深度优先搜索 算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会 尽可能深 的搜索树的分支。当结点 v 的所在边都己被探寻过,搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。

「回溯算法」与「深度优先遍历」都有「不撞南墙不回头」的意思。我个人的理解是:「回溯算法」强调了「深度优先遍历」思想的用途,用一个 不断变化 的变量,在尝试各种可能的过程中,搜索需要的结果。强调了 回退 操作对于搜索的合理性

题型一(排列,组合,子集问题)

全排列问题

  • 全排列
    对于回溯问题以及DFS,最好用的一种方法就是画图。(图片上传总是失败!died) 。对于全排列问题,此题显示的是没有重复数字的序列,且给定的就是无重复序列,那么就不需要设置begin参数,(begin参数后续会讲),但是我们需要设置used序列,只要当前数被使用了,就记为转态True,表示这一条路径上这个数字以及被使用了,但是DFS走到这一条路径的头的时候,返回必须将原状态复原,且转态数组也需要pop(),用以还原原来的转态
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        size = len(nums)
        used = [False for _ in range(size)]
        def dfs(nums,index,temp,used):
            if index == len(nums):
                res.append(temp[:])
                return
            for i in range(len(nums)): # 每一次都遍历整个数组
                if not used[i]: # 知道数组中的元素没有被标记为使用,就可以添加到temp中去
                    used[i] = True
                    temp.append(nums[i])
                    dfs(nums,index+1,temp,used)
                    used[i]=False
                    temp.pop()
        dfs(nums,0,[],used)
        return res
  • 全排列II
    此题中要求的是可以包含重复序列的数列,但是排列不能相同,这题和上一题唯一不同的点就是允许排列中可以出现相同的数字
class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        # 回溯+ 深度优先搜索算法
        def dfs(nums,size,depth,temp,used):
            if depth == len(nums):
                res.append(temp.copy()) # 深拷贝,以及完全拷贝,新的地址,原来的变化,新的不会跟着变化
                return
            for i in range(size):
                if not used[i]:
                    if i>0 and nums[i] == nums[i-1] and not used[i-1]:
                        continue
                    # 如果前一个没有没用到,那么后一个相同的也不会被使用到
                    # 目的是避免同一层级出现相同的情况。
                    # 全排列问题需要有一个used数组类对应记录当前这个值是否被使用过,用来避免重复
                    used[i] = True
                    temp.append(nums[i])
                    dfs(nums,size,depth+1,temp,used)
                    temp.pop()
                    used[i] =False
        size = len(nums)
        if size == 0:
            return []
        res =[]
        used = [False for _ in range(size)]
        nums.sort()
        dfs(nums,size,0,[],used)
        return res

这一题最关键的地方就是剪枝的部分,我们可以想象输入为[1,1,2]的情况下的树形图(上传失败的图片)。只有当前在进入到 1-> 1->2,1->2->1 ,且遍历完了这个支线下的所有路径, 从第一位到第二位的1时,因为是排序后的数组,相同元素必在一起,同一个相同元素在DFS回溯算法下只需要第一条路径就会遍历完所有可能的路径,所以这就是为什么( not used[i-1]) 满足就会直接跳过,因为当 nums[0] == nums[1] ==1 的时候,且此时nums[0] 为False,说明是遍历完回退回来的状态,表示相同元素的所有路径已经遍历完了,所以直接跳过nums[1]

if not used[i]:
     if i>0 and nums[i] == nums[i-1] and not used[i-1]:
                        continue

组合问题

  • 组合总和
    给定一个无重复元素的数组 candidates 和一个目标数 target ,找出candidates 中所有可以使数字和为 target 的组合.(candidates 中的数字可以无限制重复被选取。)
    说明:(所有数字(包括 target)都是正整数。解集不能包含重复的组合.)
    题目分析:此题比较容易分析,返回条件就是当target的值减到了0,就将路径添加到res中,这样最好是添加的是路径的深拷贝,这样不会因为后续的代码影响到路径的值。
    剪枝分析:只要当前递归的值比target的值大,直接break,后续的遍历就不需要再看了
class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        def dfs(candidates, begin, size, path, res, target):
            if target == 0:
                res.append(path.copy())
                return
            for index in range(begin, size):
                # 剪枝,target减一个较小的数是负数,那么减一个较大的数也是负数
                residue = target - candidates[index]
                if residue < 0:
                    break
                dfs(candidates, index, size, path + [candidates[index]], res, residue)
        # 若是可以重复选择元素,则begin到递归的这一步就不用+1,
        # +1是保证每一个元素在每一次分支中只可以选择一次,而不加1,保证index和begin相同
        # 是可以让同一个元素在多次递归中可以每一次都能选择到这个相同的值        
        
        size = len(candidates)
        if size == 0:
            return []
        candidates.sort()
        path = []
        res = []
        dfs(candidates, 0, size, path, res, target)
        return res
  • 组合总和II
    组合问题的改版, 只不过这一次不能使用重复的元素了, 由于不能使用重复的元素,数组可以先做排序,这样每次进入递归,只要将begin变量的值设置为当前值的下标加1,就不会遍历到相同的元素。同理,先寻找返回条件,很明显,当target的值变为0的时候,返回当前路径的深拷贝
class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        def dfs(size,residue,temp,begin):
            if residue == 0:
                res.append(temp.copy())
                return
            for i in range(begin,size): 
                #避免重复元素
                if candidates[i] > residue:
                    break # 一旦数组内的值比剩余的值大,直接break当前循环,返回上一层
                if i>begin and candidates[i] == candidates[i-1]:
                    continue
                # 这是在同一层级里面的 两个相同元素的分支,由于遍历元素会遍历
                # [2,2,5],所以 此时 candiates[begin] == 2,由于i>begin,所以不会跳过相同的元素,
                # 而且元素是以及排序好的数组
                # 且 candiates[i]若和candiates[i-1]相同,当前两个元素都去的话,就会出现两个[1,2,2,5]
                # 而只要在这一步跳过相同元素,进入下一层,则前一个相同元素就不会以及添加到了temp中,
                #这样后一个元素在下一层也会被添加到temp中,避免出现相同的队列的情况
                # 这一步的过程是避免了同一层级出现两个相同的元素。
                # 由于 [1,2,2,5] 这样子在第二次
                temp.append(candidates[i])
                dfs(size,residue-candidates[i],temp,i+1) 
                # 这里i+1急速保证组合内的每个值只会在一个分支内使用一次
                temp.pop()
        # 每个数字只能用一次,说明同一个元素不能重复出现
        res = []
        size = len(candidates)
        candidates.sort() # 排序数组,从小到大。
        dfs(size,target,[],0)
        return res

什么时候使用 used 数组,什么时候使用 begin 变量。有些朋友可能会疑惑什么时候使用 used 数组,什么时候使用 begin 变量。这里为大家简单总结一下:

  • 排列问题,讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用 used 数组;
  • 组合问题,不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用 begin 变量。
  1. 组合总和III
    题目:找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
    说明:所有数字都是正整数。解集不能包含重复的组合。
    分析:组合问题的改版,只不过是数字变为1-9,变化了一下k值而已,本质上的变化不是很大。先分析返回条件,n == 0 且 k == 0, 这是两个条件,满足了,拷贝路径加返回即可。
    剪枝:这题的剪枝也很好理解,分为k和n ,只要其中一个不满足条件,即刻跳出当前循环。
class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        #求解 DFS 回溯剪枝
        res = []
        def dfs(n,k,index,temp):
            if n==0 and k==0:
                res.append(temp[:]) #添加整个temp的序列
                return
            for i in range(index,10):
                if n-i<0 or k<=0: # 此时的k是上一步传来的数,若为0,则表示以及数以及满了,不能再加了
                # 而 n-i 判断的是此时的数是否超过了阈值,=0不能作为break的条件。
                    break # 由于以及 n-i< 0了,那么在其之后减去更大的数 同样也是小于0的
                temp.append(i)
                dfs(n-i,k-1,i+1,temp)
                temp.pop() # 回溯反弹之后,进入到下一个index,要把原来的index弹出,回溯到原始状态
            return
        dfs(n,k,1,[])
        return res
  1. 组合

组合问题,其实都是DFS问题, DFS问题一般都离不开递归问题,做这种类似的题的第一步是找到返回条件,这一题(给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。) 明显的看到条件是 len(temp) == k 的时候,返回递归。

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:

        ##特殊情况处理
        if n==0 or k==0:
            return res
        def dfs(nums,temp,index):
            # 首先是判断返回条件
            if len(temp) == k:
                res.append(temp.copy())
                return
            for i in range(index,n): #因为已经是从小到打的排序过程,index又不断再向后,所以不会存在后一个比前一个小
                temp.append(nums[i])
                dfs(nums,temp,i+1) # 这里的i+1才是真正做到了避免重复的情况,因为必须是将index赋给 i+1
                temp.pop()       
        nums = [i for i in range(1, n + 1)]
        res = []
        dfs(nums,[],0)
        return res
        # 这一题也类似于使用begin ,这里的index 其实就是begin, 因为这里的排列不可以重复数组,不可以前后顺序调换
        # 所以不需要使用used,而且有限定的k值,只要遍历再输出就可以了

子集问题

子集问题和排列问题有相似之处,只不过需要将每一个可能的非重复组合都要找到,本质上还是DFS, 而且这题也许要使用到begin变量,每次递归begin+1,这样后续遍历会忽略当前的值。但是这一题还是回溯算法,也可以使用迭代算法(后续贴出),这里主要关注回溯,

  • 子集
    但是这一题还是回溯算法,也可以使用迭代算法(后续贴出),这里主要关注回溯, 回溯的关键就是不合适就返回上一个状态。
class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        #子集,思路1,DFS 加递归
        # 同样判断条件是需要begin 和 k值
        visited = []
        k = len(nums)
        def dfs(begin,temp):
            visited.append(temp)
            for i in range(begin,k):
                dfs(i+1,temp+[nums[i]])
                #当bagin == k的时候,当前循环全部结束,返回到最上层的循环。
        dfs(0,[])
        return visited

其他

  • 第K个排列
  • 复原IP地址

题型二(Flood Fill)

  • 733图像渲染
  • 200岛屿问题
  • 130被围绕的区域
  • 79单词搜索

题型三(字符串中的回溯问题)

  • 17电话号码的字母组合
  • 字母大小写全排列
  • 括号生成

题型四(游戏问题_困难题)

  • N皇后
  • 解数独
  • 祖玛游戏
  • 扫雷游戏
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值