Leetcode 刷题第24天 |77,216,17

本文详细介绍了回溯算法的工作原理,其在解决递归问题中的应用,以及如何通过剪枝优化算法效率。以LeetCode中的组合、组合总和和字母组合问题为例,展示了回溯算法的具体实现和优化策略。
摘要由CSDN通过智能技术生成

前言

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。
回溯是递归的副产品,只要有递归就会有回溯。
回溯是一种暴力搜索法,并不高效。虽然可以通过剪枝优化算法,但是并不能改变回溯算法的本质。

为什么回溯算法并不高效,为什么还必须要使用呢?因为没得选,简单的for循环不足以解决问题,泛化性很弱。

仔细想一下,在二叉树中,递归法解决问题,肯定都有回溯,只不过有时候回溯被隐藏起来了。所以,所有的回溯算法是不是都可以抽象成树形结构。只不过不一定是二叉树,可能是多叉树。

回溯算法可以分为三个步骤:

  • 回溯函数返回值以及参数
  • 回溯函数终止条件
  • 单层搜索逻辑
    因此,回溯算法模板如下:
def backtracking(参数比较多,不太容易一次性想清楚)
    if(终止条件):
        收集结果
        return		# 这里的return一定要有,否则很容易陷入死循环
    # 单层搜索逻辑
    for(集合元素集):
        处理节点
        递归函数 
        回溯操作(撤销处理节点的情况)
    return 操作

Leetcode 77 组合

题目:

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
提示:

  • 1 <= n <= 20
  • 1 <= k <= n

算法思想:

第一层:[1, n]
第二层:[2, n],[3, n],[4, n],…,[n] # 避免重复的组合
第三层:…
第四层:…

当然不一定递归这么多层,仅是示意图。
在本题中,k是控制着递归深度,n控制着每次for循环所走的范围。

  • 回溯函数模板返回值以及参数
    path:记录当前路径上的节点
    result:记录所有满足条件的组合
    startIndex:为了避免result中的重复组合
  • 回溯函数终止条件
    当path的长度恰好为k的时候,终止
  • 单层搜索逻辑
    path中加入节点
    递归加入下一个节点
    path中出栈

ps:总感觉这种回溯算法像锯齿形状

代码实现:

  1. 普通实现:
class Solution:
    def combine(self, n: int, k: int):
        result = []  # 存放结果集
        self.backtracking(n, k, 1, [], result)
        return result
    def backtracking(self, n, k, startIndex, path, result):
    	'''
			n: 输数据范围
			k: 表示递归深度
			startIndex: 用于避免重复选取数字
			path: 每个符合条件的结果集
			result:所有满足条件的结果集,最终返回的结果
		'''
        if len(path) == k:
            result.append(path[:])
            return
        for i in range(startIndex, n + 1):  # 需要优化的地方
            path.append(i)  # 处理节点
            self.backtracking(n, k, i + 1, path, result)
            path.pop()  # 回溯,撤销处理的节点
  1. 剪枝实现:
class Solution:
    def combine(self, n: int, k: int):
        result = []  # 存放结果集
        self.backtracking(n, k, 1, [], result)
        return result
    def backtracking(self, n, k, startIndex, path, result):
        if len(path) == k:
            result.append(path[:])
            return
        for i in range(startIndex, n + 2 - (k - len(path))):  # 剪枝的位置
            path.append(i)  # 处理节点
            self.backtracking(n, k, i + 1, path, result)
            path.pop()  # 回溯,撤销处理的节点

为什么在遍历n的时候剪枝?
如果n=10,k = 3,当前path中还没有元素,如果想取得k个数,那么遍历n的时候,至多可以取到8。
k - len(path)意味着path中还需要几个元素
n - (k - len(path))意味着在path中有len(path)个元素的情况下,仍然可以从哪里取元素才能满足条件。
n + 2 其中,一个1表示range()函数是左闭右开的;另外一个1是下标从1开始,弥补这个差距。最开始没想清楚,可以通过带入具体的数据尝试。

Leetcode 216 组合总和3

题目

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。

算法思想:

依然采用与上一题类似的想法,只不过多一个判断条件,使得总和为n。

代码实现:

  1. 普通实现:
原始版本
class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        path = []
        result = []
        startIndex = 1
        self.backtracing(k, n, path, result, startIndex)
        return result

    def backtracing(self, k, n, path, result, startIndex):
        if len(path) == k:
            if sum(path) == n:
                result.append(copy.deepcopy(path))
              return 
        
        for i in range(startIndex, 10):
            path.append(i)
            self.backtracing(k, n, path, result, i+1)
            path.pop()
  1. 剪枝实现:
    主要控制循环次数,例如k=3,但是当前只有9个数,但是已经遍历到8了,后边的数怎么都打不到3个
# 剪枝一次,从k的角度
class Solution:
    def combinationSum3(self, k: int, n: int):
        path = []
        result = []
        startIndex = 1
        self.backtracing(k, n, path, result, startIndex)
        return result

    def backtracing(self, k, n, path, result, startIndex):
        if len(path) == k:
            if sum(path) == n:
                result.append(copy.deepcopy(path))
            return  # 少了这一个return,导致出现无限递归的情况,导致递归的深度远大于需要的深度
        for i in range(startIndex, 9 - (k - len(path)) + 1 + 1):
            path.append(i)
            self.backtracing(k, n, path, result, i+1)
            path.pop()
  1. 二次剪枝:
    二次剪枝主要是考虑当天path中的元素个数小于k,但是元素之和已经大于或等于所期待的总和了,所以要剪枝。
# 二次剪枝
class Solution:
    def combinationSum3(self, k: int, n: int):
        path = []
        result = []
        startIndex = 1
        self.backtracing(k, n, path, result, startIndex)
        return result

    def backtracing(self, k, n, path, result, startIndex):
        if len(path) < k and sum(path) > n: # 主要限制path中的数据少于k个,但是path得总和大于n的情况。
            return
        if len(path) == k:
            if sum(path) == n:
                result.append(copy.deepcopy(path))
            return  # 少了这一个return,导致出现无限递归的情况,导致递归的深度远大于需要的深度
        for i in range(startIndex, 9 - (k - len(path)) + 1 + 1):
            path.append(i)
            self.backtracing(k, n, path, result, i+1)
            path.pop()

Leetcode 17 电话号码的字母组合

题目:

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
示例1
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 [‘2’, ‘9’] 的一个数字。

算法思想:

依然采用回溯算法。

  • 回溯函数模板返回值以及参数
    s:保存当前某个符合条件的字母组合
    result:所有满足条件的组合
    index:遍历当前集合的索引【为什么不是startIndex?因为这里遍历的是两个集合,每次都是从头开始,没必要。】
  • 回溯函数终止条件
    当当前index 指向digits的最后一个字符的末尾,结束。
  • 单层搜索逻辑
    s中加入元素【递】
    递归
    s中加入的元素弹出【归】

代码实现:

  1. 普通实现:
class Solution:
    def __init__(self) -> None:
        self.letterMap = [
            "",     # 0
            "",     # 1
            "abc",  # 2
            "def",  # 3
            "ghi",  # 4
            "jkl",  # 5
            "mno",  # 6
            "pqrs", # 7
            "tuv",  # 8
            "wxyz"  # 9
        ]
        self.result = []
        self.s = ""
    
    def backtracking(self, digits, index):
        # index 有别与startIndex:因为是在多个集合中寻找
        if index == len(digits):    # 如果说,index指向最后一个数字结束,那就意味着最后一个数字代表的字符并没有处理
            self.result.append(self.s)
            return
        digit = int(digits[index])    # 将索引处的数字转换为整数
        letters = self.letterMap[digit]
        for i in range(len(letters)):
            self.s += letters[i]
            self.backtracking(digits, index+1)
            self.s = self.s[:-1]

    def letterCombinations(self, digits):
        if len(digits) == 0:
            return self.result
        self.backtracking(digits, 0)
        return self.result

本题关于剪枝的操作似乎并不是很清楚,只要全部遍历结束就好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值