【算法题】网格系列

目录

 

面试题4 二维数组中的查找--左下角二分查找 &240

51. N皇后-回溯

37. 解数独-回溯

36. 有效的数独-回溯

778. 水位上升的泳池中游泳-优先队列

面试题 16.04. 井字游戏

10 矩阵中的路径——深度优先搜索DFS

11 机器人运动范围

29. 顺时针打印矩阵 

200 岛屿数量——DFS 

695. 岛屿的最大面积

1162. 地图分析-BFS

207 课程表——DFS

47. 礼物的最大价值--动态规划

62. 不同路径 

63. 不同路径 II 


面试题4 二维数组中的查找--左下角二分查找 &240

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

思路

类似于二分查找,根据题目,如果拿数组中任意一个元素与目标数值进行比较,如果该元素小于目标数值,那么目标数值一定是在该元素的下方或右方,如果大于目标数值,那么目标数值一定在该元素的上方或者左方。对于二分查找来说,每次比较只能移动一个指针,在二维数组的查找中,两个指针是一个上下方向移动,一个是左右方向移动 。

两个指针可以从同一个角出发。 假设我们从左上角出发,也就是row=0 和 col=0,如果元素小于目标数值,我们会将row往下移或着col往右移,这样,被屏蔽的区域可能会是目标元素所在的区域。比如row+=1,那么第一行除左上角以外的元素就被忽略了,如果col+=1,那么第一列出左上角以外的元素就被忽略了。因此这样是不可行的。所以本题从左下角出发寻找解题思路

col1 col2 col3

row1 1 2 3 4 5

2 3 4 5 6

3 4 5 6 7

class Solution:
    def findNumberIn2DArray(self, matrix: List[List[int]], target: int) -> bool:
        if matrix == []: return False #重要
        rows = len(matrix) - 1
        cols = len(matrix[0]) - 1
        i = rows
        j = 0
        while i >= 0 and j <= cols:
            if target > matrix[i][j]:
                j += 1   #不能写成j++
            elif target < matrix[i][j]:
                i -= 1
            else:
                return True
        return False
       

能够得知 i += 1 的效率往往要比 i = i + 1 更高一些

时间复杂度 O(M+N) :其中,N 和 M 分别为矩阵行数和列数,此算法最多循环 M+N 次。

空间复杂度 O(1): i, j 指针使用常数大小额外空间。

51. N皇后-回溯

给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。

PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。

这是 N = 8 的一种放置方法:

这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。

输入: 4

输出: [

 [".Q..",  // 解法 1

  "...Q",

  "Q...",

  "..Q."],

 

 ["..Q.",  // 解法 2

  "Q...",

  "...Q",

  ".Q.."]

]

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        res = []
        track = []
        def isValid(track, row, col):
            if not track:
                return True
            for i in range(row):
                if track[i][col] == 'Q':  # 本列
                    return False
            i  = row - 1
            j  = col - 1
            while i >= 0 and j >= 0: #左上
                if track[i][j] == 'Q':
                    return False
                i -= 1
                j -= 1
            i = row - 1
            j = col + 1
            while i >= 0 and j < n: # 右上
                if track[i][j] == 'Q':
                    return False
                i -= 1
                j += 1
            return True


        def backtrack(track, row):
            #print(track)
            if len(track) == n:
                res.append(track[:])
                return
            for i in range(n):#n:col
                ans = ['.'] * n #改在这里,就不需要回退时处理了不然会出现[Q,Q]
                if isValid(track, row, i):
                    ans[i] = 'Q'
                    track.append(list(ans))
                    backtrack(track,row + 1)
                    track.pop()
        backtrack([], 0)
        res2 = [[] for _ in range(len(res))]
        for index,re in enumerate(res):
            for r in re:    
                r_str = "".join(r)
                res2[index].append(r_str)
            
        return res2
       

37. 解数独-回溯

编写一个程序,通过已填充的空格来解决数独问题。

一个数独的解法需遵循如下规则:

数字 1-9 在每一行只能出现一次。

数字 1-9 在每一列只能出现一次。

数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

空白格用 '.' 表示。

 

求解数独的思路很简单粗暴,就是对每一个格子所有可能的数字进行穷举

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        if not board: return 
        def isValid(board, row, col, c):
            for i in range(9):
                if board[row][i] == c:
                    return False
                if board[i][col] == c:
                    return False
                if board[3 * (row//3) + i//3][3 * (col//3) + i%3] == c:
                    return False
            return True
        def backtrack(board):
            for i in range(len(board)):
                for j in range(len(board[0])):
                    if board[i][j] == '.':
                        for c in ["1","2","3","4","5","6","7","8","9"]:
                            if isValid(board, i, j, c):
                                board[i][j] = c
                                if backtrack(board):
                                    return True
                                else:
                                    board[i][j] = '.'
                        return False
            return True
        backtrack(board)

36. 有效的数独-回溯

判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。

一个有效的数独(部分已被填充)不一定是可解的。

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
class Solution:
    def isValidSudoku(self, board: List[List[str]]) -> bool:    
        def isValid(board, row, col, c):
            count1,count2,count3 = 0,0,0
            for i in range(9):
                if board[row][i] == c:
                    count1 += 1
                if board[i][col] == c:
                    count2 += 1
                if board[3 * (row//3) + i//3][3 * (col//3) + i%3] == c:
                    count3 += 1
            if count1==1 and count2==1 and count3==1: 
                return True
            else:
                return False
        for i in range(len(board)):
            for j in range(len(board[0])):
                c = board[i][j]
                if c !='.' and not isValid(board,i,j,c):
                    return False
        return True

778. 水位上升的泳池中游泳-优先队列

在一个 N x N 的坐标方格 grid 中,每一个方格的值 grid[i][j] 表示在位置 (i,j) 的平台高度。

现在开始下雨了。

当时间为 t 时,此时雨水导致水池中任意位置的水位为 t 。你可以从一个平台游向四周相邻的任意一个平台,但是前提是此时水位必须同时淹没这两个平台。假定你可以瞬间移动无限距离,也就是默认在方格内部游动是不耗时的。当然,在你游泳的时候你必须待在坐标方格里面。

你从坐标方格的左上平台 (0,0) 出发。最少耗时多久你才能到达坐标方格的右下平台 (N-1, N-1)?

输入: [[0,2],[1,3]]
输出: 3
解释:
时间为0时,你位于坐标方格的位置为 (0, 0)。
此时你不能游向任意方向,因为四个相邻方向平台的高度都大于当前时间为 0 时的水位。


等时间到达 3 时,你才可以游向平台 (1, 1). 因为此时的水位是 3,坐标方格中的平台没有比水位 3 更高的,所以你可以游向坐标方格中的任意位置

用优先队列保存下一步可以游向的平台,每次都选择高度最小的平台。以这种方式到达终点时,路径中遇到的最高平台就是答案。

class Solution(object):
    def swimInWater(self, grid):
        N = len(grid)
        seen = {(0, 0)} #标记已访问
        pq = [(grid[0][0], 0, 0)]
        ans = 0
        while pq:
            d, i, j = heapq.heappop(pq)#弹出堆中最小值
            ans = max(ans, d)
            if i == j == N-1: return ans #达到边界返回
            for a, b in ((i-1, j), (i+1, j), (i, j-1), (i, j+1)):
                if 0 <= a < N and 0 <= b < N and (a, b) not in seen:
                    heapq.heappush(pq, (grid[a][b], a, b))
                    seen.add((a, b))

面试题 16.04. 井字游戏

设计一个算法,判断玩家是否赢了井字游戏。输入是一个 N x N 的数组棋盘,由字符" ","X"和"O"组成,其中字符" "代表一个空位。

以下是井字游戏的规则:

  • 玩家轮流将字符放入空位(" ")中。
  • 第一个玩家总是放字符"O",且第二个玩家总是放字符"X"。
  • "X"和"O"只允许放置在空位中,不允许对已放有字符的位置进行填充。
  • 当有N个相同(且非空)的字符填充任何行、列或对角线时,游戏结束,对应该字符的玩家获胜。
  • 当所有位置非空时,也算为游戏结束。
  • 如果游戏结束,玩家不允许再放置字符。

如果游戏存在获胜者,就返回该游戏的获胜者使用的字符("X"或"O");如果游戏以平局结束,则返回 "Draw";如果仍会有行动(游戏未结束),则返回 "Pending"。

输入: board = ["O X"," XO","X O"]

输出: "X"

输入: board = ["OOX","XXO","OXO"]

输出: "Draw"

解释: 没有玩家获胜且不存在空位

输入: board = ["OOX","XXO","OX "]

输出: "Pending"

解释: 没有玩家获胜且仍存在空位

class Solution:
    def tictactoe(self, board: List[str]) -> str:
        n = len(board)
        def check(c):
            s = c * n
            return any((
                any(row == s for row in board),
                any(col == s for col in map(''.join, zip(*board))),
                all(board[i][i] == c for i in range(n)),
                all(board[i][n - i - 1] == c for i in range(n))
            ))
        if check('X'):
            return 'X'
        if check('O'):
            return 'O'
        if ' ' in ''.join(board):
            return 'Pending'
        return 'Draw'

10 矩阵中的路径——深度优先搜索DFS

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。

[["a","b","c","e"],

["s","f","c","s"],

["a","d","e","e"]]

但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。

本问题是典型的矩阵搜索问题,此类问题通常可使用 深度优先搜索(DFS)+ 剪枝 解决。

算法原理:

 

深度优先搜索: 可以理解为暴力法遍历矩阵中所有字符串可能性。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。

剪枝: 在搜索中,遇到 这条路不可能和目标字符串匹配成功 的情况(例如:此矩阵元素值和目标字符值不同、路径已访问此元素),则应立即返回,称之为 可行性剪枝 。

 

从每个节点 DFS 的顺序为:下、上、右、左

递归参数: 当前元素在 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。

终止条件:

返回 false: ① 行列索引越界 或 ② 当前矩阵元素与目标字符不同 或 ③ 当前矩阵元素已访问过 (③ 可合并至 ② ) 。

返回 true : k = len(word) - 1,即字符串 word 已匹配完成。

递推工作: 

标记当前矩阵元素: 将 board[i][j] 值暂存于变量 tmp ,并修改为字符 '/' ,代表此元素已访问过,防止之后搜索时重复访问。

搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需一条可行路径) ,并记录结果至 res 。

还原当前矩阵元素: 将 tmp 暂存值还原至 board[i][j] 元素。

回溯返回值: 返回 res ,代表是否搜索到目标字符串。

class Solution:
  def exit(self, board, word):
    def dfs(i,j,k):
      if not 0=<i< len(board) or not 0<=j<len(board[0]) or board[i][j]!=work[k]: return False
      if k == len(word) - 1:
      return True
      tmp, board[i][j] = board[i][j], '/'
      res = dfs(i+1,j, k+1) or dfs(i-1,j,k+1) or dfs(i,j-1,k+1) or dfs(i,j+1,k+1)
      board[i][j] = tmp
      return res
    for i in range(len(board)):
      for j in range(len(board[0])):
         if dfs(i,j,0): return True
    return False
   

 

11 机器人运动范围

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

示例 1:

输入:m = 2, n = 3, k = 1

输出:3

class Solution:
    def movingCount(self, m, n, k):
        visited=[[False for _ in range(n)] for _ in range(m)] 
        return self.dfs(0,0,m,n,visited,k) 


    def dfs(self,i,j,m,n,visited,k):
        if not 0<=i<m or not 0<=j<n or self.cal(i)+self.cal(j)>k or visited[i][j]==True: #边界条件
            return 0
       visited[i][j]=True
        #回溯子状态
        return self.dfs(i-1,j,m,n,visited,k)+self.dfs(i,j-1,m,n,visited,k)+self.dfs(i+1,j,m,n,visited,k)+self.dfs(i,j+1,m,n,visited,k)+1
  
    def cal(self,num): #计算行坐标和列坐标数位和
        total=0
        while num>0:
            total+=num%10
            num//=10
        return total

29. 顺时针打印矩阵 

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。 

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]

输出:[1,2,3,6,9,8,7,4,5]

class Solution:
    def spiralOrder(self, matrix:[[int]]) -> [int]:
        if not matrix: return []
        l, r, t, b, res = 0, len(matrix[0]) - 1, 0, len(matrix) - 1, []
        while True:
            for i in range(l, r + 1): res.append(matrix[t][i]) # left to right
            t += 1
            if t > b: break
            for i in range(t, b + 1): res.append(matrix[i][r]) # top to bottom
            r -= 1
            if l > r: break
            for i in range(r, l - 1, -1): res.append(matrix[b][i]) # right to left
            b -= 1
            if t > b: break
            for i in range(b, t - 1, -1): res.append(matrix[i][l]) # bottom to top
            l += 1
            if l > r: break
        return res

200 岛屿数量——DFS 

目标是找到矩阵中 “岛屿的数量” ,上下左右相连的 1 都被认为是连续岛屿。
dfs方法: 设目前指针指向一个岛屿中的某一点 (i, j),寻找包括此点的岛屿边界。
1. 从 (i, j) 向此点的上下左右 (i+1,j),(i-1,j),(i,j+1),(i,j-1) 做深度搜索。
2. 终止条件:
(1)(i, j) 越过矩阵边界;
(2)grid[i][j] == 0,代表此分支已越过岛屿边界。
主循环:
遍历整个矩阵,当遇到 grid[i][j] == '1' 时,从此点开始做深度优先搜索 dfs,岛屿数 count + 1 且在深度优先搜索中删除此岛屿。
最终返回岛屿数 count 即可。
class Solution:
    def numIslands(self, grid: [[str]]) -> int:
        m = len(grid)
        if m == 0: return 0
        n = len(grid[0])
        def dfs(i, j):
            if i<0 or i>=m or j<0 or j>=n or grid[i][j]=='0': return
            grid[i][j] = '0'
            dfs(i + 1, j)
            dfs(i, j + 1)
            dfs(i - 1, j)
            dfs(i, j - 1)
        count = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == '1':
                    dfs(i, j)
                    count += 1
        return count

695. 岛屿的最大面积

仍然可以采用原位修改的方式避免记录visited的开销。我们的做法是将grid[i][j] = 0

时间复杂度:O(m∗n)

空间复杂度:O(m∗n)

 

class Solution:
     def maxAreaOfIsland(self, grid):
         m = len(grid)
         if m ==0: return 0
         n = len(grid[0])
         ans = 0
         def dfs(i,j):
             if i<0 or i >=m or j<0 or j>=n: return 0
             if grid[i][j] == 0: return 0
             grid[i][j] = 0
             top = dfs(i+1,j)
             bottom = dfs(i-1,j)
             left = dfs(i,j-1)
             right = dfs(i,j+1)
             return 1+sum([top,bottom,left,right])
        for i in range(m):
            for j in range(n):
                ans = max(ans,dfs(i,j))
        return ans
    

1162. 地图分析-BFS

不用visited,而是原地修改。由于这道题求解的是最远的距离,而距离我们可以使用BFS来做。算法:

对于每一个海洋,我们都向四周扩展,寻找最近的陆地,每次扩展steps加1。

如果找到了陆地,我们返回steps。

我们的目标就是所有steps中的最大值。

https://leetcode-cn.com/problems/as-far-from-land-as-possible/solution/python-tu-jie-chao-jian-dan-de-bfs1162-di-tu-fen-x/

class Solution:
    def maxDistance(self, grid: List[List[int]]) -> int:
        n = len(grid)
        steps = -1
        queue = [(i, j) for i in range(n) for j in range(n) if grid[i][j] == 1]
        if len(queue) == 0 or len(queue) == n ** 2: return steps
        while len(queue) > 0:
            for _ in range(len(queue)): 
                x, y = queue.pop(0)
                for xi, yj in [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]:
                    if xi >= 0 and xi < n and yj >= 0 and yj < n and grid[xi][yj] == 0:
                        queue.append((xi, yj))
                        grid[xi][yj] = -1
            steps += 1
                
        return steps

207 课程表——DFS

现在你总共有 n 门课需要选,记为 0 到 n-1。

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]

给定课程总量以及它们的先决条件,判断是否可能完成所有课程的学习?

示例 1:

输入: 2, [[1,0]]

输出: true

解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。

示例 2:

输入: 2, [[1,0],[0,1]]

输出: false

解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。

思路: 通过DFS判断图中是否有环

借助一个标志列表flags,用于判断每个节点i(课程)的状态:

  • 未被DFS访问: i == 0
  • 已被其他节点启动的DFS访问: i == -1
  • 已被当前节点启动的DFS访问:i == 1

对 numCourses 个节点依次执行DFS,判断每个节点起步DFS是否存在环,若存在环直接返回FALSE。

DFS流程

终止条件:当flag[i] == -1, 说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True.当flag[i] ==1, 说明在本轮 DFS 搜索中节点 i 被第 2 次访问,即 课程安排图有环,直接返回False。

将当前访问节点 i 对应 flag[i] 置 1,即标记其被本轮 DFS 访问过;

递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 False

当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 −1并返回 True

若整个图 DFS 结束并未发现环,返回 True

class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        def dfs(i, adjacency, flags):
            if flags[i] == -1: return True
            if flags[i] == 1: return False
            flags[i] = 1
            for j in adjacency[i]:
                if not dfs(j, adjacency, flags): return False
            flags[i] = -1
            return True
        
        adjacency = [[] for _ in range(numCourses)]
        flags = [0 for _ in range(numCourses)]
        for cur, pre in prerequisites:
            adjacency[pre].append(cur)
        for i in range(numCourses):
            if not dfs(i, adjacency, flags): return False
        return True

47. 礼物的最大价值--动态规划

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

动态规划
这次用的方法不需要额外的存储空间,直接在原数组上进行修改
时间复杂度O(m*n),空间复杂度O(1)
class Solution:
    def maxValue(self, grid):
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if i == 0 and j==0: continue
                if i== 0 and j >= 1:
                    grid[i][j] = grid[i][j-1]+grid[i][j]
                 elif j == 0 and i >=1:
                     grid[i][j] = grid[i-1][j] +grid[i][j]
                  else:
                      A = grid[i-1][j] + grid[i][j]
                      B = grid[i][j-1]+ grid[i][j]
                      grid [i][j] = max(A,B)
         return grid[len(grid)-1][len(grid[0])-1]
                 

62. 不同路径 

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0]*(n) for _ in range(m)]
        for i in range(m):
            for j in range(n):
                if i==0 or j==0:
                    dp[i][j] = 1
                else:
                    dp[i][j] = dp[i-1][j]+dp[i][j-1]
        return dp[-1][-1]

63. 不同路径 II 

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

输入:

[

  [0,0,0],

  [0,1,0],

  [0,0,0]

]

输出: 2

解释:

3x3 网格的正中间有一个障碍物。

从左上角到右下角一共有 2 条不同的路径:

1. 向右 -> 向右 -> 向下 -> 向下

2. 向下 -> 向下 -> 向右 -> 向右

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m = len(obstacleGrid) #每一行
        n = len(obstacleGrid[0]) #每一列
        for i in range(m):
            for j in range(n):
                if obstacleGrid[i][j]: #如果有障碍物
                    obstacleGrid[i][j] = 0
                else: #没有障碍物
                    if i==j==0:
                        obstacleGrid[i][j]=1
                    else:
                        a = obstacleGrid[i-1][j] if i!=0 else 0 #上方格
                        b = obstacleGrid[i][j-1] if j!=0 else 0 #下方格
                        obstacleGrid[i][j] = a+b
        return obstacleGrid[-1][-1]
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值