数据结构与算法之高级搜索(剪枝)

252 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天,点击查看活动详情

作者: 千石
支持:点赞、收藏、评论
欢迎各位在评论区交流

前言

本文内容来自我平时学习的一些积累,如有错误,还请指正

在题目实战部分,我将代码实现和代码解释设置在了解题思路的下方,方便各位作为参考刷题

一些话

本文内容来自我平时学习的一些积累,如有错误,还请指正

在题目实战部分,我将代码实现和代码解释设置在了解题思路的下方,方便各位作为参考刷题

题目练习步骤:

  1. 给自己10分钟,读题并思考解题思路
  2. 有了思路以后开始写代码,如果在上一步骤中没有思路则停止思考并且看该题题解
  3. 在看懂题解(暂时没看懂也没关系)的思路后,背诵默写题解,直至能熟练写出来
  4. 隔一段时间,再次尝试写这道题目

前置知识

剪枝

剪枝是一种常用的算法优化技术,其本质是减少搜索空间,避免无效搜索,从而提高算法效率。在数据结构与算法中,剪枝常常用于搜索、动态规划等算法中。

剪枝的实现方式有很多种,以下是一些常用的剪枝技术:

  • 回溯剪枝

回溯算法通常采用深度优先搜索策略,枚举所有可能的解,然后进行剪枝,排除一些不符合条件的解。回溯剪枝是一种剪枝技术,通过记录已经搜索过的状态,避免重复搜索,从而减少搜索次数。回溯剪枝常常用于求解排列、组合等问题。

  • 动态规划剪枝

动态规划剪枝通常用于解决最优化问题。该技术通常采用备忘录或者状态转移方程等方式,记录已经计算过的结果,避免重复计算,从而提高算法效率。动态规划剪枝通常用于求解最短路径、最大连续子序列和等问题。

  • 前缀和剪枝

前缀和剪枝通常用于解决区间和问题。该技术通过预处理前缀和数组,快速计算任意区间的和,避免重复计算,从而提高算法效率。前缀和剪枝常常用于求解子数组和等问题。

  • 启发式剪枝

启发式剪枝通常采用一些启发式规则,根据问题的特点,对搜索空间进行剪枝。启发式剪枝的关键是设计好剪枝规则,通常需要对问题进行深入理解和分析。启发式剪枝常常用于求解最优化问题,如TSP(旅行商问题)等。

  • 剪枝的特性:

    1. 提高算法效率:剪枝可以减少搜索空间,避免无效搜索,从而提高算法效率。
    2. 需要合适的剪枝策略:剪枝的效果取决于剪枝策略的合理性和优化程度,需要根据具体问题选择适合的剪枝策略。
    3. 可能会导致漏解:某些剪枝策略可能会排除一些合法解,需要进行验证和修正。
    4. 与算法结合使用:剪枝通常需要与其他算法结合使用,如回溯、动态规划、分治等。

实战

题目一:51. N 皇后

image.png

解题思路

这是一道经典的搜索问题,可以采用回溯算法来求解,思路如下:

  1. 初始化一个空的棋盘,大小为N*N,其中所有的位置都为0(表示没有皇后)。
  2. 从第一行开始,逐行搜索。对于每一行,从第一列开始,逐列尝试放置皇后。如果该位置可以放置皇后(即该位置的同一行、同一列、同一对角线上都没有其他皇后),则将该位置标记为1,表示放置了皇后。
  3. 如果所有的皇后都已经放置完毕,将当前棋盘保存到结果中,并回溯到上一行,尝试该行的下一个位置。
  4. 如果当前行的所有位置都已经尝试完毕,回溯到上一行,将该行当前位置标记为0(表示没有皇后),继续尝试该行的下一个位置。
  5. 如果所有的行都已经尝试完毕,算法结束,返回所有合法的解。

Tips:在每一行中,我们只能放置一个皇后,因此每个皇后的位置是互斥的。此外,每个皇后所在的行、列、主对角线和副对角线上都不能有其他皇后,因此需要使用列剪枝和斜线剪枝来排除不合法的解。

在回溯算法的基础上,我们可以通过剪枝来优化算法效率。具体来说,我们可以通过以下两种剪枝方式来优化算法:

  1. 列剪枝:在搜索的过程中,我们可以维护一个列表,记录每一列是否已经有皇后占用。当我们在搜索的某一行尝试放置皇后时,只需要考虑未被占用的列,即可避免重复搜索。
  2. 斜线剪枝:在搜索的过程中,我们可以维护两个集合,分别记录已经被占用的主对角线和副对角线。当我们在搜索的某一行尝试放置皇后时,只需要考虑未被占用的主对角线和副对角线,即可避免重复搜索。

复杂度分析

时间复杂度:在最坏情况下,N皇后问题的时间复杂度是指数级别的,即O(N!)O(N!)。但是通过列剪枝和斜线剪枝,可以大大减少搜索空间,提高算法效率。具体来说,列剪枝可以将搜索空间减少到O(NN)O(N^N),而斜线剪枝可以将搜索空间减少到O(2N)O(2N)。因此,综合考虑,N皇后问题的时间复杂度可以近似为O(NN)O(N^N)

代码实现

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        self.res = []
        self.cols = [False] * n # 记录每一列是否已经有皇后占用
        self.diag1 = [False] * (2 * n - 1) # 记录主对角线是否已经有皇后占用
        self.diag2 = [False] * (2 * n - 1) # 记录副对角线是否已经有皇后占用
        self.helper([], n, 0)
        return self.res
    
    def helper(self, cur, n, row):
        if row == n:
            self.res.append(self.format_res(cur))
            return
        for col in range(n):
            if self.valid_pos(row, col, n):
                self.cols[col] = True
                self.diag1[row + col] = True
                self.diag2[row - col + n - 1] = True
                cur.append(col)
                self.helper(cur, n, row + 1)
                cur.pop()
                self.cols[col] = False
                self.diag1[row + col] = False
                self.diag2[row - col + n - 1] = False
    
    def valid_pos(self, row, col, n):
           return not self.cols[col] and not self.diag1[row + col] and not self.diag2[row - col + n - 1]

    def format_res(self, cur):
        res = []
        for col in cur:
            row_str = '.' * col + 'Q' + '.' * (len(cur) - col - 1)
            res.append(row_str)
        return res

结果展示

image.png