Day 24 回溯算法part01 : 理论基础 77. 组合

回溯算法理论基础

回溯是一个试错的策略,用于解决计算问题的优化问题以及某些约束满足问题,以下是关于回溯的知识总结:

1. 定义

回溯是一种通过穷举所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解的话(或至少不是最后一个解),回溯算法会通过在上一步进行一些变化来消除该候选解,然后重新尝试。

2. 基本思想

回溯的基本思想是建立一个候选解并且在确认已经找到一个解之前,需要再次确认它是否满足所给的约束条件。

3. 关键点

  • 选择: 对于每个具体的选择点,需要从众多候选中选择一个。
  • 约束: 设定哪些候选可以(基于之前的选择)。
  • 目标: 已经确定的选择序列是一个解(或至少是一个潜在的解)。

4. 解题框架

回溯算法可以通过递归函数来实现,其结构通常如下:

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

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

5. 常见问题

  • 组合问题:例如从n个数中选择k个数。
  • 排列问题:例如列出所有的排列方式。
  • 子集问题:例如列出所有的子集。
  • N皇后问题。
  • 0-1背包问题。
  • 图的着色问题。

6. 时间复杂度

由于回溯算法通常涉及到指数级的时间复杂度,所以很容易超时。确定回溯算法的时间复杂度通常基于具体的问题。最坏情况下,你可能需要查看所有可能的解,这将是指数时间。

7. 优化策略

  • 剪枝: 在确定某个候选解不可能进一步发展成一个真正的解之前提前终止它。
  • 备忘录法: 通过存储已经计算过的状态,避免重复计算。

结论

回溯是算法设计中的一个强大工具,它的主要缺点是可能会尝试太多的选择,这使得它在某些情况下非常慢。因此,关于如何使用回溯、何时使用和如何优化,都是需要仔细考虑的问题。

为了掌握回溯,最好的办法是通过实际的练习和应用,解决一些经典的回溯问题。

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开始进行选择,每次选择后,再从下一个数字开始选择。
  • 每选择一个数字,将其添加到当前组合中。
  • 当当前组合的长度为k时,将其添加到结果列表中。
  • 撤销上一步的选择,回溯到上一层,继续选择下一个数字。

代码如下:

class Solution(object):
    def combine(self, n, k):
        """
        :type n: int
        :type k: int
        :rtype: List[List[int]]
        time: 回溯算法需要遍历所有可能的组合,总数为 C(n,k)。
        C(n,k) 是从 n 个数中选 k 个数的组合数,也可以表示为 O(n!/k!(n−k)!)
        space: 空间复杂度主要取决于递归栈的深度,也就是 O(k)。
        """

        output = []

        def backtracking(first = 1, curr = []):
            # 当前组合长度达到k时,添加到结果列表
            if len(curr) == k:
                output.append(curr[:]) 
                # 创建了 curr 的一个新的副本,这是一个浅拷贝
                # 保存的是当前组合的状态,而不是它的引用,这样当 curr 在后续被修改时,
                # 之前保存的状态不会受到影响。
                return

            for i in range(first, n + 1):
                # 剪枝 确保如果剩余的数字不足以完成组合,则不再继续搜索。
                if n - i + 1 < k - len(curr):
                    break

                # 将数字i添加到当前组合
                curr.append(i)
                # 使用下一个数字,继续回溯
                backtracking(i + 1, curr)
                # 撤销选择, 回溯
                curr.pop()
            
        backtracking()
        return output




            

关于剪枝:

  • 当我们选择数字时,我们需要考虑剩余的数字是否足够满足组合的要求。
  • 举例来说,假如 n=7, k=4,我们正在考虑第一个数字。一开始我们可以从1到7中选择任意一个数字。但当我们选择了4作为第一个数字时,我们就只剩下5、6、7三个数字可以选择,不可能再组成长度为4的组合。所以在这种情况下,我们不应该进入更深的递归。
  • 剪枝的具体方法是:在for循环中,当 n - i + 1 < k - len(curr) 时,我们就停止循环。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值