算法基础——回溯(1)

算法基础——回溯

回溯递归是同时存在的。回溯算法效率并不高,本质是穷举,用于某些只能暴力搜索解决的问题。

这些问题可以分为:

  • 组合问题
  • 排列问题
  • 切割问题
  • 子集问题
  • 棋盘问题:N皇后,解数独

回溯解决问题可以抽象成为N叉树的树型结构。回溯在循环中设计递归,进行层叠嵌套。其中for循环横向遍历,递归纵向遍历,回溯不断在上一层调整结果集

1、组合 leetcode 77

copy:本题添加path时使用py内置copy函数进行浅拷贝。直接使用path是引用,会因之后的递归path改变而更改(尽管前面的path已经加入list)。而使用浅拷贝而不是深拷贝,是因为ans中的每个path中存放的是数字不会被更改。具体两者区别
list和array使用切片也有区别
全局变量与局部变量:局部可以调用相对更全局的变量,而全局要调用局部需要global声明。
range:如果在 range 函数中,第一个参数大于第二个参数,并且最后一个参数为 -1,那么表示生成一个递减的区间。

例如,range(5, 1, -1) 会生成一个递减的区间: [5, 4, 3, 2]。这个区间包括起始值 5,但不包括结束值 1,每次递减 1。

在设计循环、递归以及其构成的回溯,动态规划,贪心算法的时候,明白他们在重复相似行为时保持的是什么,非常关键。如循环不变式,再到本题的情况。
对于下方第一个算法,函数dfs传入的i表示当前考虑的数字范围,从整体思路上看,是依次考虑最大数取n,n-1,n-2···的情况。对于下方第二个算法,函数dfs传入的i表示现在考虑第i个数在和不在里面的情况。if i>d则i可以不在里面,这时候继续递归。然后最后三行考虑i在里面的情况。

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        ans=[]
        path=[]
        def dfs(i:int)->None:
            d=k-len(path) #还需要选择d个数字
            if d==0:
                ans.append(path.copy())
                return
            for j in range(i,d-1,-1):# 这里逆向写更自然,当然你也可以翻转写它
                 path.append(j)
                 dfs(j-1)
                 path.pop()
        dfs(n)
        return ans
class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        ans = []
        path = []
        def dfs(i: int) -> None:
            d = k - len(path)  # 还要选 d 个数
            if d == 0:
                ans.append(path.copy())
                return
            # 不选 i
            if i > d: dfs(i - 1)
            # 选 i
            path.append(i)
            dfs(i - 1)
            path.pop()
        dfs(n)
        return ans

至于python自带的组合数库,也可一行解决本题

from itertools import combinations
class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        return list(combinations(range(1, n+1), k))

2、组合总和Ⅲ leetcode 216

拿到题自己写了下,有点累赘

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        ans = []
        path = []
        def dfs(sum: int, t: int):
            d=k-len(path)
            if sum==0 and d==0:
                ans.append(path.copy())
                return
            if sum==0 or d==0:
                return
            for i in range(min(sum,t-1,9),0,-1):
                path.append(i)
                dfs(sum-i,i)
                path.pop()
        dfs(n,n+1)# 寻找和为n,最大值小于n+1的满足K-len(path)限制的数字组合
        return ans

下面是灵茶山艾府的做法,与上题一样,
法一:每次迭代以最大值不同区分。

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        ans = []
        path = []
        def dfs(i: int, t: int) -> None:
            d = k - len(path)  # 还要选 d 个数
            if t < 0 or t > (i * 2 - d + 1) * d // 2:  # 剪枝首项加尾项乘以项数除以二
                return
            if d == 0:  # 找到一个合法组合
                ans.append(path.copy())
                return
            for j in range(i, d - 1, -1):
                path.append(j)
                dfs(j - 1, t - j)
                path.pop()
        dfs(9, n)
        return ans

法二:每次迭代以选不选这个数区分

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        ans = []
        path = []
        
        def dfs(i: int, t: int) -> None:
            d = k - len(path)  # 还要选 d 个数
            if t < 0 or t > (i * 2 - d + 1) * d // 2:  # 剪枝
                return
            if d == 0:  # 找到一个合法组合
                ans.append(path.copy())
                return
                
            # 不选 i
            if i > d:
                dfs(i - 1, t)
                
            # 选 i
            path.append(i)
            dfs(i - 1, t - i)
            path.pop()
            
        dfs(9, n)
        return ans

3、电话号码的字母组合 leetcode 17

自己开头写了一版,由于字典写成int型,复杂了一点,其实想到后面for循环直接循环的digits字符串,应该用字符串的数字。

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        if not digits:
            return []
        dic={2:"abc", 3:"def", 4:"ghi", 5:"jkl",
             6:"mno", 7:"pqrs", 8:"tuv", 9:"wxyz"}
        ans=[]
        d=[]
        n=len(digits)-1
        def dfs(n): # 获得n位置
            # d=len(digits)-len(d)
            if len(d)==len(digits):
                ans.append("".join(d[::-1]))
                return
            for i in dic[int(digits[n])]:
                d.append(i)
                dfs(n-1)
                d.pop()
        dfs(n)
        return ans
                

腐烂的橘子写的回溯传两个参数就非常简洁

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        if not digits: return []

        phone = {'2':['a','b','c'],
                 '3':['d','e','f'],
                 '4':['g','h','i'],
                 '5':['j','k','l'],
                 '6':['m','n','o'],
                 '7':['p','q','r','s'],
                 '8':['t','u','v'],
                 '9':['w','x','y','z']}
                
        def backtrack(conbination,nextdigit):
            if len(nextdigit) == 0:
                res.append(conbination)
            else:
                for letter in phone[nextdigit[0]]:
                    backtrack(conbination + letter,nextdigit[1:])# 字符串拼接操作值得学习

        res = []
        backtrack('',digits)
        return res

实际上本题使用回溯可能比循环嵌套运行时间还久一点,主要是本题不存在去重,剪枝等情况,所以回溯也不具有思路简单的优势。
时间复杂度上,O(3^m× 4^n ),其中m是输入中对应3 个字母的数字个数,n是输入中对应4个字母的数字个数。
空间复杂度上,O(m+n),哈希表(字典)空间固定,递归层数占主导地位,取决于总的输入数字数目。

tips:写的时候遇到的一些问题
ans.append(“”.join(d.reverse())):d.reverse()返回的是None,而不是反转后的列表。应该先使用d.reverse()来对列表进行反转,然后再使用"".join(d)来将反转后的列表转换为字符串。这时可以用字符串的切片操作[::-1]来解决反转列表的需求。


总结

  • 回溯算法中的循环,对应遍历树结构一层节点的横向进程,而递归相当于纵向进程
  • 明白算法中设计的递归函数,循环结构的不变性很关键(完成什么样的相似的任务)
  • 回溯法三部曲
    1、递归函数的返回值以及参数
    2、回溯函数终止条件
    3、单层搜索的过程
  • 对于回溯题目,有时可以进行减枝优化
  • 提到组合想到回溯
  • 19
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值