回溯算法(基于Python)

devtools/2024/10/15 18:30:02/

递归

递归(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 1result = 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())returnfor i in range(0,len(nums)):    # 当前步骤操作if used[i]==1:              # 跳过之前步骤选过的数字continueused[i] = 1                 # 之前没选过的数字入选path.append(nums[i])f(used)                     # 递归path.pop()                  # 回溯used[i] = 0f(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 pathif index == len(digits):result.append(path)returnletters = 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:returnif sum(path)==target:result.append(path.copy())returnfor 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)returnif 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())returnfor 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 0for i in range(1,row+1):if col - i >= 0 and path[row-i][col-i] == 'Q':  # 检查左上角return 0if col + i < n and path[row-i][col+i] == 'Q':   # 检查右上角return 0return 1def f(row):if row == n:result.append([''.join(x) for x in path])returnfor 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.ppmy.cn/devtools/97920.html

相关文章

波导阵列天线单元 学习笔记3 基于空气填充双模馈网的双圆极化膜片天线阵列

摘要&#xff1a; 此通信提出了一种使用空气填充双模馈网的基于膜片极化器的双圆极化天线阵列。一种1分4的圆腔单层覆盖在膜片极化器上来抑制栅瓣。全公司馈网被一个双模传输线所实现&#xff0c;以此在一组馈网内联合了TEM模式&#xff08;由HW悬架线激励&#xff09;和TE10模…

Transformer(课程笔记)

一&#xff1a;Motivation RNN需要顺序的执行&#xff0c;不利于并行计算。 RNN的变体例如GRU、LSTM等需要依靠注意力机制解决信息瓶颈等问题。 抛弃RNN结构&#xff0c;提出了Transformer结构。 Transformer整体架构 二&#xff1a; 输入层&#xff08;BPE&#xff0c;PE&…

Vue:Treeselect基本使用

官网 属性配置 属性含义备注:options数据列表数据格式一定要与下面的对应起来:multiple是否多选当多选的时候&#xff0c;接收参数一定要定义成列表类型searchable可搜索:limitText显示每一组的数量:disable-branch-nodes禁止选中父级 案例 <template><div class&…

Java OkHttp使用(二)

文章目录 引言使用 OkHttp 发送回调其他 引言 记录一下 OkHttp 的使用&#xff1b;OkHttp 异步发送回调请求&#xff0c;增加回调失败重试。 使用 OkHttp 发送回调 /*** 回调重试类*/ Data public class CallBackRetryData {/*** 回调信息JSON*/private JSONObject bodyRequ…

c++--类(上)

C之类&#xff08;上&#xff09; 一、类的定义1.1 类定义格式1.2 访问限定符1.3 类域 二、实例化2.1 实例化的概念2.2 对象大小 三、this指针 一、类的定义 1.1 类定义格式 1、class为定义类的关键字&#xff0c;{}中为类的主体&#xff0c;注意类定义结束时后⾯分号不能省略…

「OC」探索CALayer:基础知识与实用技巧简要介绍

「OC」探索CALayer&#xff1a;基础知识与实用技巧简要介绍 文章目录 「OC」探索CALayer&#xff1a;基础知识与实用技巧简要介绍前言认识CALayerCALayer的相关属性 UIView和CALayer区别联系创建UIView和CALayer的原因 开始创建CALayer视图层级CALayers 和 Sublayersposition与…

CI/CD

目录 1.什么是CI/CD? 2.Gitlab仓库部署 3.部署Jenkins 3.1 使用jenkins拉取代码 3.2 对代码进行编译、打包 4.部署tomcat服务器 1.什么是CI/CD? 通俗来说就是启动一个服务&#xff0c;能够监听代码变化&#xff0c;然后自动执行打包&#xff0c;发布等流程: CICD 是持…

通过Docker部署Synapse服务器

今天我们在阿贝云免费服务器上进行部署测试。阿贝云免费服务器&#xff0c;简直就是IT界的一颗明星&#xff01;1核CPU、1G内存、10G硬盘、5M带宽&#xff0c;简直就是一个不错的免费服务器选择。 首先&#xff0c;让我们简要介绍一下使用到的Docker和Synapse软件。Docker是一…