算法-图论-深搜/广搜

写在前面的小结

本文主要记录了使用bfs, dfs解决图上的问题。

这几题中图的类型主要有两类:

  1. 岛屿类问题:需要设置directions,对上下左右进行遍历。
  2. 普通图,用 邻接表、邻接矩阵 记录节点之间的联系。

几个注意点:

  • 深搜、广搜 通常需要使用 visited矩阵,防止对节点的重复遍历。
  • 不同的问题需要在遍历过程中记录不同的数值,例如:面积、周长等

1. 岛屿数量 (卡码网 99)

给定一个由 1(陆地)和 0(水)组成的矩阵,你需要计算岛屿的数量。
岛屿由水平方向或垂直方向上相邻的陆地连接而成,并且四周都是水域。你可以假设矩阵外均被水包围。

from collections import deque
# 上下左右
directions = [[0,1], [0,-1], [-1,0], [1,0]]

# 深搜
def dfs(graph, visited, x, y):
    visited[x][y] = True
    for i in range(4):
        next_x = x + directions[i][0]
        next_y = y + directions[i][1]
        if next_x < 0 or next_x >= len(graph) or next_y < 0 or next_y >= len(graph[0]):
            continue
        if visited[next_x][next_y] == True or graph[next_x][next_y] == 0:
            continue
        dfs(graph, visited, next_x, next_y)

# 广搜
def bfs(graph, visited, x, y):
    que = deque([])
    que.append([x, y])
    visited[x][y] = True
    while que:
        cur_x, cur_y =  que.popleft()
        for i in range(4):
            next_x = cur_x + directions[i][0]
            next_y = cur_y + directions[i][1]
            if next_x < 0 or next_x >= len(graph) or next_y < 0 or next_y >= len(graph[0]):
                continue
            if visited[next_x][next_y] == True or graph[next_x][next_y] == 0:
                continue
            visited[next_x][next_y] = True
            que.append([next_x, next_y])

def main():
    row_num, col_num = map(int, input().split())
    graph = []
    visited = [[False for _ in range(col_num)] for _ in range(row_num)]
    for i in range(row_num):
        graph.append(list(map(int, input().split())))
    
    result = 0
    for i in range(row_num):
        for j in range(col_num):
            if graph[i][j] == 1  and visited[i][j] == False:
                result += 1
                bfs(graph, visited, i, j)
    
    print(result)


if __name__ == "__main__":
    main()

2. 孤岛的面积(卡码网 101)

给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被水域单元格包围。
孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。
现在你需要计算所有孤岛的总面积,岛屿面积的计算方式为组成岛屿的陆地的总数。

思路

  • 先将位于边界上的岛屿,访问一遍visited[x][y] = True
  • 再将result清零,统计所有孤岛的面积。
# 广搜做法
from collections import deque

directions = [[0,1], [0,-1], [-1,0], [1,0]]
result = 0

def bfs(graph, visited, x, y):
    # 声明 result 是一个全局变量
    global result
    deq = deque([])
    visited[x][y] = True
    result += 1
    deq.append([x, y])
    while deq:
        cur_x, cur_y = deq.popleft()
        for i in range(4):
            next_x = cur_x + directions[i][0]
            next_y = cur_y + directions[i][1]
            if next_x < 0 or next_x >= len(graph) or next_y < 0 or next_y >= len(graph[0]):
                continue
            if graph[next_x][next_y] == 0 or visited[next_x][next_y]:
                continue
            visited[next_x][next_y] = True
            result += 1
            deq.append([next_x, next_y])
    

def main():
    num_row, num_col = map(int, input().split())
    graph = []
    visited = [[False for _ in range(num_col)] for _ in range(num_row)]
    for _ in range(num_row):
        graph.append(list(map(int, input().split())))
        
    
    # 沿着四条边界,将处在边界上的岛屿的visited都标记为True
    for j in range(num_col):
        if graph[0][j] == 1 and visited[0][j] == False:
            bfs(graph, visited, 0, j)
        if graph[num_row-1][j] == 1 and visited[num_row-1][j] == False:
            bfs(graph, visited, num_row-1, j)
            
    for i in range(num_row):
        if graph[i][0] == 1 and visited[i][0] == False:
            bfs(graph, visited, i, 0)
        if graph[i][num_col-1] == 1 and visited[i][num_col-1] == False:
            bfs(graph, visited, i, num_col-1)
    
    
    # 这些边界上的岛屿的面积不计入结果
    global result
    result = 0
    
    # 对于内部的所有岛屿计算总面积
    for i in range(1, num_row):
        for j in range(1, num_col):
            if graph[i][j] == 1 and visited[i][j] == False:
                bfs(graph, visited, i, j)
    
    print(result)

if __name__ == "__main__":
    main()

3. 太平洋大西洋水流问题(LeetCode 417)

现有一个 N × M 的矩阵,每个单元格包含一个数值,这个数值代表该位置的相对高度。
矩阵的左边界和上边界被认为是第一组边界,而矩阵的右边界和下边界被视为第二组边界。

矩阵模拟了一个地形,当雨水落在上面时,水会根据地形的倾斜向低处流动,但只能从较高或等高的地点流向较低或等高并且相邻(上下左右方向)的地点。
我们的目标是确定那些单元格,从这些单元格出发的水可以达到第一组边界和第二组边界。

思路:

  • 暴力做法:从每个点开始进行一次dfs,时间复杂度为 O(n^4)
  • 优化后每个节点最多访问两次,时间复杂度为O(2*n^2)
class Solution:
    def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:

        # flag=1表示从边界1出发进行访问,flag=2表示从边界2出发进行访问
        def dfs(heights, visited, x, y, flag):
            directions = [[0,1], [0,-1], [-1, 0], [1, 0]]
            if visited[x][y] == 0:
                visited[x][y] = flag
            elif visited[x][y] == 1 and flag == 2:
                visited[x][y] = 3
            
            for i in range(4):
                next_x = x + directions[i][0]
                next_y = y + directions[i][1]
                if next_x < 0 or next_x >= len(heights) or next_y < 0 or next_y >= len(heights[0]):
                    continue
                if flag == visited[next_x][next_y] or visited[next_x][next_y] == 3:
                    continue
                # 从边界往中间逆流而上
                if heights[next_x][next_y] >= heights[x][y]:
                    dfs(heights, visited, next_x, next_y, flag)

        num_row = len(heights)
        num_col = len(heights[0])
        # 0表示无法从任一边界到达(i,j),
        # 1表示只能从边界1到达,
        # 2表示只能从边界2到达
        # 3表示从2种边界都能到达
        visited = [[0 for _ in range(num_col)] for _ in range(num_row)]
            
        # 从边界1反向dfs
        # 上边界
        for j in range(num_col):
            dfs(heights, visited, 0, j, 1)
        # 左边界
        for i in range(num_row):
            dfs(heights, visited, i, 0, 1)
        
        # 从边界2反向dfs
        # 下边界
        for j in range(num_col):
            dfs(heights, visited, num_row-1, j, 2)
        # 右边界
        for i in range(num_row):
            dfs(heights, visited, i, num_col-1, 2)
        
        result = []
        for i in range(num_row):
            for j in range(num_col):
                if visited[i][j] == 3:
                    result.append([i, j])

        return result

4. 最大人工岛(LeetCode 827)

给你一个大小为 n x n 二进制矩阵 grid 。最多只能将一格 0 变成 1 。
返回执行此操作后,grid 中最大的岛屿面积是多少?
岛屿 由一组上、下、左、右四个方向相连的 1 形成。

class Solution:
    def largestIsland(self, grid: List[List[int]]) -> int:
        # 上下左右
        directions = [[0,1], [0,-1], [-1,0], [1,0]]
        global area    # 记录每个单个岛屿的面积
        def dfs(grid, visited, x, y, islandId):
            global area
            visited[x][y] = True
            grid[x][y] = islandId
            area += 1
            for i in range(4):
                next_x = x + directions[i][0]
                next_y = y + directions[i][1]
                if next_x >= 0 and next_x < len(grid) and next_y >= 0 and next_y < len(grid[0]):
                    if visited[next_x][next_y] == False and grid[next_x][next_y] > 0:
                        dfs(grid, visited, next_x, next_y, islandId)

        result = 0
        islandId = 2        # 岛屿的编号从2开始
        area = 0
        allLand = True      # 判断是否全部为陆地
        map = dict()        # 用于建立岛号与岛屿面积的映射
        map[0] = 0

        visited = [[False for _ in range(len(grid[0]))] for _ in range(len(grid))]
        # 建立每个岛屿和岛屿面积的映射
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if grid[i][j] == 0:
                    allLand = False
                if grid[i][j] == 1 and visited[i][j] == False:
                    area = 0
                    dfs(grid, visited, i, j, islandId)
                    map[islandId] = area
                    islandId += 1

        if allLand:
            result = len(grid) * len(grid[0])
            return result

        islandSet = set()
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if grid[i][j] == 0:
                    # 用于记录当前0上下左右的岛屿号
                    islandSet.clear()     
                    curArea = 1
                    for k in range(4):
                        next_i = i + directions[k][0]
                        next_j = j + directions[k][1]
                        if next_i < 0 or next_i >= len(grid) or next_j < 0 or next_j >= len(grid[0]):
                            continue
                        if grid[next_i][next_j] not in islandSet:
                            islandSet.add(grid[next_i][next_j])
                            curArea += map[grid[next_i][next_j]]
                    result = max(result, curArea)

        return result

5. 单词接龙(LeetCode 108)

在字典(单词列表) wordList 中,从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:

  • 序列中第一个单词是 beginWord 。
  • 序列中最后一个单词是 endWord 。
  • 每次转换只能改变一个字母。
  • 转换过程中的中间单词必须是字典 wordList 中的单词。

给定两个长度相同但内容不同的单词 beginWord 和 endWord 和一个字典 wordList ,
找到从beginWordendWord最短转换序列中的单词数目。如果不存在这样的转换序列,返回 0。

输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。

思路

  • 本题等长单词,差距为1,即为两点相连。
  • 这里的做法没有显式地构造图,但本质上是邻接矩阵
  • 使用广度遍历得到的一定是最短路径
class Solution:
    # 广度遍历,到达endWord的路径即为最短路径
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        if endWord not in wordList:
            return 0
        
        # 判断2个等长的单词是否差距为1
        def isConnected(word1: str, word2: str) -> bool:
            count = 0
            for i in range(len(word1)):
                if word1[i] != word2[i]:
                    count += 1
            return count == 1
        
        visited = [False for _ in range(len(wordList))]
        que = [[beginWord, 1]]
        while que :
            s, step = que.pop(0)
            if isConnected(s, endWord):
                return step+1
            for i in range(len(wordList)):
                if visited[i] == False and isConnected(s, wordList[i]):
                    que.append([wordList[i], step+1])
                    visited[i] = True

        return 0

6. 有向图的完全可达性(卡码网 105)

给定一个有向图,包含 N 个节点,节点编号分别为 1,2,...,N。
现从 1 号节点开始,如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。
注意

  • grid = defaultdict(list),建立的是一个键为数值,值为列表的字典
from collections import defaultdict

def main():
    num_node, num_edge = map(int, input().split())
    # 邻接表
    grid = defaultdict(list)
    for _ in range(num_edge):
        node1, node2 = map(int, input().split())
        grid[node1].append(node2)
    
    reachable = []
    
    # dfs
    def dfs(grid, startId):
        for i in grid[startId]:
            if i not in reachable:
                reachable.append(i)
                dfs(grid, i)
    
    dfs(grid, 1)
    
    for i in range(2, num_node+1):
        if i not in reachable:
            print(-1)
            return 

    print(1)

if __name__ == "__main__":
    main()

7. 岛屿的周长(LeetCode 464)

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

class Solution:
    def islandPerimeter(self, grid: List[List[int]]) -> int:        
        global result
        result = 0

        def dfs(grid, visited, x, y):
            global result
            directions = [[0,1], [0,-1], [-1,0], [1,0]]
            visited[x][y] = True
            # 如果四周都没有1时,周长增加4
            result += 4
            for i in range(4):
                next_x = x + directions[i][0]
                next_y = y + directions[i][1]
                if next_x >=0 and next_x < len(grid) and next_y >= 0 and next_y < len(grid[0]):
                    if grid[next_x][next_y] == 1:
                        # 每相邻一个1,从4中减去1
                        result -= 1
                        if visited[next_x][next_y] == False:
                            dfs(grid, visited, next_x, next_y)
        
        visited = [[False for _ in range(len(grid[0]))] for _ in range(len(grid))]
        findOne = False
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if grid[i][j] == 1:
                    findOne = True
                    dfs(grid, visited, i, j)
                    break
            if findOne:
                break
        
        return result
posted @ 2024-10-28 17:16  Frank23  阅读(5)  评论(0编辑  收藏  举报