代码随想录训练营 Day22打卡 回溯算法part01 理论基础 77. 组合 216. 组合总和III 17. 电话号码的字母组合

代码随想录训练营 Day22打卡 回溯算法part01

一、 理论基础

什么是回溯法

回溯法,又称回溯搜索法,是一种搜索方式,其本质是穷举所有可能的解并选择符合条件的解。

回溯法常常与递归联系在一起,递归是回溯的基础,回溯是递归的副产品。在回溯中,函数一般会通过递归调用自身来进行问题的求解。

回溯法的效率

尽管回溯法是一种非常重要的算法,但其效率并不高。回溯法的核心是穷举,即尝试所有可能的解,然后从中选择符合条件的解。为了提升效率,可以在回溯过程中加入剪枝操作,以减少不必要的计算。然而,回溯法的本质决定了它并不是一个高效的算法。

回溯法解决的问题

回溯法可以用于解决以下几类问题:

  1. 组合问题:从N个数中按一定规则找出k个数的集合。
  2. 切割问题:将一个字符串按一定规则进行切割的方式。
  3. 子集问题:求一个N个数的集合中符合条件的子集。
  4. 排列问题:求N个数按一定规则全排列的方式。
  5. 棋盘问题:如N皇后问题、数独问题等。

回溯法模板

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

这份模板对于解决回溯法的题目非常重要,通过理解和应用这个模板,可以解决许多复杂的问题。初学者可能会觉得有点抽象,但在具体题目讲解中会更容易理解。已经做过回溯法题目的同学,应该会对这个模板感同身受。

二、 力扣77. 组合

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

我把组合问题抽象为如下树形结构:
在这里插入图片描述
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。

图中可以发现n相当于树的宽度,k相当于树的深度。

主要思路:

  1. 初始化和启动回溯:

我们从combine方法开始,初始化一个空的结果集result。

  1. 回溯函数 backtracking:

backtracking函数用于递归生成所有可能的组合。

  1. 递归终止条件:

当当前路径path的长度等于目标组合长度k时,我们找到一个有效的组合,将其复制并加入结果集result。

  1. 递归过程:

遍历从startIndex到n - (k - len(path)) + 2的范围内的所有可能选择。这里的范围通过优化减少不必要的遍历,提升效率。
对于每个选择:
    将当前数字i加入当前路径path。
    递归调用backtracking,并将startIndex更新为i + 1,继续生成后续的组合。
    回溯:在递归返回后,撤销当前选择,即从path中移除最后一个数字。

  1. 优化:

循环的结束条件通过n - (k - len(path)) + 2进行了优化,以避免不必要的计算,提升效率。

from typing import List

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        result = []  # 存放结果集
        self.backtracking(n, k, 1, [], result)
        return result
    
    def backtracking(self, n: int, k: int, startIndex: int, path: List[int], result: List[List[int]]):
        """
        使用回溯算法生成组合
        
        :param n: 1到n的整数范围
        :param k: 组合中数字的个数
        :param startIndex: 当前递归的起始索引
        :param path: 当前组合路径
        :param result: 存放所有组合结果的列表
        """
        if len(path) == k:
            # 当路径长度等于k时,将当前路径加入结果集
            result.append(path[:])
            return
        
        # 优化:如果剩余的数字已经不足以填满k个位置,则提前结束循环
        for i in range(startIndex, n - (k - len(path)) + 2):
            path.append(i)  # 处理当前节点,将i加入路径
            self.backtracking(n, k, i + 1, path, result)  # 递归,更新起始索引为i+1
            path.pop()  # 回溯,撤销处理的节点,移除最后一个元素

# 示例调用
sol = Solution()
print(sol.combine(4, 2))  # 输出: [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]

力扣题目链接
题目文章讲解
题目视频讲解

三、 力扣216. 组合总和III

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
    只使用数字1到9
    每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 :
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

本题k相当于树的深度,9(因为整个集合就是9个数)就是树的宽度。

例如 k = 2,n = 4的话,就是在集合[1,2,3,4,5,6,7,8,9]中求 k(个数) = 2, n(和) = 4的组合。

选取过程如图:
在这里插入图片描述
图中,可以看出,只有最后取到集合(1,3)和为4 符合条件。

代码思路:

  1. 初始化和启动回溯:

在 combinationSum3 方法中,初始化一个空的结果集 result。
调用 backtracking 方法开始回溯过程。

  1. 回溯函数 backtracking:

如果当前和 currentSum 超过目标和 targetSum,则提前返回(剪枝)。
如果路径长度等于 k 且当前和等于 targetSum,将当前路径加入结果集。
否则,从当前起始索引遍历到 9,依次尝试每个数字。

from typing import List

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        """
        找到所有由 k 个不同的数字组成的组合,这些数字的和为 n
        """
        result = []  # 存放结果集
        self.backtracking(n, k, 0, 1, [], result)
        return result

    def backtracking(self, targetSum: int, k: int, currentSum: int, startIndex: int, path: List[int], result: List[List[int]]):
        """
        使用回溯算法生成组合
        
        :param targetSum: 目标和
        :param k: 组合中数字的个数
        :param currentSum: 当前路径的和
        :param startIndex: 当前递归的起始索引
        :param path: 当前组合路径
        :param result: 存放所有组合结果的列表
        """
        if currentSum > targetSum:  # 剪枝操作
            return  # 如果当前和超过目标和,则返回
        if len(path) == k:
            # 当路径长度等于k时,检查当前和是否等于目标和
            if currentSum == targetSum:
                result.append(path[:])  # 将当前路径加入结果集
            return
        # 优化:如果剩余的数字已经不足以填满k个位置,则提前结束循环
        for i in range(startIndex, 9 - (k - len(path)) + 2):
            currentSum += i  # 处理当前节点,将i加入当前和
            path.append(i)  # 将i加入路径
            self.backtracking(targetSum, k, currentSum, i + 1, path, result)  # 递归,更新起始索引为i+1
            currentSum -= i  # 回溯,撤销处理的节点,从当前和中减去i
            path.pop()  # 回溯,从路径中移除最后一个元素

# 示例调用
sol = Solution()
print(sol.combinationSum3(3, 7))  # 输出: [[1, 2, 4]]
print(sol.combinationSum3(3, 9))  # 输出: [[1, 2, 6], [1, 3, 5], [2, 3, 4]]

力扣题目链接
题目文章讲解
题目视频讲解

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

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

每个按键(2-9)对应一组字母,给定一串数字,生成所有可能的字母组合。通过回溯算法实现这一目标。

版本一

  1. 初始化:

初始化 letterMap,包含每个数字对应的字母。
初始化 result 作为存放结果的列表。
初始化 s 作为当前组合的字符串。

  1. 回溯函数 backtracking:

如果当前索引等于输入数字的长度,表示已完成一个组合,将其加入结果集。
获取当前数字对应的字母集,并依次尝试每个字母。
将字母加入当前组合 s,递归处理下一个数字。
回溯时删除最后加入的字母,恢复状态。

  1. 主函数 letterCombinations:

检查输入是否为空,为空则返回空结果。
调用回溯函数从第一个数字开始处理。

class Solution:
    def __init__(self):
        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):
        if index == len(digits):
            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)  # 递归调用,注意索引加1,处理下一个数字
            self.s = self.s[:-1]  # 回溯,删除最后添加的字符

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

版本二

  1. 初始化:

初始化 letterMap,包含每个数字对应的字母。
初始化 result 作为存放结果的列表。

  1. 回溯函数 getCombinations:

如果当前索引等于输入数字的长度,表示已完成一个组合,将其加入结果集。
获取当前数字对应的字母集,并依次尝试每个字母。
将字母加入当前组合 s,递归处理下一个数字。

  1. 主函数 letterCombinations:

检查输入是否为空,为空则返回空结果。
调用回溯函数从第一个数字开始处理。

class Solution:
    def __init__(self):
        self.letterMap = [
            "",     # 0
            "",     # 1
            "abc",  # 2
            "def",  # 3
            "ghi",  # 4
            "jkl",  # 5
            "mno",  # 6
            "pqrs", # 7
            "tuv",  # 8
            "wxyz"  # 9
        ]
        self.result = []

    def getCombinations(self, digits, index, s):
        if index == len(digits):
            self.result.append(s)
            return
        digit = int(digits[index])  # 将索引处的数字转换为整数
        letters = self.letterMap[digit]  # 获取对应的字符集
        for letter in letters:
            self.getCombinations(digits, index + 1, s + letter)  # 递归调用,注意索引加1,处理下一个数字

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

力扣题目链接
题目文章讲解
题目视频讲解

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值