回溯法

回溯法是深度优先遍历中的一种特有现象,主要用于在一个较大的数据集中寻找满足特定条件的解。回溯法就是当前状态不满足条件时,就回到上一个状态,即回到过去,然后再次向下搜索。在此过程中,需要用到决策树(因为在每一个节点上,都要做出决策要走哪一条路径),在该树上完成向上回溯,其实就是多叉树的遍历问题。

回溯算法在遍历的过程中,如果能够提前判断这一条路径不满足结果,就可以提前结束向下的遍历,即剪枝,可以适当较少复杂度。回溯法与枚举法的区别就在于,枚举是一直走到最后才回退寻找其余路径,直到尝试完所有路径。而回溯可以通过剪枝提前确定这条路是否满足条件,如果不满足,就不再在该条路上继续遍历。

回溯算法的模板:

res= []    # 结果路径集
def backtrack(路径, 待选列表):
    if 满足结束条件:
        res.append(路径)
        return
    
    for 选择 in 待选列表:
        选择一个待选路径放入结果集中,并将该路径从待选列表中移除
        将该选择加入到结果路径集中
        backtrack(路径, 待选列表)
        将刚刚选择的路径再加入到待选列表中
        (撤销选择,因为在回到上一层节点的时候,需要重置上一次的选择,即不满足就回退到上一步)

例题:

全排列https://leetcode-cn.com/problems/permutations/

问题:给定一个 没有重复 数字的序列,返回其所有可能的全排列。

1、画出决策树,遇到每一个节点时判断是否满足结束条件(当前选择中包含所有待选列表的值),如果满足,就加入结果列表集。如果不满足,继续向下一层遍历并选择。

2、(根据模板)遍历待选列表中的值,选择一个放入当前选择列表中,然后将该值从待选列表中删除,加入到当前选择列表中。然后递归实现。之后撤销之前的操作,即回到上一层节点继续向下遍历。

public class Solution{
	List<List<Integer>> permute(int[] nums) {
		LinkedList<Integer> track = new LinkedList<>();
		List<List<Integer>> res = new LinkedList<>();
		backtrack(nums, track, res);
		return res;
	}

	void backtrack(int[] nums, LinkedList<Integer> track, List<List<Integer>> res) {
		if (track.size() == nums.length) {
			res.add(new LinkedList<>(track));
			return;
		}

		for (int i = 0; i < nums.length; i++) {
			if (track.contains(nums[i]))
				continue;
			track.add(nums[i]);
			backtrack(nums, track, res);
			track.removeLast();
		}
	}
}

全排列 II:https://leetcode-cn.com/problems/permutations-ii/

问题:给定一个可包含重复数字的序列,返回所有不重复的全排列

剪枝的条件:当前元素和钱一个元素值相同且前一个没有被使用:nums[i] == nums[i-1] and check[i-1] == 0

递归结束条件:当前结果集长度等于候选列表长度:len(path) == len(nums)

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        res = []
        check = [0]*len(nums)       
        self.backtrack([], nums, check, res)
        return res
        
    def backtrack(self, path, nums, check, res):
        if len(path) == len(nums):
            res.append(path[:])
            return
        
        for i in range(len(nums)):
            if check[i] == 1:
                continue
            if i > 0 and nums[i] == nums[i-1] and check[i-1] == 0:
                continue
            check[i] = 1
            path.append(nums[i])
            self.backtrack(path, nums, check, res)
            path.pop()
            check[i] = 0

8皇后问题

在8*8的棋盘上放8个皇后,且其中任一皇后不能吃掉其余皇后。 皇后可以吃掉同一行、同一列和对角线上的其他皇后。 问:共有多少种放法。

分析:

1.从第0行开始遍历,如果可以循环到最后一行,则说明该条路径满足题意,就输出该结果 。

2.定义一个临时棋盘,用来遍历该行中每一列的所有子情况,

每次循环子情况的时候需要初始化棋盘,将当前行放置的皇后清空,然后将棋盘 [当前行][列]置为1,然后检查循环的当前列是否能放置皇后 。如果不能,就进行下一列循环;如果不能,就回到上一行,即回溯,再重新选择下一列进行遍历。

代码如下:

# 判断当前位置能否放皇后
def isSafety(chess, row, col):
    step = 1
    while row - step >= 0:
        if chess[row - step][col] == 1:
            return False
        if col - step >= 0 and chess[row - step][col - step] == 1:
            return False
        if col + step < 8 and chess[row - step][col + step] == 1:
            return False
        step += 1
    return True


def putQueen(chess, row):
    # 如果循环到最后一行,就输出结果
    if row == 7:
        print("===================================")
        for i in range(8):
            print(chess[i])
        return
    # 如果不是最后一行,则新建临时数组,遍历子情况
    temp = chess.copy()
    # temp = [[0] * 8 for i in range(8)]
    # 遍历当前行所有子情况
    for i in range(8):
        for j in range(8):
            temp[row][j] = 0
        chess[row][i] = 1
        # 如果可以,就放皇后
        if isSafety(chess, row, i):
            putQueen(chess, row + 1)


if __name__ == '__main__':
    count = 0
    chess = [[0] * 8 for i in range(8)]
    putQueen(chess, 0)

组合总和:https://leetcode-cn.com/problems/combination-sum/comments/

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取

代码解析如下:

# for循环和递归配合可以实现回朔: 
# 当递归从递归出口出来之后,上一层的for循环就会继续执行。
# 而for循环的继续执行就会给出当前节点下的下一条可行路径。而后递归调用,就顺着这条从未走过的路径又向下走一步。
class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        n = len(candidates)
        if n == 0:
            return []
        candidates.sort()
        path,res = [],[]
        self.track(0,candidates,path,res,target)
        return res

    def track(self,start,candidates,path,res,target):
        if 0 == target:     #递归终止条件
            res.append(path[:])
            return
        for i in range(start,len(candidates)):    # 走到start节点
            if target - candidates[i] < 0:   # 如果当前节点不满足条件,就不再继续向下搜索
                return
            path.append(candidates[i]) # 如果满足条件,将当前位置放入结果列表
            self.track(i,candidates,path,res,target-candidates[i])#从该节点继续向下走
            path.pop()  # 回溯,回到上一层,所以将当前节点删除

组合总和 II:https://leetcode-cn.com/problems/combination-sum-ii/

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

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

分许:

与上一题的不同之处在于,本题中每个数字在每个组合中只能使用一次,即要去重。其余的思路不变,只是在for循环中,要判断同一层中是否出现相同的值,确保同一层(i > start)中没有相同的元素(candidates[i] == candidates[i-1])。

本题可以采用加的形式,按照顺序依次加上数组中的数字,直到大于等于target,如果等于target,就把等于target的列表结果加入到结果集中。

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        if not candidates:
            return []
        candidates.sort()
        n = len(candidates)
        res = []

        def backtrack(start, tsum, tmp_list):
            if tsum == target:  # 相加等于目标值,递归结束
                res.append(tmp_list)
                return
            for i in range(start, n):
                if tsum + candidates[i]  > target:  # 不满足,剪枝,改层递归结束
                    break
                # 在一个for循环中,所有被遍历到的数都在同一层。一个层级中,不能出现相同的元素。
                if j > start and candidates[i] == candidates[i-1]:
                    continue
                backtrack(i + 1, tsum + candidates[i], tmp_list + [candidates[i]])

        backtrack(0, 0, [])
        return res

子集https://leetcode-cn.com/problems/subsets/

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

分析:本题就是求决策树中所有的节点值

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res,path = [],[]
        self.track(0,[],res,nums)
        return res

    def track(self,start, path,res,nums):
        res.append(path[:])
        for i in range(start,len(nums)):    # i从start开始遍历
            path.append(nums[i])    # 加入选择列表
            self.track(i+1,path,res,nums)   # 回溯
            path.pop()      # 回到上一个状态
        

子集 II:https://leetcode-cn.com/problems/subsets-ii/

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

分析:

去重:在for循环中,要判断同一层中是否出现相同的值,确保同一层(i > start)中没有相同的元素(nums[i] == nums[i-1])。

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        if not nums:
            return []
        res = []
        nums.sort()

        def traceback(start, path):
            if len(path) <= len(nums): # 如果满足条件,就将结果加入到结果集中
                res.append(path[:])
            for i in range(start, len(nums)):
                # 在当前层级下(相同的递归深度),如果前后字符是相同的,
                # 那么跳过这个字符(因为已经处理过了),不能重复添加到path中。
                if i > start and nums[i] == nums[i-1]:
                    continue
                path.append(nums[i])
                traceback(i + 1, path)
                path.pop()

        traceback(0, [])
        return res

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值