leetcode-回溯算法总结

本文深入探讨了回溯算法在解决复杂问题中的应用,包括电话号码的字母组合、括号生成、全排列、子集生成、单词搜索、分割回文串、数独解法等。通过对多个LeetCode经典题目的分析,揭示了回溯算法的设计思想和优化技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

leetcode-17-电话号码的字母组合(letter combination of a phone number)-java

在类中建立一个数字对应字母的map,和对应的结果list

方法1:
进入combine("",digits)
第一个字符串为当前字符串,后面的为还能加上的数字字符串
每次从数组字符串中取最前一位,得到对应的char数组,将nowStr+对应的数组(用for循环,分别加几次),然后再次combine
如果remain为“”,说明一小次循环完成,将nowstr加入result
可以将hashmap 改为char[][]数组,每个数字的字母放在对应的地方

方法2:

public void combine(String digits,List<String> result,String beforString,int nowIndex)

nowIndex为这次进入方法的深度,即在digits的哪个i,根据那个字符得到nowChars数组,进行循环

进入:combine(digits, result, "", 0);

递归方法:每次 combine(digits, result, beforString+nowChars[i], nowIndex+1);

结束条件:nowIndex==digits.length()

leetcode-22-括号生成(generate parentheses)-java

nowstr为当前字符串,left为剩下的左括号,right为剩下的右括号
递归方法:如果right>left说明左边已经有多的( 接下来))或者()都可以,可以加右括号
如果left>0说明left有多的,可以加左括号
结束条件:如果left=right=0 说明此次结束,加入result

leetcode-46-全排列(permutations)-java

解法1(成功,8ms,较慢)

在类中建立一个result的变量,放置结果
建立permutateRemain函数,变量为上一次的全排列几位的list(now)和还能排的数字的list(remain)
在函数中,
如果remain剩余的量为0,将now加入result
对remain进行遍历,在遍历中
遍历的数字为now
先建立一个对now的copy newlist,将newlist加入now
然后将remain除去now,进行新的permutateRemain,然后在remain重新加入now,结束遍历

如何对一个hashset遍历时,还对它进行add,remove操作:

方法1:不用hashset作为剩余集合,用ArrayList,传入下标,将first与first之后的数字一一交换,从而遍历。

方法2:用ArrayList,创建一个visited数组,来得到哪个数字已经被遍历了

leetcode-78-子集(subsets)-java

解法0:

就是从数组,先处理一定有nums[index]的情况,再处理一定没有nums[index]的情况。

但处理没有xxx情况时,调用combine2方法,不把now加入,因为调用combine2方法的方法x,里面的now与combine2的now相同,已经加入过了

解法1(别人的)

不错的方法

https://blog.youkuaiyun.com/wodedipang_/article/details/52996928

使用位操作,不使用递归。首先,计算一下该数组nums一共有多少个子集,设数组nums的长度为n,那么它的子集总数为num=2^n。
设置一个变量index,其初始值为1。那么从0到2^n-1中数,对于每一个数i,用index(从1到10,100,100(2进制))与这个i进行与操作,如果得出的结果大于0,则把该数输入到List<>中取,比较n次,因为数组的长度为n。

解法2(别人的)

回溯算法

这道题需要求给定数组的子集,特别要求有:
1、必须是升序
2、不能出现重复的

所以做法其实也就是,首先排序,然后回溯。。和昨天那题一样,可以回去看一下。记得选择下一个的时候,别和当前的值重复就可以了。

解法3(别人的)

回溯算法|递归实现

本解法采用回溯算法实现,回溯算法的基本形式是“递归+循环”,正因为循环中嵌套着递归,递归中包含循环,这才使得回溯比一般的递归和单纯的循环更难理解,其实我们熟悉了它的基本形式,就会觉得这样的算法难度也不是很大。原数组中的每个元素有两种状态:存在和不存在。

① 外层循环逐一往中间集合 temp 中加入元素 nums[i],使这个元素处于存在状态

② 开始递归,递归中携带加入新元素的 temp,并且下一次循环的起始是 i 元素的下一个,因而递归中更新 i 值为 i + 1

③ 将这个从中间集合 temp 中移除,使该元素处于不存在状态

解法4(别人的)

组合|非递归实现

这种方法是一种组合的方式

① 最外层循环逐一从 nums 数组中取出每个元素 num

② 内层循环从原来的结果集中取出每个中间结果集,并向每个中间结果集中添加该 num 元素

③往每个中间结果集中加入 num

④将新的中间结果集加入结果集中

leetcode-79-单词搜索(word search)-java

使用回溯算法,对board中每个与word首字母相同的字符ij,开启begin函数
begin中
如果result已经为true,不再继续
如果char[i][j]与word首字母不同,不再继续
如果相同,
如果word长度为1,说明已经到最后一步,result=true
否则,char[i][j]='*',因为让接下来的操作不到这一位
然后newWord=word截去首位的字符串
然后根据i,j的位置,对上下左右进行begin
最后char[i][j]=now,恢复
也可以char[i][j]='*',换成一个boolean二维数组,记录跑过的地方

leetcode-131-分割回文串-java

1、每一个结点表示剩余没有扫描到的字符串,产生分支是截取了剩余字符串的前缀;

2、产生前缀字符串的时候,判断前缀字符串是否是回文。

    如果前缀字符串是回文,则可以产生分支和结点;
    如果前缀字符串不是回文,则不产生分支和结点,这一步是剪枝操作。

3、在叶子结点是空字符串的时候结算,此时从根结点到叶子结点的路径,就是结果集里的一个结果,使用深度优先遍历,记录下所有可能的结果。

    采用一个路径变量 path 搜索,path 全局使用一个(注意结算的时候,需要生成一个拷贝),因此在递归执行方法结束以后需要回溯,即将递归之前添加进来的元素拿出去;
    path 的操作只在列表的末端,因此合适的数据结构是栈。

验证回文串那里,每一次都得使用“两边夹”的方式验证子串是否是回文子串。

于是思考“用空间换时间”,利用动态规划把结果先算出来,这样就可以以 O(1) 的时间复杂度直接得到一个子串是否是回文。
 

leetcode-212-单词搜索 II-java

先构建一个字典树,内部结构是char[],然后将字典树的root节点进入回溯,不断回溯时,当前i,j改变时,节点也不断进入下一层。

leetcode-301-删除无效的括号-java

主要的思想是使用回溯法,每走到一个括号,要么使用这个括号,要么不使用这个括号,直到遍历完字符串每个字符后,检查最后字符串的有效性。现在主要的问题是如何对回溯过程进行剪枝。
从题意中要求删去最少个数的括号可以想到,需要删除的左右括号数在递归开始前就可以确定下来,首先对字符串进行一次遍历,用deleteLeft记录当前未匹配的左括号数,用deleteRight记录需要删除的右括号数,因为只能删除不能增加,遇到一个右括号时,若deleteLeft为0,就必定要删除一个多余的右括号,deleteRight加一,若deleteLeft大于0,则该右括号可以与前面一个未匹配的左括号匹配,deleteLeft减一。遍历结束后,剩余的deleteLeft就是需要删除的多余的左括数。
进入递归后,同样用一个数leftCount记录当前构建的字符串中未匹配的左括号数,因为事先已经确定了需要删除的左右括号总数,遇见一个左括号,要deleteLeft大于零才能走到删除该左括号的分支,右括号同理。此外,对于左括号,由于不能确定保留它最后能否形成有效的字符串,所以所有左括号都一定可以走到保留它的分支,到最后递归终点处再由deleteLeft == 0判断是否有冗余的左括号。而对于右括号,当发现leftCount == 0时,若保留该右括号则必定不能形成一个有效字符串,所以可以直接在这里进行剪枝。
 

leetcode-44-通配符匹配-java

解法1

动态规划

(一)状态

    f[i][j]表示s1的前i个字符,和s2的前j个字符,能否匹配

(二)转移方程

    如果s1的第 i 个字符和s2的第 j 个字符相同,或者s2的第 j 个字符为 “?”
    f[i][j] = f[i - 1][j - 1]


    如果s2的第 j 个字符为 *
        若s2的第 j 个字符匹配空串, f[i][j] = f[i][j - 1]
        若s2的第 j 个字符匹配s1的第 i 个字符, f[i][j] = f[i - 1][j]
        这里注意不是 f[i - 1][j - 1], 举个例子就明白了 (abc, a*) f[3][2] = f[2][2]

(三)初始化

    f[0][i] = f[0][i - 1] && s2[i] == *
    即s1的前0个字符和s2的前i个字符能否匹配(是否s2的前i个字符都是*)

(四)结果

    f[m][n]
 

leetcode-10-正则表达式匹配-java

解法1

回溯法

如果模式串中有星号,它会出现在第二个位置,即 pattern[1] 。这种情况下,我们可以直接忽略模式串中这一部分,或者删除匹配串的第一个字符,前提是它能够匹配模式串当前位置字符,即 pattern[0]。如果两种操作中有任何一种使得剩下的字符串能匹配,那么初始时,匹配串和模式串就可以被匹配。

解法2

因为题目拥有 最优子结构 ,一个自然的想法是将中间结果保存起来。我们通过用 dp(i,j)表示 text[i:]和 pattern[j:] 是否能匹配。我们可以用更短的字符串匹配问题来表示原本的问题。

我们用中同样的回溯方法,除此之外,因为函数 match(text[i:], pattern[j:]) 只会被调用一次,我们用 dp(i, j)来应对剩余相同参数的函数调用,这帮助我们节省了字符串建立操作所需要的时间,也让我们可以将中间结果进行保存。
 

leetcode-39-组合总和-java

回溯算法,combine方法,如果target<0直接返回即可。如果等于0,将list里的内容复制到copy,然后排序,将排序后的copy里的元素组成string,放入map中。如果大于0,则遍历candidates里的每个元素,把list里加入num,然后combine方法,里面target为target-num。

最后把map里的value放入lists中。

主要缺点就是没有考虑到list的重复,这样就不用放入map中进行过滤了

解决不重复问题

重复的原因是,重复的原因是在较深层的结点值考虑了之前考虑过的元素,因此我们需要设置“下一轮搜索的起点”即可

在搜索的时候,需要设置搜索起点的下标 begin ,由于一个数可以使用多次,下一层的结点从这个搜索起点开始搜索;
在搜索起点 begin 之前的数因为以前的分支搜索过了,所以一定会产生重复。

因为不会产生重复,所以不用map,直接加入result里面即可
 

剪枝提速

如果一个数位搜索起点都不能搜索到结果,那么比它还大的数肯定搜索不到结果,基于这个想法,我们可以对输入数组进行排序,以减少搜索的分支;

排序是为了提高搜索速度,非必要;

搜索问题一般复杂度较高,能剪枝就尽量需要剪枝。把候选数组排个序,遇到一个较大的数,如果以这个数为起点都搜索不到结果,后面的数就更搜索不到结果了。
 

剑指offer-28-字符串的排列-java

我们可以先考虑特例,即字符串中没有重复字符的时候该怎么办。
没有重复字符的时候我们可以,固定第一位,使第一位依次与后面的各位交换。然后递归对第一位后面的子字符串进行相同操作。
然后我们考虑有重复项的时候
方法1:例如:对abb,第一个数a与第二个数b交换得到bab,然后考虑第一个数与第三个数交换,此时由于第三个数等于第二个数,所以第一个数就不再用与第三个数交换了。再考虑bab,它的第二个数与第三个数交换可以解决bba。此时全排列生成完毕!
方法2::最后加入一个HashSet进行去重

leetcode-037-解数独

解法1(别人的)

类似人的思考方式去尝试,行,列,还有 3*3 的方格内数字是 1~9 不能重复。

我们尝试填充,如果发现重复了,那么擦除重新进行新一轮的尝试,直到把整个数组填充完成。

数独首先行,列,还有 3*3 的方格内数字是 1~9 不能重复。

声明布尔数组,表明行列中某个数字是否被使用了, 被用过视为 true,没用过为 false。

初始化布尔数组,表明哪些数字已经被使用过了。

尝试去填充数组,只要行,列, 还有 3*3 的方格内 出现已经被使用过的数字,我们就不填充,否则尝试填充。

如果填充失败,那么我们需要回溯。将原来尝试填充的地方改回来。

递归直到数独被填充完成。

leetcode-040-组合总和2

解法1(别人的)

与第 39 题(组合之和)的差别

这道题与上一问的区别在于:

第 39 题:candidates 中的数字可以无限制重复被选取;

第 40 题:candidates 中的每个数字在每个组合中只能使用一次。

相同点是:相同数字列表的不同排列视为一个结果。

如何去掉重复的集合(重点)

为了使得解集不包含重复的组合。有以下 22 种方案:

使用 哈希表 天然的去重功能,但是编码相对复杂;

这里我们使用和第 39 题和第 15 题(三数之和)类似的思路:不重复就需要按 顺序 搜索, 在搜索的过程中检测分支是否会出现重复结果 。注意:这里的顺序不仅仅指数组 candidates 有序,还指按照一定顺序搜索结果。

由第 39 题我们知道,数组 candidates 有序,也是 深度优先遍历 过程中实现「剪枝」的前提。
将数组先排序的思路来自于这个问题:去掉一个数组中重复的元素。很容易想到的方案是:先对数组 升序 排序,重复的元素一定不是排好序以后相同的连续数组区域的第 11 个元素。

也就是说,剪枝发生在:同一层数值相同的结点第 22、33 ... 个结点,因为数值相同的第 11 个结点已经搜索出了包含了这个数值的全部结果,同一层的其它结点,候选数的个数更少,搜索出的结果一定不会比第 11 个结点更多,并且是第 11 个结点的子集。(说明:这段文字很拗口,大家可以结合具体例子,在纸上写写画画进行理解。)

说明:

解决这个问题可能需要解决 第 15 题(三数之和)、 第 47 题(全排列 II)、 第 39 题(组合之和)的经验;

对于如何去重还不太清楚的朋友,可以参考当前题解的 高赞置顶评论 。

解释语句: if cur > begin and candidates[cur-1] == candidates[cur] 是如何避免重复的。

leetcode-047-全排列2

解法1(别人的)

为什么要先排序 因为结果是要去重的,而结果都是列表形式的,那列表怎么判重呢?就是排序之后一个一个的比较,所以那还不如先排序之后再计算,再计算的过程中判断是否有重复,免得对每个结果再做一次排序去重操作。

怎么判断当前分支是否是重复的分支。 因为之前已经拍好序了,所以当前元素nums[i] 如果和前一个元素相同 ,即nums[i] == nums[i-1]就说明该分支 有可能 是重复的。 但是这个相等条件有两种可能 一种是,1 1‘ 2,也就是选择完1之后再选择第二个1,两个元素虽然重复,但是第二个元素是前一个元素的下一层,这时是没有问题的。 另一种是之前的 同层 分支已经有 1 1‘ 2了,这次的选择是 1‘ 1 2 。两个元素重复,且重的是同层路径。那就说明是重复分支。

具体区分的办法是 nums[i-1] 的used状态是被选择的,那么说明当前的nums[i] 是 nums[i-1]的下一层路径。 否则如果 nums[i-1] 的状态是没被选择的,那么说明当前 的nums[i] 是nums[i-1] 同层路径。

leetcode-051-n皇后

解法1(成功,2ms,极快)

本题也是回溯算法的经典题目。N 皇后问题也是一个暴力穷举的问题。
其实题目可以改成,在每一行放置一个皇后,让这些皇后不能相互攻击。(因为每一行最多只有一个皇后)
我们可以先不考虑每一个皇后之间不能相互攻击的条件,如果要求每行只能放一个皇后,我们能否穷举出所有的放置方法?
考虑皇后之间相互攻击的问题
对于每一个格子进行计算分析能不能放置皇后,最后的代码实现会跳过这些格子,把皇后放在合法的位置上。
具体的,在每一个位置放置皇后,然后调用 backtrack 函数,进入下一行进行穷举进行判断
isValid 函数的逻辑
按照题意我们需要检查八个方向是否有其他方向,但实际上我们只需要检查三个方向即可。
因为我们的逻辑是每一行只放一个皇后,所以这个皇后的左边和右边不需要进行检查了。
因为我们是一行一行从上向下放置皇后,所以下方,左下方和右下方不会有皇后(还没放皇后)。
最后我们需要检查的只有上方、左上和右上三个方向。
斜方向45度,只需target-i=span,然后看target-queens[i]是否等于+-span即可。
函数 backtrack 依然像个在决策树上游走的指针,每个节点就表示在 board[row][col] 上放置皇后,通过 isValid 函数可以将不符合条件的情况剪枝。

leetcode-052-n皇后2

解法1(成功,3ms,较快)

这道题和「51. N 皇后」非常相似,区别在于,第 5151 题需要得到所有可能的解,这道题只需要得到可能的解的数量。因此这道题可以使用第 5151 题的做法,只需要将得到所有可能的解改成得到可能的解的数量即可。

皇后的走法是:可以横直斜走,格数不限。因此要求皇后彼此之间不能相互攻击,等价于要求任何两个皇后都不能在同一行、同一列以及同一条斜线上。

直观的做法是暴力枚举将 N 个皇后放置在 N \times N×N 的棋盘上的所有可能的情况,并对每一种情况判断是否满足皇后彼此之间不相互攻击。暴力枚举的时间复杂度是非常高的,因此必须利用限制条件加以优化。

显然,每个皇后必须位于不同行和不同列,因此将 N 个皇后放置在 N×N 的棋盘上,一定是每一行有且仅有一个皇后,每一列有且仅有一个皇后,且任何两个皇后都不能在同一条斜线上。基于上述发现,可以通过回溯的方式得到可能的解的数量。

回溯的具体做法是:依次在每一行放置一个皇后,每次新放置的皇后都不能和已经放置的皇后之间有攻击,即新放置的皇后不能和任何一个已经放置的皇后在同一列以及同一条斜线上。当 N 个皇后都放置完毕,则找到一个可能的解,将可能的解的数量加 1。

由于每个皇后必须位于不同列,因此已经放置的皇后所在的列不能放置别的皇后。第一个皇后有 N 列可以选择,第二个皇后最多有 N-1 列可以选择,第三个皇后最多有 N-2 列可以选择(如果考虑到不能在同一条斜线上,可能的选择数量更少),因此所有可能的情况不会超过 N! 种,遍历这些情况的时间复杂度是 O(N!)。

为了降低总时间复杂度,每次放置皇后时需要快速判断每个位置是否可以放置皇后,显然,最理想的情况是在 O(1) 的时间内判断该位置所在的列和两条斜线上是否已经有皇后。

以下两种方法分别使用集合和位运算对皇后的放置位置进行判断,都可以在 O(1) 的时间内判断一个位置是否可以放置皇后,算法的总时间复杂度都是 O(N!)。

方法一:基于集合的回溯
为了判断一个位置所在的列和两条斜线上是否已经有皇后,使用三个集合 columns、diagonals 1和diagonals 2

分别记录每一列以及两个方向的每条斜线上是否有皇后。

列的表示法很直观,一共有 N 列,每一列的下标范围从 0 到 N-1,使用列的下标即可明确表示每一列。

如何表示两个方向的斜线呢?对于每个方向的斜线,需要找到斜线上的每个位置的行下标与列下标之间的关系。

方向一的斜线为从左上到右下方向,同一条斜线上的每个位置满足行下标与列下标之差相等,例如 (0,0) 和 (3,3) 在同一条方向一的斜线上。因此使用行下标与列下标之差即可明确表示每一条方向一的斜线。


方向二的斜线为从右上到左下方向,同一条斜线上的每个位置满足行下标与列下标之和相等,例如 (3,0) 和 (1,2)在同一条方向二的斜线上。因此使用行下标与列下标之和即可明确表示每一条方向二的斜线。


每次放置皇后时,对于每个位置判断其是否在三个集合中,如果三个集合都不包含当前位置,则当前位置是可以放置皇后的位置。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值