回溯算法框架解析

原文请看回溯算法框架

这两天在刷Leetcode N皇后单词搜索时,探究回溯算法的解题思路,结合资料整理框架模版,便于总结和参考。解决回溯问题,就是解决决策树问题,当前的决策对后面的选择至关重要,话说条条大路通罗马,每一次决策过程就是遍历一条可走得通的路,但是算法是有条件的,往往只有一条几条路走得通。

在决策过程中过程中,要考虑三个问题:

  • 路径选择: 已经做出的选择
  • 选择列表: 所有可供的选择
  • 结束条件: 也就是决策树底层,无法再做出其他选择

代码模版则是:

result = []
def backtrack(路径, 选择列表):
    if 满足条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

暂时不理解没关系,我也是在做题时把模版往题上套才慢慢明白。

LeetCode 79 单词搜索

题目描述:

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:
在这里插入图片描述

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

示例 2:

在这里插入图片描述

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true

示例 3:

在这里插入图片描述

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false

分析

本题从网格中相邻的字符串中找出word,题目要求同一个单元格的字母不允许重复,如上图示例1,找到第一个’C’ 时,即可以继续向右边寻找,结果是‘E’,不对,又可以返回向下寻找’C‘,对了,继续在第二个’C’的相邻单元格中寻找,就是上述模版:

做选择-〉判断是否满足要求-〉撤销选择的过程

代码模版可以大致为:

result = []
def backtrack(word, board):
    # 满足 return False
    if board[i][j] != word[k]:
        return 失败
        
    if 找到 word:
        return 成功
        
    # 网格中遍历
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

代码解析

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        self.board = board
        self.word = word

        if self.board == None:
            return False

        self.h = len(self.board)
        self.w = len(self.board[0])
        self.help_list = [[0] * len(self.board[0]) for _ in range(len(self.board))]

        for i in range(len(board)):
            for j in range(len(board[0])):
                if self.backtrack(i, j, 0):
                    return True
        
        print(self.help_list)
        return False

    def backtrack(self, i, j, k):

            if self.board[i][j] != self.word[k]:
                return False
            self.help_list[i][j] = 1

            if k == len(self.word) - 1:
                return True

            # 方法一: 先设定方向,利用数组选择方向
            # direction = [(0,1), (0,-1), (-1,0), (1,0)]
            # for di, dj in direction:
            #     newdi, newdj = i + di, j + dj
            #     if 0 <= newdi < len(self.board) and 0 <= newdj < len(self.board[0]):
            #         if self.help_list[newdi][newdj] == 0:
            #             if self.backtrack(newdi, newdj, k + 1):
            #                 result = True
            #                 break


            # 方法二: 计算方向
            # 回溯法原理,只要有其中一个结果符合就进行下一步操作, 不符合这试试相邻的, 结果之间不互斥,
            # 但是所有结果都不符合则撤销该选择,返回False
            if 0 <= j+1 < self.w and self.help_list[i][j+1] == 0:
                if self.backtrack(i, j+1, k+1):
                    return True
            if self.w > j-1 >= 0 and self.help_list[i][j-1] == 0:
                if self.backtrack(i, j-1, k+1):
                    return True

            if self.h > i-1 >= 0 and self.help_list[i-1][j] == 0:
                if self.backtrack(i-1, j, k+1):
                    return True

            if 0 <= i+1 < self.h and self.help_list[i+1][j] == 0:
                if self.backtrack(i+1, j, k+1):
                    return True

            self.help_list[i][j] = 0
            return False

总结

上述代码中方法一代码更简洁,方向上利用数组复制,避免了方法二的代码冗余。

  • 在遇到类似方向相关的题是用数组改变方向灵活性强。
  • 回溯问题每一个子决策之间不互斥,只要有一个走通就可以返回True
  • 决策不成功则还原现场

LeetCode 51 N皇后

题目描述:

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

在这里插入图片描述

示例 1:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:

输入:n = 1
输出:[["Q"]]

分析

皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。假定斜线分别为撇和捺,位于撇线上的坐标相加等于一个固定值,位于捺线上的坐标相减等于一个固定值。如下图所示,该皇后的辐射范围为:

在这里插入图片描述

下一个皇后在放置前,先判断是否在其他皇后攻击范围之内。

路径:board 中⼩于当前行row的那些⾏都已经成功放置了皇后 
选择列表:当前行row的所有列都是放置皇后的选择 
结束条件:row 超过 board 的最后⼀⾏

代码解析

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        if n < 1: return []
        self.n = n

        # 方法一:
        # self.result = []
        # self.col = set()
        # self.pei = set()
        # self.na = set()
        # self.dfs1(n, 0, [])
        # return self._generate_result(n)

        # 方法二:
        self.result = []
        self._dfs2([], [], [])
        return [["." * i + "Q" + "." * (n - 1 - i) for i in col] for col in self.result]

    # 方法一:
    def dfs1(self, n, row, current_col):
        if row >= n:
            self.result.append(current_col)
            return
        for col in range(n):
            # 寻找合适位置
            if col in self.col or col + row in self.pei or row - col in self.na:
                continue

            # 加入条件
            self.col.add(col)
            self.pei.add(row + col)
            self.na.add(row - col)

            # 寻找下一行合适的位置,current_col 存放每一次合适的列元素索引
            self.dfs(n, row + 1, current_col + [col])

            # 如果下一层执行到col = n 则返回到此步,没有合适位置,执行后面一列
            self.col.remove(col)
            self.pei.remove(row + col)
            self.na.remove(row - col)

    def _generate_result(self, n):
        b = []
        for res in self.result:
            for i in res:
                b.append("." * i + "Q" + "." * (n - 1 - i))
        return [b[i: i + n] for i in range(0, len(b), n)]

    # 方法二: xy_diff=pei, xy_sum = na
    def _dfs2(self, queue, xy_diff, xy_sum):
        # 行,每一次从这里更新,如果上一个操作满足, 则行 + 1
        p = len(queue)
        if p == self.n:
            self.result.append(queue)
        # 列, queue
        for q in range(self.n):
            # queue 存放q(列)的值
            # 此时p = len(queue) 已经到下一行,从下一行的第一列开始计算 p-q 和 p+q. 
            if q not in queue and p - q not in xy_diff and p + q not in xy_sum:
                self._dfs2(queue + [q], xy_diff + [p - q], xy_sum + [p + q])

总结

方法二功底很深厚,学习学习

祝好!

更多文章,请关注公众号:编程牧马人

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值