描述
在给定的 m x n
网格 grid
中,每个单元格可以有以下三个值之一:
- 值
0
代表空单元格; - 值
1
代表新鲜橘子; - 值
2
代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1
。
示例 1:
输入:grid = [[2,1,1],[1,1,0],[0,1,1]] 输出:4
示例 2:
输入:grid = [[2,1,1],[0,1,1],[1,0,1]] 输出:-1 解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个方向上。
示例 3:
输入:grid = [[0,2]] 输出:0 解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 10
grid[i][j]
仅为0
、1
或2
Leecode题解:腐烂的橘子
解题思路:广度优先搜索(BFS)
首先分别将腐烂的橘子和新鲜的橘子保存在两个集合中;
模拟广度优先搜索的过程,方法是判断在每个腐烂橘子的四个方向上是否有新鲜橘子,如果有就腐烂它。每腐烂一次时间加 1,并剔除新鲜集合里腐烂的橘子;
当橘子全部腐烂时结束循环。
腐烂的橘子
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
# 获取网格的行数和列数
row = len(grid)
col = len(grid[0])
# 使用集合推导式找到所有腐烂橘子的坐标 (值为2)
bad = {(i, j) for i in range(row) for j in range(col) if grid[i][j] == 2}
# 使用集合推导式找到所有新鲜橘子的坐标 (值为1)
fresh = {(i, j) for i in range(row) for j in range(col) if grid[i][j] == 1}
# 初始化计数器
count = 0
# 当还有新鲜橘子时
while fresh:
# 如果没有腐烂橘子可以感染新鲜橘子,返回 -1
if not bad:
return -1
# 找到下一轮会腐烂的新橘子
bad = {(i + di, j + dj) for i, j in bad for di, dj in [(0, 1), (0, -1), (1, 0), (-1, 0)] if (i + di, j + dj) in fresh}
# 将新腐烂的橘子从新鲜橘子集合中移除
fresh -= bad
# 增加轮数计数器
count += 1
# 返回所需的轮数
return count
关键点
- 集合运算:使用集合存储腐烂和新鲜橘子的坐标,并通过集合运算快速更新和查找。
- 广度优先搜索(BFS):通过每轮找到当前所有腐烂橘子周围的新鲜橘子,模拟感染过程,直至没有新鲜橘子。
-
四个方向上的移动:一般使用如下方法实现四个方向的移动:
# 设初始点为 (i, j) for di, dj in [(0, 1), (0, -1), (1, 0), (-1, 0)]: # 上、下、左、右 i + di, j + dj
复杂度分析
- 时间复杂度:O(mn)。
- 空间复杂度:O(mn)。
思路二:队列实现
前面的实现方法不是很普遍,BFS 的标准实现还是用队列。
队列实现 BFS 的方法相对固定,大致分三步:
初始化队列;
最开始的坏橘子全部入队,具体是橘子的坐标和 time;
循环:当队列不为空时,先弹出队首元素,然后将这个元素能够腐烂的橘子全部入队。
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
# 获取网格的行数、列数,并初始化计数器
row, col, count = len(grid), len(grid[0]), 0
# 定义四个方向的移动向量
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
# 初始化队列
queue = []
# 遍历网格,找到所有腐烂橘子并将其加入队列,初始化腐烂橘子的轮数为0
for i in range(row):
for j in range(col):
if grid[i][j] == 2:
queue.append((i, j, count))
# 开始BFS
while queue:
# 取出队列的第一个元素
i, j, count = queue.pop(0)
# 遍历四个方向
for di, dj in directions:
# 检查新位置是否在网格内且是新鲜橘子
if 0 <= i + di < row and 0 <= j + dj < col and grid[i + di][j + dj] == 1:
# 将新鲜橘子变为腐烂橘子
grid[i + di][j + dj] = 2
# 将新腐烂的橘子加入队列,并将腐烂轮数加1
queue.append((i + di, j + dj, count + 1))
# 检查网格中是否还有新鲜橘子
for row in grid:
if 1 in row:
return -1
# 返回腐烂的轮数
return count
关键点
- 队列:使用队列实现广度优先搜索,每轮处理队列中的所有腐烂橘子,并将新腐烂的橘子加入队列。
- 方向向量:定义四个方向的移动向量,方便检查每个腐烂橘子的周围位置。
- BFS:逐层扩展腐烂橘子的范围,直到所有新鲜橘子都被腐烂或无法继续腐烂。
优化点
- 当前代码已比较高效,若要进一步优化,可以考虑使用双端队列(deque)替换列表,以提高
pop(0)
操作的性能。
数据结构–双端队列
双端队列(Double-ended Queue,简称Deque)是一种具有队列和栈特性的数据结构,可以在队列的两端进行插入和删除操作。双端队列允许从前端和后端同时进行插入和删除操作,因此可以称为“两端都可以进出的队列”。
双端队列的特点包括:
可以在队列的头部和尾部进行插入和删除操作。
元素的插入和删除操作可以分别称为入队和出队操作。
可以实现先进先出(FIFO)和后进先出(LIFO)两种操作方式。
可以用于实现栈、队列以及其他需要在两端进行插入和删除操作的场景。
双端队列的常见操作包括:
在队列头部插入元素(头部入队):将元素插入到队列头部。
在队列尾部插入元素(尾部入队):将元素插入到队列尾部。
从队列头部删除元素(头部出队):删除队列头部的元素并返回。
从队列尾部删除元素(尾部出队):删除队列尾部的元素并返回。
获取队列头部的元素:返回队列头部的元素,但不删除。
获取队列尾部的元素:返回队列尾部的元素,但不删除。
判断队列是否为空:判断队列中是否有元素。
获取队列中的元素个数:返回队列中元素的个数。
双端队列的实现方式有多种,包括使用数组、链表等数据结构。具体的实现方式可以根据不同的需求和场景选择合适的方式。