回溯(更新至8.29)

我感觉回溯的题都挺难的,改用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]不能同时存在。
回溯的框架写多了应该会熟悉(虽然我现在不熟悉),现在列出两个重点。

  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掉。回溯惯例。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值