我感觉回溯的题都挺难的,改用python之后重新整理一个合集吧。
4.26 字符串的排列组合
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]
回溯的思路显而易见,但不知道怎么系统地写这个框架。我把这道题的框架理解为:确定第1个字符、确定第2个字符、… 、确定第n个字符。中间还要防止重复的问题。
题解的亮点在于:
①通过交换字符来完成确定字符的操作
②通过Set来防止重复字符,Set不是全局的,只影响当前函数的循环
③记得递归之后把位置交换回来
class Solution:
def permutation(self, s: str) -> List[str]:
c, res = list(s), []
def dfs(x):
if x == len(c) - 1:
res.append(''.join(c)) # 添加排列方案
return
dic = set()
for i in range(x, len(c)):
if c[i] in dic: continue # 重复,因此剪枝
dic.add(c[i])
c[i], c[x] = c[x], c[i] # 交换,将 c[i] 固定在第 x 位
dfs(x + 1) # 开启固定第 x + 1 位字符
c[i], c[x] = c[x], c[i] # 恢复交换
dfs(0)
return res
如果没有交换回来的操作的话,会出现错误的结果:
可以脑补到:
第一次递归得到abc
第二次递归退回固定第二个字符的情况,交换2/3 得到acb
第三次递归退回固定第一个字符的情况,交换1/2 得到cab
第四次递归的思路同第二次递归,交换2/3 得到cba
第五次递归以cba为基础想要交换1/3 但是a已经在set中,所以pass!
7.1 组合总和
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
请注意上个例子中[1,7]和[7,1]不能同时存在。
回溯的框架写多了应该会熟悉(虽然我现在不熟悉),现在列出两个重点。
- 在python中,如果按照这种格式加入,会出现错误!
res = []
cur = [] #初始化一个cur
....
cur.append(...)
res.append(cur)
因为这样的结果会使所有的cur同步进行!并不会保存当前结果,如下:
正确的做法是在函数形参上加上cur(最好),加入的时候选择res.append(cur[:])
你看第一道例题,如果是字符串,也可以选择res.append(“”.join( c ) )
2.如何保证解法不重复?
这个方法其实在回溯很常用,一定要记住。
就是先使用.sort()排序,然后进行一个判断,控制相同的数的输入逻辑
假设数为
2
1
,
2
2
2_1,2_2
21,22(如:上一个相同的数如果没有取到,下一个也别想取)
这样的结果就是保证出现
(
2
1
,
2
2
)
、
(
2
1
)
、
(
)
(2_1,2_2)、(2_1)、()
(21,22)、(21)、()三种情况。
我们来看两种解法。
首先是我的解法:
排序后,对每个数依次抉择:取还是不取?
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
res, sum, cur = [], 0, []
def dfs(x, sum, cur, flag = 0):
if (sum == target):
res.append(cur[:])
return
if (sum > target):
return
if (x == len(candidates)):
return
dfs(x + 1, sum, cur, flag = 0)
if (x > 0 and candidates[x] == candidates[x - 1] and flag == 0): #防止相同的排列进入回溯
return
cur.append(candidates[x])
dfs(x + 1, sum + candidates[x], cur, flag = 1)
cur.pop()
candidates.sort()
dfs(0, 0, cur)
return res
接下来是官方解法
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
def dfs(begin, path, residue):
if residue == 0:
res.append(path[:])
return
for index in range(begin, size):
if candidates[index] > residue:
break
if index > begin and candidates[index - 1] == candidates[index]:
continue
path.append(candidates[index])
dfs(index + 1, path, residue - candidates[index])
path.pop()
size = len(candidates)
if size == 0:
return []
candidates.sort()
res = []
dfs(0, [], target)
return res
官方的意思是采用循环来回溯
假如canditates是[1,2,3,4,5]
那回溯方案是选1,然后回溯[2,3,4,5];选2,回溯[3,4,5];选3,回溯[4,5]…
对于如何避免相同的数:在同一个循环只选前不选后。
比如在一个循环有[1,1,…],第一个1没有选到了循环的第二次,根据条件第二个1我们也不会选。
如果第一个1选了,跳到了新循环,我们还是可以选第二个1(因为新循环的时候index = begin),也可以不选第二个1。
这个要好好理解。
并且由于排序过,如果target爆了,可以及时使用break打断后续循环。
24点
给你四个数,看能否通过加减乘除括号组成24。
AMD的编程题,能想到要用回溯,但不知道怎么回,我们可以这么假设:
我们不断循环看4个数能组成什么数。
1.4个数选2个数
2.这两个数选择运算(+ - × ÷)
3.4个数变为3个数(这样能解释括号的运算)
4.3个数选2个数
…
其实流程也不是很难是吧,计算一个数的结果是一个三重循环。
附上代码:
def judegePoint24(self, nums:List[int])-> bool:
TARGET = 24
EPSILON = 1e-6
ADD, MUL, SUB, DIV = 0, 1, 2, 3
def solve(nums:List[float])->bool:
if not nums:return False
if len(nums) == 1: return abs(nums[0] - TARGET) < EPSLION)
for i, x in enumerate(nums):
for j, y in enumerate(nums):
if i != j:
newNums = list()
for k, z in enumerate(nums):
if k != i and k != j:
newNums.append(z) #添加没被选的数。
for k in range(4):
if k == ADD:
newNums.append(x + y)
elif k == SUB:
newNums.append(x - y)
elif k == MUL:
newNums.append(x * y)
elif k == DIV:
if (abs(y) < EPSLION):
continue
newNums,append(x / y)
if solve(newNums):
return True
newNums.pop()
return False
return solve(nums)
注意以下几点:
①注意除法的特点,判断除数是否为0。这里的0也不是0,这里的判断24也不是24。由于有除法的存在,不存在绝对的整数,会有一定误差,于是我们定义EPSILON,只要小于0.0001,就说明等于0/等于25。
②记住append完pop掉。回溯惯例。