搜索:BFS、DFS

tech2023-10-14  97

数据结构:

广度优先搜索遍历BFS——队列深度优先搜索遍历DFS——栈

连通分量的个数

200.岛屿数量【中等】

LeetCode传送门 给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。 岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。 此外,你可以假设该网格的四条边均被水包围。

示例:

输入: [ [‘1’,‘1’,‘0’,‘0’,‘0’], [‘1’,‘1’,‘0’,‘0’,‘0’], [‘0’,‘0’,‘1’,‘0’,‘0’], [‘0’,‘0’,‘0’,‘1’,‘1’] ]输出: 3

思路1:dfs

将二维网格看成一个无向图,竖直或水平相邻的 1 之间有边相连。为了求出岛屿的数量,可以扫描整个二维网格。如果一个位置为 1,则以其为起始节点开始进行深度优先搜索。在深度优先搜索的过程中,每个搜索到的 1 都会被重新标记为 0(也可令建一个数组mark,用来标记元素是否访问)。最终岛屿的数量就是进行深度优先搜索的次数。

时间复杂度:O(MN),其中 M 和 N 分别为行数和列数。 空间复杂度:O(MN),在最坏情况下,整个网格均为陆地,深度优先搜索的深度达到 MN。

class Solution: def dfs(self, grid, r, c): grid[r][c] = "0" nr, nc = len(grid), len(grid[0]) for x, y in [(r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)]: if 0 <= x < nr and 0 <= y < nc and grid[x][y] == "1": self.dfs(grid, x, y) def numIslands(self, grid: List[List[str]]) -> int: nr = len(grid) if nr == 0:return 0 nc=len(grid[0]) num_islands = 0 for r in range(nr): for c in range(nc): if grid[r][c] == "1": num_islands += 1 self.dfs(grid, r, c) return num_islands class Solution: def numIslands(self, grid: List[List[str]]) -> int: def DFS(grid,mark,x,y): mark[x][y]=1 dx=[-1,1,0,0] dy=[0,0,-1,1] for i in range(4): newx=x+dx[i] newy=y+dy[i] if newx<0 or newx>=len(grid) or newy<0 or newy>=len(grid[0]): continue if mark[newx][newy]==0 and grid[newx][newy]=='1': DFS(grid,mark,newx,newy) island_num=0 mark=[[0]*len(grid[0]) for _ in range(len(grid))] for i in range(len(grid)): for j in range(len(grid[i])): if mark[i][j]==0 and grid[i][j]=='1': DFS(grid,mark,i,j) island_num+=1 return island_num

思路2:bfs

为了求出岛屿的数量,可以扫描整个二维网格。如果一个位置为 1,则将其加入队列,开始进行广度优先搜索。在广度优先搜索的过程中,每个搜索到的 1 都会被重新标记为 0。直到队列为空,搜索结束。最终岛屿的数量就是进行广度优先搜索的次数。

时间复杂度:O(MN),其中 M 和 N 分别为行数和列数。 空间复杂度:O(min(M,N)),在最坏情况下,整个网格均为陆地,队列的大小可以达到min(M,N)。

class Solution: def bfs(self, grid, row, col): grid[row][col] = "0" neighbors = collections.deque([(row, col)]) while neighbors: r,c=neighbors.popleft() for x, y in [(r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)]: if 0 <= x <len(grid) and 0 <= y <len(grid[0]) and grid[x][y] == "1": neighbors.append((x,y)) grid[x][y]="0" def numIslands(self, grid: List[List[str]]) -> int: nr = len(grid) if nr == 0:return 0 nc=len(grid[0]) num_islands = 0 for r in range(nr): for c in range(nc): if grid[r][c] == "1": num_islands += 1 self.bfs(grid, r, c) return num_islands class Solution: def numIslands(self, grid: List[List[str]]) -> int: nr = len(grid) if nr == 0:return 0 nc = len(grid[0]) num_islands = 0 for r in range(nr): for c in range(nc): if grid[r][c] == "1": num_islands += 1 grid[r][c] = "0" neighbors = collections.deque([(r, c)]) while neighbors: row, col = neighbors.popleft() for x, y in [(row - 1, col), (row + 1, col), (row, col - 1), (row, col + 1)]: if 0 <= x < nr and 0 <= y < nc and grid[x][y] == "1": neighbors.append((x, y)) grid[x][y] = "0" return num_islands

547.朋友圈【中等】

LeetCode传送门 班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。 给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

思路:dfs

class Solution: def findCircleNum(self, M: List[List[int]]) -> int: n = len(M) count = 0 visited = set() def dfs(i): for j in range(n): if M[i][j] and j not in visited: visited.add(j) dfs(j) for i in range(n): if i not in visited: count += 1 visited.add(i) dfs(i) return count

1319.连通网络的操作次数【中等】

LeetCode传送门 用以太网线缆将 n 台计算机连接成一个网络,计算机的编号从 0 到 n-1。线缆用 connections 表示,其中connections[i] = [a, b] 连接了计算机 a 和 b。 网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。 给你这个计算机网络的初始布线 connections,你可以拔开任意两台直连计算机之间的线缆,并用它连接一对未直连的计算机。请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 -1 。

思路

当计算机的数量为 n 时,我们至少需要 n - 1 根线缆才能将它们进行连接。因此如果数组 connections 的长度小于 n - 1,我们可以直接返回 -1;而当数组 connections 的长度大于等于 n - 1 时,我们一定可以找到一种修改线缆的方式。将这 n 台计算机看成 n 个节点,每一条线缆看成一条边,那么计算机和线缆就组成了一个无向图,假设这个无向图中有 k 个连通分量,如果我们的操作是「添加一根线缆」而不是「移动一根线缆」,那么只需要添加 k - 1 根线缆就可以将所有计算机进行连接了。考虑到「移动一根线缆」的操作一定不会优于「添加一根线缆」,那么至少需要移动 k - 1 根线缆,才有可能将所有计算机进行连接。移动电缆的方法:如果一个节点数为 m 的连通分量中有超过 m - 1 条边,就一定可以找到一条多余的边,且移除这条边之后,连通性保持不变。我们就可以用这条边来将连接两个连通分量,使得图中连通分量的个数减少 1。 class Solution: def makeConnected(self, n: int, connections: List[List[int]]) -> int: if len(connections)<n-1: return -1 edges={x:[] for x in range(n)} for c0,c1 in connections: edges[c0].append(c1) edges[c1].append(c0) used=set() def dfs(u): used.add(u) for v in edges[u]: if v not in used: dfs(v) k=0 for i in range(n): if i not in used: k+=1 dfs(i) return k-1

连通分量的大小

695.岛屿的最大面积【中等】

LeetCode传送门 给定一个包含了一些 0 和 1 的非空二维数组 grid 。 一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。 找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)

思路:dfs

class Solution: def maxAreaOfIsland(self, grid: List[List[int]]) -> int: m,n=len(grid),len(grid[0]) def dfs(i,j): if 0<=i<m and 0<=j<n and grid[i][j]: grid[i][j]=0 return 1+dfs(i+1,j)+dfs(i-1,j)+dfs(i,j+1)+dfs(i,j-1) return 0 ans=0 for x in range(m): for y in range(n): ans=max(ans,dfs(x,y)) return ans class Solution: def dfs(self,grid,x,y): m,n=len(grid),len(grid[0]) if x<0 or y<0 or x==m or y==n or grid[x][y]==0: return 0 grid[x][y]=0 res=1 for dx,dy in [[0,1],[0,-1],[1,0],[-1,0]]: newx,newy=x+dx,y+dy res+=self.dfs(grid,newx,newy) return res def maxAreaOfIsland(self, grid: List[List[int]]) -> int: ans=0 for i,l in enumerate(grid): for j,n in enumerate(l): ans=max(self.dfs(grid,i,j),ans) return ans

思路:bfs

class Solution: def maxAreaOfIsland(self, grid: List[List[int]]) -> int: ans = 0 for i, l in enumerate(grid): for j, n in enumerate(l): cur = 0 q = collections.deque([(i, j)]) while q: cur_i, cur_j = q.popleft() if cur_i < 0 or cur_j < 0 or cur_i == len(grid) or cur_j == len(grid[0]) or grid[cur_i][cur_j] == 0: continue cur += 1 grid[cur_i][cur_j] = 0 for di, dj in [[0, 1], [0, -1], [1, 0], [-1, 0]]: next_i, next_j = cur_i + di, cur_j + dj q.append((next_i, next_j)) ans = max(ans, cur) return ans

827.最大人工岛【困难】

LeetCode传送门 在二维地图上, 0代表海洋, 1代表陆地,我们最多只能将一格 0 海洋变成 1变成陆地。 进行填海之后,地图上最大的岛屿面积是多少?(上、下、左、右四个方向相连的 1 可形成岛屿)

思路

改变后的面积=1+相邻连通分量(不重复)的面积。对于每个连通块,将所有格点赋值为 index (grid[r][c]=index),并记录他们的大小 area[index] = dfs(…)。然后对于每个 0,查看周围的邻居编号并放入集合 seen,将这个区域的大小加入结果,这就是当前节点的面积大小,在其中找到最大的。为了解决没有 0 的情况,我们只需要记录之前计算的最大面积并输出即可。 class Solution: def largestIsland(self, grid: List[List[int]]) -> int: n=len(grid) def neighbors(r,c): for nr,nc in [[r-1,c],[r+1,c],[r,c-1],[r,c+1]]: if 0<=nr<n and 0<=nc<n: yield nr,nc def dfs(r,c,index): ans=1 grid[r][c]=index for nr,nc in neighbors(r,c): if grid[nr][nc]==1: ans+=dfs(nr,nc,index) return ans area={} index=2 for r in range(n): for c in range(n): if grid[r][c]==1: area[index]=dfs(r,c,index) index+=1 ans=max(area.values() or [0]) for r in range(n): for c in range(n): if grid[r][c]==0: seen={grid[nr][nc] for nr,nc in neighbors(r,c) if grid[nr][nc]>1} ans=max(ans,1+sum(area[i] for i in seen)) return ans

463.岛屿的周长【简单】

LeetCode传送门 给定一个包含 0 和 1 的二维网格地图,其中 1 表示陆地 0 表示水域。 网格中的格子水平和垂直方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。 岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。

思路

从左至右、从上至下遍历如果当前值为1,则周长加4,如果左边有1,减2(两条边重合不计做边长),如果上边有1,减2。 class Solution: def islandPerimeter(self, grid: List[List[int]]) -> int: ans=0 for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j]==1: ans+=4 if i-1>=0 and grid[i-1][j]==1: ans-=2 if j-1>=0 and grid[i][j-1]==1: ans-=2 return ans

无权图最短路径——广度优先搜索

127.单词接龙【中等】

LeetCode传送门

最短路径长度

给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:

每次转换只能改变一个字母。转换过程中的中间单词必须是字典中的单词。

说明:

如果不存在这样的转换序列,返回 0。所有单词具有相同的长度。所有单词只由小写字母组成。字典中不存在重复的单词。你可以假设 beginWord 和 endWord 是非空的,且二者不相同。

示例:

输入: beginWord = “hit”, endWord = “cog”, wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]

输出:5

解释:一个最短转换序列是 “hit” -> “hot” -> “dot” -> “dog” -> “cog”,返回它的长度 5。

思路:bfs

对给定的 wordList 做预处理,找出所有的通用状态。将通用状态记录在字典中,键是通用状态,值是所有具有通用状态的单词。将包含 beginWord 和 1 的元组放入队列中,1 代表节点的层次。我们需要返回 endWord 的层次也就是从beginWord 出发的最短距离。为了防止出现环,使用访问数组记录。当队列中有元素的时候,取出第一个元素,记为 current_word。找到 current_word 的所有通用状态,并检查这些通用状态是否存在其它单词的映射,这一步通过检查 all_combo_dict 来实现。从 all_combo_dict 获得的所有单词,都和 current_word 共有一个通用状态,所以都和 current_word 相连,因此将他们加入到队列中。对于新获得的所有单词,向队列中加入元素 (word, level + 1) 其中 level 是 current_word 的层次。最终当你到达期望的单词,对应的层次就是最短变换序列的长度。 from collections import defaultdict class Solution(object): def ladderLength(self, beginWord, endWord, wordList): if endWord not in wordList or not endWord or not beginWord or not wordList: return 0 n = len(beginWord) all_combo_dict = defaultdict(list) for word in wordList: for i in range(n): all_combo_dict[word[:i] + "*" + word[i+1:]].append(word) queue = [(beginWord, 1)] visited = set(beginWord) while queue: current_word, level = queue.pop(0) for i in range(n): intermediate_word = current_word[:i] + "*" + current_word[i+1:] for word in all_combo_dict[intermediate_word]: if word == endWord: return level + 1 if word not in visited: visited.add(word) queue.append((word, level + 1)) # all_combo_dict[intermediate_word] = [] return 0

定义函数判断是否相连:超时

class Solution: def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: def connect(word1,word2): cnt=0 for i in range(len(word1)): if word1[i]!=word2[i]: cnt+=1 if cnt>1: return False break return cnt==1 def construct_graph(beginWord,wordList): graph={} wordList.append(beginWord) for i in range(len(wordList)): graph[wordList[i]]=[] for i in range(len(wordList)): for j in range(i+1,len(wordList)): if connect(wordList[i],wordList[j]): graph[wordList[i]].append(wordList[j]) graph[wordList[j]].append(wordList[i]) return graph def BFS_graph(beginWord,endWord,graph): Q=[] visit=set() Q.append([beginWord,1]) while Q: node,step=Q.pop(0) visit.add(node) if node==endWord: return step neighbor=graph[node] for vertex in neighbor: if vertex not in visit: Q.append([vertex,step+1]) visit.add(vertex) return 0 graph=construct_graph(beginWord,wordList) return BFS_graph(beginWord,endWord,graph)

126.单词接龙2【困难】

LeetCode传送门

最短路径序列

给定两个单词(beginWord 和 endWord)和一个字典 wordList,找出所有从 beginWord 到 endWord 的最短转换序列。转换需遵循如下规则:

每次转换只能改变一个字母。转换后得到的单词必须是字典中的单词。

说明:

如果不存在这样的转换序列,返回一个空列表。所有单词具有相同的长度。所有单词只由小写字母组成。字典中不存在重复的单词。你可以假设 beginWord 和 endWord 是非空的,且二者不相同。

示例:

输入: beginWord = “hit”, endWord = “cog”, wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]输出: [ [“hit”,“hot”,“dot”,“dog”,“cog”], [“hit”,“hot”,“lot”,“log”,“cog”] ]

思路:bfs

需要记录最短路径序列;要返回所有的最短路径序列,不能找到最短路径就返回。 class Solution: def findLadders(self, beginWord, endWord, wordList): n = len(beginWord) dic = collections.defaultdict(list) for w in wordList: for i in range(n): dic[w[:i] + '*' + w[i+1:]].append(w) q, s = collections.deque([(beginWord, [beginWord])]), collections.deque() seen, res = set(), [] while q: # q为当前层所有结点,s为下一层所有结点 # 可以得到所有的最短路径 while q: w, path = q.popleft() if w == endWord: res.append(path) seen.add(w) for i in range(n): for v in dic[w[:i] + '*' + w[i+1:]]: if v not in seen: s.append((v, path + [v])) if res: return res q, s = s, q return []

最长路径

矩阵中的最长递增路径【困难】

LeetCode传送门 给定一个整数矩阵,找出最长递增路径的长度。 对于每个单元格,你可以往上,下,左,右四个方向移动。 你不能在对角线方向上移动或移动到边界外(即不允许环绕)。

思路:记忆化dfs

朴素深度优先搜索的时间复杂度过高的原因是进行了大量的重复计算,同一个单元格会被访问多次,每次访问都要重新计算。由于同一个单元格对应的最长递增路径的长度是固定不变的,因此可以使用记忆化的方法进行优化。用矩阵 memo 作为缓存矩阵,已经计算过的单元格的结果存储到缓存矩阵中。使用记忆化深度优先搜索,当访问到一个单元格 (i,j) 时,如果 m e m o [ i ] [ j ] ≠ 0 memo[i][j]\neq0 memo[i][j]=0,说明该单元格的结果已经计算过,则直接从缓存中读取结果,如果 m e m o [ i ] [ j ] = 0 memo[i][j]=0 memo[i][j]=0,说明该单元格的结果尚未被计算过,则进行搜索,并将计算得到的结果存入缓存中。遍历完矩阵中的所有单元格之后,即可得到矩阵中的最长递增路径的长度。 class Solution: def longestIncreasingPath(self, matrix: List[List[int]]) -> int: DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)] if not matrix:return 0 @lru_cache(None) def dfs(row,column): best=1 for dx,dy in DIRS: newRow,newColumn=row+dx,column+dy if 0<=newRow<rows and 0<=newColumn<columns and matrix[newRow][newColumn]>matrix[row][column]: best=max(best,dfs(newRow,newColumn)+1) return best ans=0 rows,columns=len(matrix),len(matrix[0]) for i in range(rows): for j in range(columns): ans=max(ans,dfs(i,j)) return ans

路径序列

797.所有可能的路径【中等】

LeetCode传送门 给一个有 n 个结点的有向无环图,找到所有从 0 到 n-1 的路径并输出(不要求按顺序) 二维数组的第 i 个数组中的单元都表示有向图中 i 号结点所能到达的下一些结点(译者注:有向图是有方向的,即规定了a→b你就不能从b→a)空就是没有下一个结点了。

思路:递归

题目中给出的图是有向无环的,那么我们可以通过深度优先搜索的方法,递归求解出所有的路径。设图中有 N 个节点,在搜索时,如果我们到达了节点 N - 1,那么此时的路径就为 {N - 1};否则如果我们到达了其它的节点 node,那么路径就为 {node} 加上 {所有从 nei 到 N - 1} 的路径集合,其中 nei 是 node 直接相邻的节点。 class Solution: def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]: n=len(graph) def solve(node): if node==n-1:return [[n-1]] ans=[] for nei in graph[node]: for path in solve(nei): ans.append([node]+path) return ans return solve(0)

LeetCode题解

最新回复(0)