算法之搜索专题

搜索

深度优先搜索(DFS)和广度优先搜索(BFS)都是常见的图搜索算法。它们的目的是访问和遍历图中的所有节点,并找到满足特定条件的节点。虽然这两种算法的实现方式不同,但它们都有以下的特点。

首先,DFS 和 BFS 都是无向图或者有向图的通用算法。其次,它们都需要使用一个队列来存放待处理的节点。在 DFS 中,该队列被称为堆栈,而在 BFS 中,该队列被称为队列。无论是哪一种方法,都会访问每个节点,并跟踪已经访问过的节点。

深度优先搜索

深度优先搜索是一种用于查找或遍历树或图的算法。该算法会沿着树的深度遍历树的分支,尽可能深的搜索子树。当节点所对应的分支都已经被访问过了,算法会回溯到之前的节点,继续搜索其他的分支。这样,DFS 会将图中的所有节点遍历一遍。具体实现时,可以使用递归或者栈来实现。

基本套路就是:

  1. 首先,将节点标记为已访问
  2. 然后,对于每个未访问的邻居,重复步骤 1
  3. 最后,回溯到之前的节点

总而言之,DFS 的核心思想是沿着某个路径一直走到底,直到不能再走为止,然后返回最近的一个未访问过的节点,并尝试其他的路径。

广度优先搜索

广度优先搜索是一种用于查找或遍历树或图的算法。该算法从起点开始遍历整张图,首先访问起点的所有邻居节点,然后访问邻居节点的邻居节点,以此类推。换句话说,该算法会先访问距离起点为 1 的所有节点,然后是距离起点为 2 的所有节点,以此类推。

基本套路就是:

  1. 创建一个队列 Q,将起点入队
  2. 将起点标记为已访问
  3. 重复以下步骤,直到队列为空:
    1. 弹出队首元素 v
    2. 对于 v 的所有未访问的邻居 w,将其标记为已访问并入队

核心思想就是按照距离从小到大的顺序访问所有节点,并将每个节点标记为已访问过,以确保不重不漏地访问整张图。

此外按照我们的搜索类型,我们还可以将搜索分为连通性搜索和状态搜索

连通性搜索

不需要用重新构造新的节点,在原有的节点的基础上进行搜索的情况,可以归纳为连通性搜索情况。

状态搜索

状态搜索,在这里是指,需要在搜索的过程当中,不断构造新的节点,改变现有状态的一种搜索。
在这一过程当中,存在指数级的状态空间,因此存在大量的优化,例如双向BFS搜索,或者启发式搜索。例如PSO,遗传算法等等,算法竞赛当中常见的有IDA* 和 A*

例题

1. 深度优先搜索(DFS)

1.1 Leetcode 22题 括号生成

链接:https://leetcode.cn/problems/generate-parentheses/

在这里插入图片描述

思路如下:

首先,构造用于存储搜索结果的列表 res。接下来,定义一个名为 dfs 的辅助函数,该函数接收三个参数:当前要搜索的字符串 s、左括号数量 left 和右括号数量 right。在函数中,我们需要进行以下操作:

1)如果 left 和 right 都等于 n,则将 s 加入结果集 res,并返回。

2)如果 left 小于 n,则将字符 ‘(’ 加入 s 中,执行递归调用 dfs(s+‘(’, left+1, right)。

3)如果 right 小于 left,则将字符 ‘)’ 加入 s 中,执行递归调用 dfs(s+‘)’, left, right+1)。

最终,我们将调用 dfs 函数,将空字符串和左右括号数量都初始化为 0。通过这种方式,我们可以生成所有由 n 对括号组成的有效括号字符串。

代码实现:

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        res = []
        def dfs(s, l, r):
            if l == n and r == n:
                res.append(s)
                return
            if l < n:
                dfs(s + "(", l + 1, r)
            if r < l:
                dfs(s + ")", l, r + 1)
        dfs("", 0, 0)
        return res

1.2 Leetcode 46题 全排列

链接:https://leetcode.cn/problems/permutations/
在这里插入图片描述

思路如下:

首先,构造用于存储搜索结果的列表 res。接下来,定义一个名为 dfs 的辅助函数,该函数接收两个参数:当前要搜索的序列 nums和一个用于存储已选数字的集合 visited。在函数中,我们需要进行以下操作:

1)如果 visited 中元素的数量等于 nums 中元素的数量,则将 nums 加入结果集 res,并返回。

2)对于 nums 中的每个元素 num,如果 num 不在 visited 中,则将其加入 visited 中,并执行递归调用
dfs(nums, visited),然后将 num 从 visited 中移除。

最终,我们将调用 dfs 函数,并将空列表和空集合作为初始参数。通过这种方式,我们可以生成输入序列的所有排列。

代码实现:

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []
        used = set()
        def dfs(path, used):
            if len(path) == len(nums):
                res.append(path[:])
                return
            for num in nums:
                if num not in used:
                    path.append(num)
                    used.add(num)
                    dfs(path, used)
                    path.pop()
                    used.remove(num)
        dfs(path, used)
        return res

1.3 Leetcode 77题 组合

链接:https://leetcode.cn/problems/combinations/
在这里插入图片描述

代码实现:

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        res = []
        path = []
        def dfs(start):
            if len(path) == k:
                res.append(path[:])
                return
            for i in range(start, n+1):
                path.append(i)
                dfs(i+1)
                path.pop()
        dfs(1)
        return res

2. 广度优先搜索(BFS)

2.1 Leetcode 756. 金字塔转换矩阵

链接:https://leetcode.cn/problems/pyramid-transition-matrix/description/
在这里插入图片描述
解题思路:

从底部的每个位置开始,依次向上层逐个扩展,每一步看能否找到两个砖块合并成一个新砖块。如果能够得到底部的最后一行,则说明可以合并出一个砖块,此时将结果加入答案数组。

由于第 i i i 行有 i + 1 i+1 i+1 个砖块,因此最底层共有 2 m 2^m 2m 个位置需要进行一次模拟。对于每个砖块,我们都需要遍历 O ( 1 ) O(1) O(1)
个候选应该放在上方的可能性(注意这个常数实际上比字母表长度还要小),因此总的时间复杂度为 O ( n ⋅ 2 m ) O(n \cdot 2^m) O(n2m)。算法的空间复杂度为 O ( 2 m ) O(2^m) O(2m),即生成的状态数。

具体实现中,我们使用了一个字典 a l l o w e d _ d i c t \mathtt{allowed\_dict} allowed_dict 来记录所有可以组成新砖块的候选组合,以及一个队列
q u e u e \mathtt{queue} queue 存储待处理的位置。初始时,将每个底部位置 ( i , j ) (i, j) (i,j) 存入队列中,同时将其作为位置为 i + 1 , j i+1,j i+1,j
i + 1 , j + 1 i+1,j+1 i+1,j+1 的砖块的候选组合加入 a l l o w e d _ d i c t \mathtt{allowed\_dict} allowed_dict 中。之后,每次从队列中取出一个位置 ( i , j ) (i, j) (i,j),并枚举其上方的两个位置 ( i − 1 , j ) (i-1,j) (i1,j) ( i − 1 , j + 1 ) (i-1,j+1) (i1,j+1)。如果可以通过
a l l o w e d _ d i c t \mathtt{allowed\_dict} allowed_dict 找到它们对应的组合 ( a , b ) (a,b) (a,b) 并将其合并为新砖块,则将 ( i − 1 , j ) (i-1,j) (i1,j)
存入队列中,并且在 a l l o w e d _ d i c t \mathtt{allowed\_dict} allowed_dict
中记录以新砖块为下方的可行组合。最后,如果队列为空时还没有找到可以合并的砖块,则返回空数组。

解题代码:


from collections import deque
class Solution(object):
    def pyramidTransition(self, bottom, allowed):
        """
        :type bottom: str
        :type allowed: List[str]
        :rtype: bool {ab:[c,d]}
        """
        maps = {
    
    }
        for all in allowed:
            if (maps.get(all[:2])):
                maps[all[:2]] += all[2]
            else:
                maps[all[:2]] = all[2]
        return self.bfs(bottom,maps)

    def bfs(self,bottom,maps):
        q =[]
        q.append(bottom)
        used = {
    
    }
        while(len(q)):
            t = q.pop(0)
            if(used.get(t)):
                continue
            if(len(t)==1):
                return True
            temp = []
            flag = True
            #由当前的小s扩展到新的小s
            for j in range(len(t)-1):
                b_k = t[j:j+2]
                b_t = maps.get(b_k)
                if(not b_t):
                    flag = False
                    break
                temp.append(b_t)
            #现在要组装小s
            if(flag):
                ns = []
                self.dfs(temp,0,[],ns)
                for s2 in ns:
                    q.insert(0,s2)
            used[t] = 1
        return False

    def dfs(self,temp,i,path,ns):

        if(i==len(temp)):
            ns.append("".join(path))
            return
        for c in temp[i]:
            path.append(c)
            self.dfs(temp,i+1,path,ns)
            path.pop()



2.2 Leetcode 127题 单词接龙

链接:https://leetcode.cn/problems/word-ladder/

在这里插入图片描述
解题思路:

首先,我们需要转换输入的单词列表为一个 set 类型的变量 word_set,以便后续快速查找某个单词是否在列表中。接下来,定义一个队列
queue 和一个集合 visited,将 beginWord 加入队列,并将其加入 visited 中。对于每个单词 node
在队列中,我们需要进行以下操作:

1)遍历该单词的所有邻居(即只相差一个字符的单词),如果邻居中存在 endWord,返回当前深度(即转换序列的长度)。

2)如果邻居不在 visited 中,则将其加入队列和 visited 中。

最终,如果未能找到最短转换序列,则返回 0。

代码实现:

class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        if endWord not in wordList or not beginWord or not endWord or not wordList:
            return 0
        wordSet = set(wordList)
        q = collections.deque()
        q.append((beginWord, 1))
        visited = set()
        visited.add(beginWord)
        while q:
            cur_word, level = q.popleft()
            if cur_word == endWord:
                return level
            for i in range(len(cur_word)):
                for j in range(26):
                    new_word = cur_word[:i] + chr(j+97) + cur_word[i+1:]
                    if new_word in wordSet and new_word not in visited:
                        q.append((new_word, level+1))
                        visited.add(new_word)
        return 0

2.3 Leetcode 126题 单词接龙 II(选做)

链接:https://leetcode.cn/problems/word-ladder-ii/
在这里插入图片描述

(可以使用双向队列进行搜索,但是这边选择了建图,然后使用spfa算法)

from collections import defaultdict
from typing import List

class Solution:
    def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
        # 将单词列表转化为集合,方便后续判断单词是否在列表中
        word_set = set(wordList)

        # 特判:如果终点不在列表中,则无法到达
        if endWord not in word_set:
            return []

        # 建立单词的通用状态字典
        # key 为通用状态,value 为所有具有该通用状态的单词
        generic_dict = defaultdict(list)
        for word in word_set:
            for i in range(len(word)):
                # 将单词的第 i 个字符替换为通配符 '*'
                generic_word = word[: i] + '*' + word[i + 1:]
                generic_dict[generic_word].append(word)
		"""
		{
		    '*ot': ['hot', 'dot', 'lot'],
		    'h*t': ['hot'],
		    'ho*': ['hot'],
		    'd*t': ['dot'],
		    'do*': ['dot', 'dog'],
		    'cog': ['cog', 'log']
		}
		"""


        # 记录每个单词的前驱节点
        prev_dict = {
    
    }
        # 记录起点到达每个单词的最短路径长度
        distance_dict = {
    
    }
        # 初始化所有单词的距离为正无穷
        for word in word_set:
            distance_dict[word] = float('inf')
        # 将起点加入队列,并初始化距离为 0
        queue = [(beginWord, 0)]
        distance_dict[beginWord] = 0

        # 广度优先搜索算法(spfa)
        while queue:
            cur_word, cur_depth = queue.pop(0)

            # 如果已经到达终点,则停止搜索
            if cur_word == endWord:
                break

            # 枚举当前单词的所有通用状态
            for i in range(len(cur_word)):
                # 将单词的第 i 个字符替换为通配符 '*'
                generic_word = cur_word[: i] + '*' + cur_word[i + 1: ]
                # 遍历所有具有该通用状态的单词
                for word in generic_dict[generic_word]:
                    # 如果与当前单词不同
                    if word != cur_word:
                        # 如果未访问过
                        if distance_dict[word] == float('inf'):
                            # 将其加入队列,并更新距离和前驱节点
                            queue.append((word, cur_depth + 1))
                            distance_dict[word] = cur_depth + 1
                            prev_dict[word] = [cur_word]
                        # 如果已经访问过,并且当前距离与之前记录的距离相等
                        elif distance_dict[word] == cur_depth + 1:
                            # 将当前单词作为新的前驱节点
                            prev_dict[word].append(cur_word)

        # 还原最短路径
        res = []
        path = []

        # 从终点开始还原路径
        def dfs(word: str):
            # 将当前单词加入路径中
            path.append(word)
            if word == beginWord:
                # 如果已经回溯到起点,则将路径反转,并加入结果集
                res.append(path[::-1])
            elif word in prev_dict:
                # 否则,遍历所有前驱节点
                for prev in prev_dict[word]:
                    dfs(prev)
            # 回溯时,将当前单词弹出路径
            path.pop()

        dfs(endWord)
        return res



猜你喜欢

转载自blog.csdn.net/FUTEROX/article/details/130039019
今日推荐