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,也就是四个数选两两组合,看下图:
leetcode(力扣) 77. 组合(回溯 & 剪枝-----清晰图解+回溯套路模板)_第1张图片

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个数,这里盗用一下卡哥的图,可以看到 除了最左边的一条,其余都不符合要求,都可以剪掉。

leetcode(力扣) 77. 组合(回溯 & 剪枝-----清晰图解+回溯套路模板)_第2张图片

那么如何在代码中控制需要剪掉的分支呢?
假设目前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

你可能感兴趣的:(个人笔记,交流学习,python,leetcode,1024程序员节)