蓝桥杯打卡Day6

本文介绍了回溯算法的基本原理,包括定义解空间、搜索策略和剪枝技巧,并以组合问题为例,展示了如何用Python实现,包括不带剪枝和带剪枝的两种版本。
摘要由CSDN通过智能技术生成

目录

算法简介

网站原题

(组合问题)力扣:77. 组合

分析解答


去年忙各种事情一直断更到现在,话不多说,今天来看回溯算法

算法简介

回溯算法,也可以称为回溯搜索法,是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化来丢弃该解,即“回溯”并尝试另一个可能的解。这个过程一直进行到找到所有解或确定无解为止。

一个通过往返交通方式选择解释回溯思想的小示例:【回溯算法——跟着皇后学回溯】 https://www.bilibili.com/video/BV1c8411776k/?share_source=copy_web&vd_source=2e72b56d6a26d60223e77e7009d0535e

回溯算法的基本框架包括以下几个步骤:

  1. 定义问题的解空间:这是第一步,也是最重要的一步。解空间包含了所有可能的解。
  2. 确定易于搜索的解空间结构:选择一种合适的数据结构或表示方法,以便于有效地搜索解空间。
  3. 以深度优先搜索的策略搜索解空间:从根节点开始,按照深度优先的方式搜索解空间树。在搜索的过程中,可能会遇到需要决策的情况,这时会生成多个分支,每个分支代表一种可能的解。
  4. 在搜索过程中用剪枝函数避免无效搜索:这是提高算法效率的关键。当搜索到某个节点时,如果发现该节点不满足问题的约束条件(即该节点不可能产生解),则剪去该节点及其所有子节点,避免无效搜索。

基本算法框架如下:

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表) (递归步骤)
        撤销选择(回溯步骤)

关键在于理解这里的递归,当满足结束条件时,控制流结束递归来到回溯步骤,然后开始新一轮的for循环,具体如下图:

回溯算法可以解决多种类型的问题,包括但不限于:

  • 组合问题:在N个数中按一定规则找出k个数的集合。
  • 切割问题:一个字符串按一定规则有几种切割方式。
  • 子集问题:一个N个数的集合里有多少符合条件的子集。
  • 排列问题:N个数按一定规则全排列,有几种排列方式。
  • 棋盘问题:如N皇后问题、解数独等。

此外,回溯算法还可以应用于图论中的路径问题,如判断在一个矩阵中是否存在一条包含某字符串所有字符的路径等。今天先看一道比较简单的组合问题~~

网站原题

(组合问题)力扣: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

分析解答

先用 n=4, k=2 这样不太大的数举例,使用回溯算法的程序控制流如下:

---以下分析基于力扣评论区的大神题解

用树形结构表达该算法的搜索过程如下:

 下面是不考虑剪枝时的基本解法:

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        # List[List[int]] 表示这个函数应该返回一个列表,其中每个元素是一个列表,列表中元素是整数
        result, track = [], []
        self.backtrack(n, k, 1, track, result)
        return result

    def backtrack(n, k, start, track, result):  
        if len(track) == k:  
            result.append(track[:])  
            return  # 一旦找到组合,返回当前调用,但不影响外部循环  
  
        for i in range(start, n+1):  
            track.append(i)  
            backtrack(n, k, i+1, track, result)  
            track.pop()  # 回溯,移除最后一个元素,准备尝试下一个元素 

注意:对于第10行,新手小白常容易出现的一个错误是写成 result.append(track),输出结果总是嵌套空列表,这是因为result.append(track)实际上将列表变量【track】的引用添加到result列表中,而不是变量【track】存储的列表数据。这意味着当你修改track时,result中的元素也会随之改变。所以正确的写法应该是:

result.append(track[:])

或者:

result.append(track.copy())

另外复习一下,如果想要将 track 中的每个元素分别添加到 result 中,而不是将 track 作为一个整体列表添加进去时,可以使用 extend 方法:

result.extend(track)

将得到类似下面的结果:

[1,2,1,3,1,4,2,3,2,4,3,4]

接下来加入剪枝:

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        result, track = [], []
        self.backtrack(n, k, 1, track, result)
        return result

    def backtrack(self, n, k, start, track, result):
        if k == 0:
            result.append(track[:])
            return
        for i in range(start, n-k+2):
            track.append(i)
            self.backtrack(n, k-1, i+1, track, result)
            track.pop()

 可以看到关键的剪枝步骤在这两行:

self.backtrack(n, k-1, i+1, track, result)

for i in range(start, n-k+2):

注:n-k+2 = n+1 - (k-1)

或者用一种更直观的剪枝策略:

class Solution:  
    def combine(self, n: int, k: int) -> List[List[int]]:  
        result, track = [], []  
        self.backtrack(n, k, 1, track, result)  
        return result  
  
    def backtrack(self, n, k, start, track, result):  
        # 剪枝:如果当前track长度加上剩余可选元素数量小于k,则无需继续递归  
        if len(track) + (n + 1- start) < k:  
            return  
  
        if len(track) == k:  
            result.append(track[:])  
            return  
  
        for i in range(start, n+1):  
            track.append(i)  
            self.backtrack(n, k, i+1, track, result)  
            track.pop()

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值