回溯算法(基于Python)

news2024/9/20 5:37:39

递归

递归(recursion)是一种算法策略,通过函数调用自身来解决问题。"递"指程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。"归"指触发终止条件后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。递归代码主要包含三个要素:

        1. 终止条件:用于决定什么时候由“递”转“归”。递归的结束条件成为递归出口。

        2. 递归调用:对应“递”,函数调用自身,通常输入更小或更简化的参数。

        3. 返回结果:对应“归”,将当前递归层级的结果返回至上一层。

递归算法解题通常显得很简洁,‌但运行效率较低‌。‌因此,‌在使用递归算法时,‌需要特别注意递归出口的设计,‌以避免出现死循环或栈溢出等问题‌。

递归算法的一个简单例子:以计算一个函数 f 在 n 处的取值为例,假设有 f(n) = g(n,f(n-1)),则首先判断是否达到终止条件,达到则直接返回结果,否则返回g(n,f(n-1)),如计算 f(n) = 1 + 2 + ... + n

def f(n):

    if n==1:            # 终止条件
        return 1

    result = n + f(n-1)  # 第n项和第n-1项的关系

    return result

回溯

回溯是递归过程,算法程序的主体就是递归函数。回溯算法可以抽象为 n 叉树,叶子结点就是递归函数要收集的结果,对应终止条件,满足终止条件时应当 return 。下面是 for 循环,用于处理集合元素,for 循环内是处理节点、递归函数、回溯操作。一次回溯就相当于一次 for 循环。

回溯的三个要素:递归函数的参数、终止条件、当前操作和子问题。

结果录入程序写在哪:仅叶子结点记入结果(如46. 全排列),则结果录入这一步放在终止条件成立时的程序内部,每个节点都记入结果(如78. 子集),则结果录入这一步放在递归函数第一行(条件成立时的程序前面)。

当前步骤的操作执行后写递归子问题的语句,递归子问题的语句后面写回溯语句。

若 path 是列表则录入结果这一步中应将其写为 path.copy()  (如46. 全排列),其他地方写 path 。若 path 是字符串则递归函数内第一行写 nonlocal path (如22. 括号生成),其他地方写 path 。回溯操作时数组写 path.pop() ,字符串写 path[0:len(path)-1] 。


LeetCode题目

由于回溯问题都可以抽象为 n 叉树,因此我们阶梯时先将问题对应的树画出来,再根据树的结构来写程序。


46. 全排列

题目:给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。例如输入 nums = [1,2,3],输出[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

算法:以 nums = [1,2,3] 为例,其树形图如下

其中 () 表示已经选取的元素,[] 表示还原数组中还未被选取的元素,{} 表示原数组各位置的元素是否已被选取过。

用 used 表示上面的 {}需要收集的结果是上图中的叶子结点 path 。因此终止条件达成时才会收集结果

递归函数的输入参数: used 。

终止条件: path 的长度已经等于 nums 。

当前操作:本次选哪个数。

子问题:剩余的数组成的排列。

def permute(nums):
        
    result, path = [], []
    used_init = [0]*len(nums)
        
    def f(used):
        if len(path)==len(nums):        # 终止条件
            result.append(path.copy())
            return
        for i in range(0,len(nums)):    # 当前步骤操作
            if used[i]==1:              # 跳过之前步骤选过的数字
                continue
            used[i] = 1                 # 之前没选过的数字入选
            path.append(nums[i])
            f(used)                     # 递归
            path.pop()                  # 回溯
            used[i] = 0
        
    f(used_init)

    return result

continue 是避免一个元素被多次选入 path 中的情况,以树状图中的第一列第一层到第二层的过程为例,前面的步骤已经选过 1 了,则下面只能再选 2 和 3 ,若循环中遇到 1 则应当跳过本次迭代( i = 1 )而直接进入下面的迭代( i = 2,3 )。

注意程序中的回溯操作包含两行,分别是 path.pop() 和 used[i]=0

在上述代码中,‌path.copy() 的使用是必要的,‌因为 Python 中的列表是可变对象。‌这意味着如果你直接将 path 列表添加到 result 列表中,‌那么当你后续修改 path 时,‌result 中已经添加的那些 path 列表的引用也会跟着改变,‌因为它们都指向同一个内存地址。‌为了避免这种情况,‌我们创建一个 path 列表的副本,‌并将其添加到 result 中。‌这样,‌即使后续修改了 path,‌result 中存储的副本也不会受到影响。‌


78. 子集

题目:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。例如输入 nums = [1,2,3],输出[[],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]]

算法:以 nums = [1,2,3] 为例,其树形图如下

需要收集的结果 path 是上图中的每个节点(不只是叶子结点)终止条件未达成时也需要收集结果

递归函数的输入参数:从哪一步开始(start)。

终止条件:开始的位置已经超出数组最后一位(start > len(nums)-1)。

当前操作:本次选哪个数。

子问题:从下一个位置开始能得到哪些子集。

path 从空集 {} 开始,备选元素集从 nums 开始,第 0 轮可取 1,2,3,递归函数中第一步就要加入 path 因为上图中的树没有加入空集的步骤,其每层生成的依次为大小 1,2,3 的子集。

start = 0 的时候输入 path = {} ,递归函数第一步 result.append(path.copy()) 会把 path 加入中 result,然后开始对其进行树的第一层操作,选取一个元素加入中 path,选择了 1 则下面一步的可选集合为 {2,3},选择了 2 则下一步可选集合为 {3} (为了避免和前面一种情况重复因此不可再选 1),选择了 3 则下一步可选集合为 {} (为了避免和前面一种情况重复因此不可再选 12)。下面两层的情况同理。path.pop() 的作用以第一列第二层为例,选了 2 得到 {1,2} 后再退回 {1} 才能再选 3 变成 {1,3} ,不退回则得到的是 {1,2,3}

def subsets(nums):
   
    result, path = [], []
        
    def f(start):
        result.append(path.copy())
        if start == len(nums):
            return       
        for i in range(start,len(nums)):
            path.append(nums[i])
            f(i+1)
            path.pop()

    f(0)
            
    return result

17. 电话号码的字母组合

题目:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

例如输入 '23' ,输出 ['ad','ae','af','bd','be','bf','cd','ce','cf'] 。

算法:以 digits = '23' 为例,其树形图如下

其中 () 表示已经选取的元素,[] 表示还原数组中还未被选取的元素。

用 index 表示遍历到了哪一位,需要收集的结果是上图中的叶子结点 path 。因此终止条件达成时才会收集结果

递归函数的输入参数: index 。

终止条件: index 的长度已经等于 digits (最后一步是 index = len(digits)-1)

当前操作:本次选 index 对应数字的对应字母集的哪个字母。

子问题:剩余的数字的对应字母集的字母组合。

def letterCombinations(digits):

    dic = {2:'abc', 3:'def', 4:'ghi', 5:'jkl', 6:'mno', 7:'pqrs', 8:'tuv', 9:'wxyz'}
    result, path = [], ''
        
    def f(index):
        nonlocal path
        if index == len(digits):
            result.append(path)
            return
        letters = dic[int(digits[index])]
        for i in letters:
            path += i  # 添加当前字符到路径中
            f(index + 1)    # 递归调用,‌处理下一个数字
            path = path[0:(len(path)-1)]  # 回溯,‌移除最后一个字符,‌尝试下一个可能的字符
        
    if digits:  # 如果输入的数字字符串非空
        f(0)
    return result

如果你在一个嵌套函数内部修改了一个在外部函数中定义的变量(path),‌你需要确保这个变量在嵌套函数中被当作是非局部变量处理。‌这可以通过在嵌套函数内部使用 nonlocal 关键字来实现。‌


39. 组合总和

题目:给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

例:输入 candidates = [2,3,5] , target = 8 ,输出 [[2,2,2,2],[2,3,3],[3,5]] 。

算法:以 candidates = [2,3,5] , target = 4 为例,其树形图如下

其中 {} 表示已经选取的元素,[] 表示还原数组中还未被选取的元素。

递归函数的输入参数: start 。

终止条件: sum(path)>target(此时直接返回)或sum(path)==target(此时将path加入结果再返回)

当前操作:本次选哪个数。

子问题:从本次选取数字开始到 candidates 结尾的数字的组合。

def combinationSum(candidates, target):

    result, path = [], []

    def f(start):
        if sum(path)>target:
            return
        if sum(path)==target:
            result.append(path.copy())
            return
        for i in range(start, len(candidates)):
            path.append(candidates[i])
            f(i)
            path.pop()

    f(0)

    return result            

22. 括号生成

题目:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。例如输入 n = 3 ,返回 n = ['((()))','(()())','(())()','()(())','()()()'] 。

算法:以 n = 2 为例,其树形图如下

递归函数的输入参数:左右括号的个数 n_left 和 n_right 。

终止条件: path长度已经达到2n

当前操作:选左括号还是右括号。

子问题:后面剩余的括号怎么选。

需要注意的是左右括号的选择条件,当左右括号数量相同且左括号少于n个时只能选取左括号,当左括号已有n个时只能取右括号,当左括号数量大于右括号数量且左括号少于n个时可以选左括号或右括号。

def generateParenthesis(n):
    
    result, path = [], ''
        
    def f(num_left, num_right):

        nonlocal path                               # path为字符串时的必要步骤

        if len(path) == 2 * n:                      # 终止条件
            result.append(path)
            return

        if num_left < n and num_left > num_right:   # 可(可)的情况
            path += '('
            f(num_left + 1, num_right)
            path = path[0:len(path)-1]
            path += ')'
            f(num_left, num_right + 1)
            path = path[0:len(path)-1]

        if num_left < n and num_right == num_left:  # 只可(的情况
            path += '('
            f(num_left + 1, num_right)
            path = path[0:len(path)-1]

        if num_left==n:                             # 只可)的情况
            path += ')'
            f(num_left, num_right + 1)
            path = path[0:len(path)-1]
        
    f(0, 0)

    return result

131. 分割回文串

题目

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。例如输入 s = 'aab' 输出 [['a','a','b'],['aa','b']] 

算法:以 s = 'aab' 为例,其树形图如下

递归函数的输入参数:开始切割的位置 start

终止条件: 切割位置已经超出最大( start = len(s) )。

当前操作:选择一个大于等于 start 的位置切割。

子问题:后面剩余的部分怎么切割。

def partition(s):

    result, path = [], []

    def backtrack(start):
        if start == len(s):
            result.append(path.copy())
            return
        for i in range(start, len(s)):
            s_candidate = s[start:i+1]
            if s_candidate == s_candidate[::-1]:  # 注意此处判断是否分割出回文子串
                path.append(s[start:i + 1])
                backtrack(i + 1)
                path.pop()

    backtrack(0)
    return result

51. N皇后

题目:按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

例:

算法:树形图第一层选择的是第一行放入棋子的位置,第二层选择第二行放入棋子的位置,以此类推。

递归函数的输入参数:要放置哪一行 row

终止条件:放置位置已经超出最大( row = n )。

当前操作:选择在本行哪个位置放置棋子。

子问题:后面剩余的部分怎么切割。

def solveNQueens(n):
        
    result, path = [], [['.'] * n for _ in range(n)]

    def is_valid(path, row, col):
        for i in range(0,row):
            if path[i][col] == 'Q':  # 检查列
                return 0
        for i in range(1,row+1):
            if col - i >= 0 and path[row-i][col-i] == 'Q':  # 检查左上角
                return 0
            if col + i < n and path[row-i][col+i] == 'Q':   # 检查右上角
                return 0
        return 1

    def f(row):
        if row == n:
            result.append([''.join(x) for x in path])
            return
        for col in range(n):
            if is_valid(path, row, col)==1:
                path[row][col] = 'Q'
                f(row + 1)
                path[row][col] = '.'

    f(0)

    return result

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2056846.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码块分类

局部代码块 public class Test {public static void main(String[] args) {{int a 10;}// 执行到此处时候,变量a已经从内存中消失了。 // System.out.println(a);} } 构造代码块 public class Test {private String name;private int age;{// 构造代码块System.out.…

【STM32 Blue Pill编程】-定时器与中断

定时器与中断 文章目录 定时器与中断1、硬件准备及接线2、GPIO配置3、代码实现STM32F103C8 配有四个定时器,分别为 TIM1、TIM2、TIM3 和 TIM4。 它们充当时钟并用于跟踪基于时间的事件。 我们将展示如何使用 HAL 库在 STM32Cube IDE 中对这些定时器进行编程。 本文将涉及如下内…

【网络】抓包工具的使用

抓包工具 文章目录 1.tcpdump抓包1.1安装 tcpdump1.2常见使用 2.wireshark抓包 1.tcpdump抓包 TCPDump 是一款强大的网络分析工具&#xff0c; 主要用于捕获和分析网络上传输的数据包。 1.1安装 tcpdump tcpdump 通常已经预装在大多数 Linux 发行版中。 如果没有安装&#…

常见java OOM异常分析排查思路分析

Java 虚拟机&#xff08;JVM&#xff09;发生 OutOfMemoryError&#xff08;OOM&#xff09;异常时&#xff0c;表示 JVM 在尝试分配内存时无法找到足够的内存资源。以下是几种常见的导致 OOM 异常的情况&#xff1a; 1. Java 堆空间不足 (Java Heap Space) 这种情况发生在 J…

【小球下落反弹】小球自由落下,每次落地后反跳回原高度的一半

一小球从100米高度自由落下&#xff0c;每次落地后反跳回原高度的一半&#xff1b;再落下&#xff0c;求它在第10次落地时&#xff0c;共经过多少米&#xff1f;第10次反弹多高&#xff1f; 使用C语言实现&#xff0c;具体代码&#xff1a; #include<stdio.h>int main(…

wo是如何克服编程学习中的挫折感的?

你是如何克服编程学习中的挫折感的&#xff1f; 编程学习之路上&#xff0c;挫折感就像一道道难以逾越的高墙&#xff0c;让许多人望而却步。然而&#xff0c;真正的编程高手都曾在这条路上跌倒过、迷茫过&#xff0c;却最终找到了突破的方法。你是如何在Bug的迷宫中找到出口的…

HarmonyOs透明弹窗(选择照片弹窗样式)

1.鸿蒙中需要实现一个如下图的弹窗 2.由上图中可以得出&#xff0c;只需要三个Text组件依次向下排列&#xff0c;弹窗背景设置透明即可&#xff0c;弹窗代码如下(仅展示弹窗样式)&#xff1a; /**** 自定义选择图片弹窗** 外部定义需要导出*/ CustomDialog //自定义弹窗 export…

Linux驱动学习之点灯(一)

学习不同的板子我们都是从点灯开始&#xff0c;linux驱动也不例外 驱动开发基础知识 何为驱动&#xff1f; 驱使硬件正常工作的代码就叫做驱动。 在一些mcu里&#xff1a; 无非就是直接操作寄存器&#xff0c;或者用库函数初始化外设&#xff0c;使外设正常工作如初始化iic&…

leetcode13. 罗马数字转整数,流程图带你遍历所有情况

leetcode13. 罗马数字转整数 示例 1: 输入: s “III” 输出: 3 示例 2: 输入: s “IV” 输出: 4 示例 3: 输入: s “IX” 输出: 9 示例 4: 输入: s “LVIII” 输出: 58 解释: L 50, V 5, III 3. 示例 5: 输入: s “MCMXCIV” 输出: 1994 解释: M 1000, CM 900, XC…

一个贼好用的开源导航网站项目——pintree!【送源码】

这两天发现了一个项目&#xff0c;它可以快速的将收藏夹里的网址导出&#xff0c;然后快速生成一个在线的网站。这个项目就是 pintree。 项目简介 Pintree 是一个开源项目&#xff0c;旨在将浏览器书签转换为导航网站。只需几个简单的步骤&#xff0c;就可以将书签转变为美观…

【CAN-IDPS】汽车网关信息安全要求以及实验方法

《汽车网关信息安全技术要求及试验方法》是中国的一项国家标准,编号为GB/T 40857-2021,于2021年10月11日发布,并从2022年5月1日起开始实施 。这项标准由全国汽车标准化技术委员会(TC114)归口,智能网联汽车分会(TC114SC34)执行,主管部门为工业和信息化部。 该标准主要…

集团数字化转型方案(二)

集团数字化转型方案通过整合物联网&#xff08;IoT&#xff09;、大数据分析、人工智能&#xff08;AI&#xff09;和云计算技术&#xff0c;构建了一个全面智能化的业务平台&#xff0c;从而实现了全集团范围内的业务流程自动化、数据驱动决策优化、以及客户体验的个性化提升。…

windows11 安装 Rancher Desktop

从官网下载了最新版的 Rancher.Desktop.Setup.1.8.1.msi 安装包&#xff0c;安装很顺利。 但是安装完&#xff0c;启动时报错 The k3s cache is empty and there is no network connection. 不明所以&#xff0c;网上查了&#xff0c;原来是github访问不了的原因&#xff0c;具…

这些星座比你想象的还努力

TOP 3. 金牛座   金牛座对于操劳操心的忍受度本来就比较高&#xff0c;对于金牛座来说这些都是踏实的象征&#xff0c;金牛座比较不相信不劳而获这件事情&#xff0c;多少血汗多少付出&#xff0c;得到多少收获&#xff0c;这让金牛座比较踏实&#xff0c;不会觉得很不安&…

CE游戏教程第三关解密

游戏规则&#xff1a;雪人可以左右移动&#xff0c;跳跃&#xff0c;跳跃到红线上&#xff0c;红线变绿&#xff0c;所有红线变率时&#xff0c;门开启&#xff0c;雪人通过门后&#xff0c;游戏胜利。如果游戏中触碰到NPC&#xff0c;游戏失败&#xff01; 经过研究&#xff…

DHCP的原理与配置

目录 DHCP的原理 DHCP是什么 DHCP的好处 DHCP的分配方式 DHCP的工作原理 DHCP的配置 环境设置 DHCP配置 验证配置是否成功 DHCP的原理 DHCP是什么 DHCP:Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议。由Internet工作小组开发&#xff0c;专门用…

自然语言处理NLP四范式

版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhlNLP四范式概述 自然语言处理(NLP)的四范式是NLP领域发展历程中的重要里程碑,它们代表了NLP技术从传统方法到现代深度学习技术的转变。第一范式是非神经网络时代的完全监督学习,它依赖于人工设…

Python基础知识学习总结(五)

一. 字典 字典是另一种可变容器模型&#xff0c;且可存储任意类型对象。 字典的每个键值 key>value 对用冒号 : 分割&#xff0c;每个对之间用逗号( , )分割&#xff0c;整个字典包括在花括号 {} 中 。 dict 作为 Python 的关键字和内置函数&#xff0c;变量名不建议命名…

CompletableFuture-应用

可以看到CompletableFuture实现了CompletionStage 和Future的两个接口。CompletionStage提供了任务之间的衔接能力&#xff0c;而Future则是经常用于阻塞获取结果。 CompletableFuture 的内部使用了基于 ForkJoinPool 的线程池&#xff0c;这种线程池可以高效地调度和执行任务…

数据结构与算法--交换排序与归并排序

文章目录 回顾提要冒泡排序冒泡排序的过程冒泡排序的实现冒泡排序算法评价 快速排序快速排序的划分方法快速排序的过程快速排序的实现快速排序算法性能分析快速排序的改进 归并排序二路归并排序合并两个有序表归并排序示例归并排序算法性能分析 各种内排序方法的比较各种内排序…