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,2] [2,1] 是一个答案,别弄错了。

回溯 、递归模板

  • 确定递归参数
  • 确定终止条件
  • 确定单次循环体

这种回溯的题,都可以画成树形结构,不熟的时候,先画图看看逻辑,

假设n=4 k=2,也就是四个数选两两组合,看下图:
在这里插入图片描述

1.确定 递归/回溯 参数:

首先需要题目给出的n和k没跑了,另外还有一个,可以在图中看到,当选择1之后,只能从2,3,4里再选,选择2之后,只能从剩下的3,4里选,也就是,选择i之后,只能从i+1里去选,所以要有一个记录下标的值startindex。

def backtrack(n,k,startindex):

2.确定终止条件:

这个比较好想,当用来记录的数组里已经有k个值了,那么就终止。
在终止之前,要将收集到的数加入到答案集。

if len(temp) == k:
     res.append(temp[:])
     return

3.确定循环体:

循环体里也就是从一层到下面一层 and 下面一层回溯到上面一层 的过程中需要操作的东西。

首先需要将当前的 元素放到记录的数组中,然后调用自己,最后回溯的时候记着弹出元素。

temp.append(i)
backtrack(n,k,i+1)
temp.pop()

细节:

  1. 回溯的过程比较抽象,题也比较难想,可以画出来树图,然后用套路化记忆,回调函数上面就是从本层到下一层需要操作的东西,回调函数下面就是从下一层返回到上一层需要操作的东西,毕竟从终止条件return之后就要开始运行回调函数下面的内容了,也就是当记录数组中达到k个值之后,显然要pop弹出一个数,然后返回到上一层树中去。

比如:

temp.append(i)      # 这就是从本层到下一层需要做的事
backtrack(n,k,i+1)   # 这就是回调函数
temp.pop()    # 这就是从下一层到上一层需要做的事
  1. 循环体中控制的是n,也就是树的横向,而不断递归 ,回溯的过程中控制的是树的纵向,也就是树的深度。搞清楚这个小细节,则for里的值就不会出错了。
  2. 要搞清楚for里的i循环变量和startindex变量。for里的startindex,可以这么理解,每次横向循环,外面的for里的i控制从1到n,而假如选择了1,后面还要选择234,这个234是下一层的,也是横向,在下一层中,第一个分支选择了2,再下一层就是从3,4里面选,所以startindex控制的是下一层从哪里开始选择,这个下一层,也是属于横向遍历的过程,可以看那个图。

完整代码

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]: 
            res = []
            temp = []
            def backtrack(n,k,startindex):
                # 终止条件
                if len(temp) == k:
                    res.append(temp[:]) 
                    #假设说如果你直接加入temp的话,那么temp一定是你一开始要设置得全局变量得一个数组list对吧,然后你每次都往res中存入得temp其实就是一个指针,当你递归完以后,回溯,将path里的最后一个数据删除了,那么res中存入得元素指针,指向得那个数组同样需要删除那个元素,最后就会导致,你在res中开辟了多个空间,但是最后每个数组指代得是同一块空间,并且最后该空间内得所有元素,最后都是空。
                    return
                # 循环体
                for i in range(startindex,n+1): # 记着+1,题目n从1开始的
                    temp.append(i)
                    backtrack(n,k,i+1)
                    temp.pop()
            backtrack(n,k,1)
            return res

优化(剪枝);

回溯其实就是纯暴力算法,只是有时候不能无脑嵌套for,倒不是时间复杂度的问题,而是有时候根本没法写,比如这个题的for,有几个k就有几层嵌套for,但是k不确定,所以没法写。

当使用回溯的时候,往往搭配着剪枝,以降低时间复杂度。

假设n=4,k=4 也就是 一共四个数,取4个数,这里盗用一下卡哥的图,可以看到 除了最左边的一条,其余都不符合要求,都可以剪掉。

在这里插入图片描述

那么如何在代码中控制需要剪掉的分支呢?
假设目前n=4 ,k=3。看一下需要剪掉的部分,当记录数组temp中为空,且当前i取3的时候,剩下可取元素为4,那么取了3再进入一下分支取4,temp中也仅有两个值。显然这个i=3的分支是需要剪掉的,也就是 当你temp数组中个数+还需要的元素个数>剩下可选的元素个数时,剪! 换句话说,你的 i 最多只能遍历到2,遍历到2,temp里是2,然后还能取3和4,此时正好为3个元素。

也就是说找一个公式来控制 i 最多可以遍历到的值,使得剩下未遍历的元素+temp里现有的元素可以满足k的要求

剩下未遍历的元素就是 元素总和n - 当前遍历到的下标i, -> n-i

即:(多项式优化)

  • n-i+len(temp) >= k
  • -i >= k-n-len(temp)
  • i <= n+len(temp)-k

所以for里的 i 条件是 i <= n-(k-len(temp), 也就是最多遍历到这,而之前是直接遍历到n+1。

实际上代码里是n-(k-len(temp))+1+1 ,为啥要+1两次呢,随便带个k和n就知道了,其实是题目n从1开始的原因。另一个+1就是无剪枝的情况下带的。

完整代码

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]: 
        # 带剪枝
            res = []
            temp =[]
            def backtrack(n,k,startindex):
                # 终止条件
                if len(temp) == k:
                    res.append(temp[:])
                    return
                for i in range(startindex,n-(k-len(temp))+1+1):
                    temp.append(i)
                    backtrack(n,k,i+1)
                    temp.pop()
            backtrack(n,k,1)
            return res
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法。 LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

深度不学习!!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值