LeetCode-51.N皇后,Python的回溯法实现及详细讲解

51. N皇后

题目:

在这里插入图片描述
在这里插入图片描述


1. 思路

关于N皇后问题。它也是一个回溯问题。为何?因为它也符合类似”深度优先搜索“的树形思路。
例如,以4皇后为例,4*4的棋盘,假设我们从第一行开始算起,我们会发现,如果你选择第一行第一列(0, 0),那么一次为出发点,你可以去尝试接下来第二行的所有位置(1, 0) ~ (1, 3);假设你第二行选择了合适的放置位置(1, 2),那么你可以接着以[(0,0), (1,2)]为出发点考虑第三行所有位置是否放置皇后合适。
显然,每个合适的皇后放置点,都是一个树形节点的开始,如下图所示:

0,00,10,20,3
1,01,11,21,3
2,02,12,22,3
3,03,13,23,3
                                            “👆 我是棋盘。。。”
在这里插入图片描述

如图所示,这显然是个可以用递归处理的回溯思想/深度优先搜索思想解决的问题。所以提到回溯,我们的大框大概就架好了:

  • 主函数用于开启递归、准备必要变量
  • 递归函数,处理相同的递归步骤
  • 若是有条件的递归,则再定义一个函数,判断是否可以进行递归
所以结合本题,思路大致梳理一下:
递归函数:
  1. 从第一行开始,对所有位置(即col)进行for循环,判断此处是否可放Queen
    1.1 若可以放Queen,那么记录好当前Queen位置,进行下一行的递归判断(row+=1)
    1.2 若不能放Queen,不进入递归
  2. 最后,不论是否可进入下一层递归,都要将col++.(原因:假设4皇后问题,前两行结果为[0, 2],那么容易发现,第三行不论是哪个位置,都不能放置Queen,那么col++的循环会跳出,发生回溯,回到row没有步进的时候,将row=1那行的col=2进行步进,将前两行结果更新为[0, 3]。这就是即使is_valid()成立,col仍要步进的情况)
  3. 当row == n(n是棋盘size)时,说明已经到了最后一行,完成了全部Queen的摆放,则要将当前的结果予以记录
主函数/入口函数:
  1. 创建成员变量self.res=[], 用于存储递归过程中得到的结果。
  2. 初始化一个列表tmp = [], 作用为在每一次递归过程中(即每一次深度优先搜索中,或者更具体地说,在树形思维中每一次走到n叉树的叶节点、走到无路可走or有答案的情况下),用于记录本次递归过程的结果。当然,根据上面递归函数所述,只有走到row==n时,才会被认为是一次正确放置N皇后的答案,才会将tmp记录到self.res里去。
递归条件判断函数:
  1. 在知道当前所在的(row, col)棋盘位置的情况下,在已知此前rows的Queen摆放信息(tmp记录)的情况下,判断当前位置(row, col)是否可放置本行的Queen

2. 代码

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:# 主函数,准备递归物料、递归函数入口
        self.res = []   # 答案记录器
        self.n = n      # 方便会用到的子函数
        tmp = [None for _ in range(self.n)]   # 递归复用数组,用一维数组记录Queen位置,数组的index就是row,index对应的元素就是col
        self.backtrace(tmp, row=0)
        return self.return_result()
        
    def backtrace(self, tmp, row):
        if row == self.n:  # 如果已经判断完全部的行了,就返回并记录吧
            self.res.append(tmp[:])   # 这里必须是tmp的切片,因为python切片是浅拷贝,浅拷贝的结果不会在后续递归中被修改掉
            return         # 这里必须有一个return,因为当row == self.n成立时,这条递归之路即该完美结束
        col = 0   # 每一行的循环判断,都从0位置开始
        while col < self.n:
            if self.is_valid(tmp, row, col):  # 判断当前位置是否可放Queen
                tmp[row] = col             # 若可以,则记录于tmp
                self.backtrace(tmp, row + 1)   # 然后开启下一行的递归判断(row+1)
            col += 1      # 不论是否成立,都应将col的位置步进(col+1)
        return  # 这里必须有一个return,因为当while循环跳出,说明本行所有col都不能放Queen,应发生回溯,目前的递归之路应提前结束
    
    def is_valid(self, tmp, row, col):
        for i, j in enumerate(tmp):
            # 在判断Queen是否可放置的时候,只有当前行之前的Queen值得被考虑。由于tmp是复用的,所以当前row及其之后的row都有可能已有无用记录
            if i == row:
                break
            if j == col or row - i == abs(col - j):  # 两个不可放Queen条件:1.在同列;2.在**左、右**两侧对角线。
                return False
        return True   # 全部经历过考验,则可以放置
    
    def return_result(self):   # 将结果转换成LeetCode要求的样子
        saver = []
        for arr in self.res:
            solve = []
            for pos in arr:
                line = ["."] * self.n
                line[pos] = "Q"
                solve.append(''.join(line))
            saver.append(solve)
        return saver

代码如上,思路很清晰,比我原本的分析只多了个LeetCode所需答案的生成函数 return_result(),但有些问题还是指的讨论的:

递归中 tmp 数组的复用

这也是我之前被卡壳的地方——那就是,在递归过程中,如何在每一次递归过程中都生成一个空list存放此次递归计算出的结果?或者说,code中所述的tmp数组,如何在每一次递归过程中复用?说实话,我觉得这应该算递归的普遍性问题,所以值得思考一下。
对于回溯问题,这种树形思维下的递归问题,最好的方法肯定是数组复用,因为树形的行走方式,注定数组一般而言是可以复用的。那么Python实现里如何复用,以及这么写为何可以复用就是个针对回溯的coding问题。
首先,若想复用数组,就当然是从递归入口处传入复用的数组。对于本题而言,数组长度就是棋盘size n。如code line-5,我们便建立了这样一个数组。因为在回溯的树形思维中,任何一条递归路走不通或走到底有结果的时候,我们都应将之终止,终止后会回到发生递归的地方(code line-17),row相对于之前的递归回退一步。而tmp由于是复用的,所以会从row对应的index处开始重新更新,所以递归函数是这样复用数组的。
其次,对于结果的记录,如code line-11,我记录的是tmp的切片。因为若仅记录tmp,那么在后面复用的过程中,tmp一被改变,self.res中记录的tmp也会变化。而Python切片是浅拷贝,而self.res中没有可变对象,所以就隔离了后序递归中对tmp的修改。


最后,我们来聊聊递归

干货

  • 当题干里出现”xxx所有组合“、”xxx所有解“的时候,我们应该想到回溯;
  • 当你通过题干分析,分析出类似”深度优先搜索“的树形思路时,我们应该想到回溯

何谓“类似深度优先搜索的树形思路”?

以第LeetCode-17题为例,该题让写出2-9数字对应的字母所有组合。
随便举个例子——2,3,4。对于2,3,4,这三个数字各自分别对应3个字母,假设从数字2对应的a出发,到数字3,它又会有3条选择(d,e,f),任选一个,到了4又是三个选择,情况如下:

在这里插入图片描述

解决回溯问题的code捷径——递归!

回溯问题的通用代码框架

class Solution:
   def lcb(self, digits: str):
       self.res = []
       self.backtrace()
       return self.res
   
   def backtrace(self, ):
       if self.valid():
           return xxx or self.res.append(xxx)
       else:
           for i in arr:
               self.bc(i-1)
   def valid():
       return True

最后,附赠LeetCode-17的答案

class Miner:
    def lcb(self, digits: str):
        if not digits: 
            return []
        phone = {'2':['a','b','c'],
                 '3':['d','e','f'],
                 '4':['g','h','i'],
                 '5':['j','k','l'],
                 '6':['m','n','o'],
                 '7':['p','q','r','s'],
                 '8':['t','u','v'],
                 '9':['w','x','y','z']}
        str_list = [phone[d] for d in digits]
        self.res = []
        self.bc('', str_list)
        return self.res
        
    def bc(self, prefix, str_list):
        if not str_list:
            self.res.append(prefix)
        else:
            for j in str_list[0]:
                self.bc(prefix + j, str_list[1:])
  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法。 LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_illusion_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值