【小白刷题之路Day2】回溯法总结(dfs)

前言:回溯法和动态规划应该是考察率最多的两类算法,参考《从“排列问题”理解“回溯搜索”(DFS + 状态重置)、位掩码技巧、递归交换》,作一次总结css

 

力扣No. 46 全排列java


 

解决回溯问题,个人经验是 必定不要偷懒,拿起纸和笔,把这个问题的递归结构画出来,通常而言,是一个树形结构,这样思路和代码就会比较清晰了。而写代码便是将画出的图用代码表现出来。python

方法:“回溯搜索”算法即“深度优先遍历 + 状态重置 + 剪枝”(这道题没有剪枝)

以示例输入: [1, 2, 3] 为例,由于是排列问题,只要咱们按照顺序选取数字,保证上层选过的数字不在下一层出现,就可以获得不重不漏的全部排列git

画出树形结构以下图程序员

 

 注意:

一、这里特别说明一点:虽然个人图是一会儿展现出来的,可是我想你画出的图应该是一层一层画出来的;

二、在每一层,咱们都有若干条分支供咱们选择。下一层的分支数比上一层少 1 ,由于每一层都会排定 1 个数,从这个角度,再来理解一下为何要使用额外空间记录那些元素使用过;

三、所有的“排列”正是在这棵递归树的全部叶子结点。算法

咱们把上面这件事情给一个形式化的描述:问题的解空间是一棵递归树,求解的过程正是在这棵递归树上搜索答案,而搜索的路径是“深度优先遍历”,它的特色是“不撞南墙不回头”。数组

在程序执行到上面这棵树的叶子结点的时候,此时递归到底,当前根结点到叶子结点走过的路径就构成一个全排列,把它加入结果集,我把这一步称之为“结算”。网络

此时递归方法要返回了,对于方法返回之后,要作两件事情:数据结构

(1)释放对最后一个数的占用;
(2)将最后一个数从当前选取的排列中弹出。app

事实上在每一层的方法执行完毕,即将要返回的时候都须要这么作。这棵树上的每个结点都会被访问 2 次,绕一圈回到第 1 次来到的那个结点,第 2 次回到结点的“状态”要和第 1 次来到这个结点时候的“状态”相同,这种程序员赋予程序的操做叫作“状态重置”。

状态重置”是“回溯”的重要操做,“回溯搜索”是有方向的搜索,不然咱们要写多重循环,代码量不可控。

说明:

一、数组 used 记录了索引 i 在递归过程当中是否被使用过,还能够用哈希表、位图来代替,在下面的参考代码 2 和参考代码 3 分别提供了 Java 的代码;

二、当程序第 1 次走到一个结点的时候,表示考虑一个数,要把它加入列表,通过更深层的递归又回到这个结点的时候,须要“状态重置”、“恢复现场”,须要把以前考虑的那个数从末尾弹出,这都是在一个列表的末尾操做,最合适的数据结构是栈(Stack)。

class Solution:

    def permute(self, nums):
        if len(nums) == 0:
            return []

        used = [False] * len(nums)
        res = []
        self.__dfs(nums, 0, [], used, res)
        return res

    def __dfs(self, nums, index, pre, used, res):
        # 先写递归终止条件
        if index == len(nums):
            res.append(pre.copy())
            return

        for i in range(len(nums)):
            if not used[i]:
                # 若是没有用过,就用它
                used[i] = True
                pre.append(nums[i])

                # 在 dfs 先后,代码是对称的
                self.__dfs(nums, index + 1, pre, used, res)

                used[i] = False
                pre.pop()

 总结:

能够经过这个例子理解“回溯”算法的“状态重置”的操做,“回溯搜索” = “深度优先遍历 + 状态重置 + 剪枝”。

一、“深度优先遍历” 就是“不撞南墙不回头”;

二、回头的时候要“状态重置”,即回到上一次来到的那个地方,“状态”要和上一次来的时候同样。

三、在代码上,每每是在执行下一层递归的先后,代码的形式是“对称的”。

 

 

注意:

ask:     为何 代码  res.append(pre.copy()):

liweiwei14199 : 这是由于在 Python、Java 语言中“可变对象”在“方法传参”中传递的是引用,若是使用 res.append(pre) 的话,在 res 变量里存的就是在叶子结点处 pre 变量的引用,而 pre 变量在最终回溯完成之后是空列表 [],使用 res.append(pre) 您会看到一堆空列表。

所以,须要使用 pre[:] 切片操做,获得一个拷贝,或者使用 pre.copy() 方法。

这部分的知识您能够在网络中搜索“Python 传参机制”、“值传递”、“引用传递”、“深拷贝”、“浅拷贝”、“列表的拷贝和切片”等关键字得到相关的知识。解释清楚这些知识已经在个人能力范围以外了。

若是我有解释错误的地方,欢迎您指出。


做者:liweiwei1419
连接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
来源:力扣(LeetCode)
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

 

力扣No.17 电话号码的字母组合


class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        if not digits:
            return []
        
        dict1 = {'2':'abc', '3':'def', '4':'ghi', '5':'jkl', '6':'mno', '7':'pqrs', '8':'tuv', "9":'wxyz'}
        
        paths = []
        self.__dfs(paths, [], digits, 0, dict1)
        return paths
    
    def __dfs(self, paths, path, digits, index, dict1):
        if index == len(digits):
            paths.append(''.join(path.copy()))
            return
        
        for i in range(len(dict1[digits[index]])):
            path.append(dict1[digits[index]][i])
            self.__dfs(paths, path, digits, index+1, dict1)
            path.pop()
        

 

 注意:

path 列表中数据为 ['a', 'e', 'i', ...]

因此须要用: paths.append(''.join(path.copy()))

path.copy() 千万不能忘!!!

相关文章
相关标签/搜索