目录
一、队列和 BFS
二、广度优先搜索 - 模板
// JAVA implementation
/**
* Return the length of the shortest path between root and target node.
*/
int BFS(Node root, Node target) {
Queue<Node> queue; // store all nodes which are waiting to be processed
int step = 0; // number of steps neeeded from root to current node
// initialize
add root to queue;
// BFS
while (queue is not empty) {
step = step + 1;
// iterate the nodes which are already in the queue
int size = queue.size();
for (int i = 0; i < size; ++i) {
Node cur = the first node in queue;
return step if cur is target;
for (Node next : the neighbors of cur) {
add next to queue;
}
remove the first node from queue;
}
}
return -1; // there is no path from root to target
}
// JAVA implementation
/**
* Return the length of the shortest path between root and target node.
*/
int BFS(Node root, Node target) {
Queue<Node> queue; // store all nodes which are waiting to be processed
Set<Node> used; // store all the used nodes
int step = 0; // number of steps neeeded from root to current node
// initialize
add root to queue;
add root to used;
// BFS
while (queue is not empty) {
step = step + 1;
// iterate the nodes which are already in the queue
int size = queue.size();
for (int i = 0; i < size; ++i) {
Node cur = the first node in queue;
return step if cur is target;
for (Node next : the neighbors of cur) {
if (next is not in used) {
add next to queue;
add next to used;
}
}
remove the first node from queue;
}
}
return -1; // there is no path from root to target
}
三、墙与门 (PLUS 会员专享)
四、岛屿数量
4.1 题目要求
4.2 解决过程
法一:BFS 迭代实现。沿竖直和水平方向遍历输入矩阵,每发现一个陆地 (标号1),就对其进行 BFS,并将可由 BFS 访问/搜索到的陆地都置为已访问/已搜索 (标号0) 以表示属于同一个岛屿,且因为已访问/已搜索而不必在后续过程中再次访问/搜索 (意为不会访问同一个节点两次)。
2020/06/23 - 51.63%
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
land_nums = 0 # 岛屿数量
for row in range(len(grid)): # 遍历行
for col in range(len(grid[0])): # 遍历列
if grid[row][col] == '1': # 发现陆地
land_nums += 1 # 岛屿数量 +1
grid[row][col] = '0' # 将已访问/已探索的陆地置为 0, 以后便不必再重复访问/搜索
# 对已发现陆地通过 BFS 扩张/辐射, 将与之相邻的陆地都标记为已访问/已探索状态
land_positions = collections.deque() # 内置双端队列, 保存已访问/已探索相邻陆地坐标
land_positions.append([row, col]) # 当前已访问/已探索陆地坐标入队
while len(land_positions) > 0: # 只要当前队列中还保存有已访问/已探索陆地坐标
x, y = land_positions.popleft() # 出队探索
for new_x, new_y in [[x, y+1], [x, y-1], [x+1, y], [x-1, y]]: # 向 4 个方向扩张/辐射
# 判断有效性, 要求是坐标界限范围内的陆地
if (0 <= new_x < len(grid)) and (0 <= new_y < len(grid[0])) and (grid[new_x][new_y] == '1'):
grid[new_x][new_y] = '0' # 因为可由 BFS 访问到, 故属同一块岛,将其置 0 代表已访问/已探索
land_positions.append([new_x, new_y]) # 已访问/已探索陆地坐标入队
return land_nums
法一改:BFS 迭代实现 - 函数封装。
2020/06/23 - 44.98%
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
from collections import deque # 双端队列
if not grid:
return 0
row = len(grid) # 行数
col = len(grid[0]) # 列数
cnt = 0 # 岛屿数
def bfs(i, j):
queue = deque() # 保存已访问/已探索的陆地队列
grid[i][j] = "0" # 将已发现的陆地置为已访问/已探索状态 0
queue.appendleft((i, j)) # 已发现陆地坐标入队
while queue: # 只要队列中还有坐标
i, j = queue.pop() # 弹出一个坐标
for x, y in [[-1, 0], [1, 0], [0, -1], [0, 1]]: # 向周围 4 个方向以单位距离搜索
tmp_i = i + x
tmp_j = j + y
# 若在范围内发现新陆地
if (0 <= tmp_i < row) and (0 <= tmp_j < col) and (grid[tmp_i][tmp_j] == "1"):
grid[tmp_i][tmp_j] = "0" # 将已发现的陆地置为已访问/已探索状态 0
queue.appendleft((tmp_i, tmp_j)) # 已发现陆地坐标入队
# 遍历所有元素
for i in range(row):
for j in range(col):
if grid[i][j] == "1": # 若发现新陆地
bfs(i, j) # 进行 BFS
cnt += 1 # 岛屿数 +1
return cnt
法二:DFS 递归实现。抛开陆地、岛屿和水等概念,该问题的实质是:求一个只含 0/1 元素的矩阵中,元素为 1 的连通区域个数,如图所示:
为此,使用 DFS 的解决过程可表述为:
- 建立一个已访问/已探索数组 (visited array) 以记录某位置是否被访问/探索过 (的状态);
- 对于一个未被访问过且元素为 1 的位置,递归进入其上下左右元素为 1 之处,将其 visited 置为 true;
- 重复上述过程;
- 遍历完相邻区域后,令结果 cnt +1,然后继续搜素下一个被访问过且元素为 1 的位置,直至遍历完整个矩阵。
但本题目仅要求计算元素为 1 的连通区域个数,因此无需额外空间存储 visited 信息。此外,上述过程不会对元素为 0 的位置进行操作,故对于已访问/已探索的位置,将其元素置 0 即可,以节省 visited 信息的存储开销。
2020/06/23 - 29.04%
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid:
return 0
row = len(grid) # 行数
col = len(grid[0]) # 列数
cnt = 0 # 岛屿数
def dfs(i, j):
grid[i][j] = "0" # 将已发现的陆地置为已访问/已探索状态 0
for x, y in [[-1, 0], [1, 0], [0, -1], [0, 1]]: # 向周围 4 个方向以单位距离搜索
tmp_i = i + x
tmp_j = j + y
# 若在范围内发现新陆地
if (0 <= tmp_i < row) and (0 <= tmp_j < col) and (grid[tmp_i][tmp_j] == "1"):
dfs(tmp_i, tmp_j) # 递归进行 DFS
# 遍历所有元素
for i in range(row):
for j in range(col):
if grid[i][j] == "1": # 若发现新陆地
dfs(i, j) # 进行 DFS
cnt += 1 # 岛屿数 +1
return cnt
法二改:DFS 递归实现。
202006/23 - 51.63%
class Solution:
def dfs(self, grid, i, j):
# 等价的有效性条件
if (i < 0) or (j < 0) or (i >= len(grid)) or (j >= len(grid[0])) or (grid[i][j] != '1'):
return
else: # 若在范围内发现新陆地 (元素为1)
grid[i][j] = '0' # 将已发现的陆地置为已访问/已探索状态 0
# 然后往上下左右4个方向依次进行递归 DFS
self.dfs(grid, i+1, j) # 右
self.dfs(grid, i-1, j) # 左
self.dfs(grid, i, j+1) # 下
self.dfs(grid, i, j-1) # 上
def numIslands(self, grid: List[List[str]]) -> int:
if not grid:
return 0
cnt = 0 # 岛屿数
# 遍历所有元素
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1': # 若发现新陆地
self.dfs(grid, i, j) # 进行 DFS
cnt += 1 # 岛屿数 +1
return cnt
法三:并查集。
2020/06/23 - 35.34%
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
f = {}
def find(x):
# Python 字典 setdefault() 函数和 get()方法 类似, 若键不存在于字典中,将会添加键并将值设为默认值
f.setdefault(x, x)
if f[x] != x:
f[x] = find(f[x])
return f[x]
def union(x, y):
f[find(x)] = find(y)
if not grid:
return 0
row = len(grid)
col = len(grid[0])
for i in range(row):
for j in range(col):
if grid[i][j] == "1":
for x, y in [[-1, 0], [0, -1]]:
tmp_i = i + x
tmp_j = j + y
if 0 <= tmp_i < row and 0 <= tmp_j < col and grid[tmp_i][tmp_j] == "1":
union(tmp_i * row + tmp_j, i * row + j)
# print(f)
res = set()
for i in range(row):
for j in range(col):
if grid[i][j] == "1":
res.add(find((i * row + j)))
return len(res)
小贴士:
问:已经有 BFS 了,为什么还要有 DFS ?
答:BFS 可以找到最短距离,但 空间复杂度高;而 DFS 的空间复杂度则相对较低。因此,使用 BFS 需要付出一定代价。通常,寻找最短路径时会使用 BFS,其他情况则更多会使用 DFS (递归代码相对好写)。
参考文献
五、打开转盘锁
5.1 题目要求
5.2 解决过程
法一:BFS 迭代实现。可先将 0000 ~ 9999 共计 10000 种状态抽象为 10000 个图节点,每个节点存在 8 个相邻节点。而两个节点之间存在一条边,当且仅当这两个节点对应的状态只有 1 位不同,且不同的那位相差 1 (包括 0 和 9 也相差 1 的情况),并且这两个节点均不在数组 deadends 中。那么求从 0000 到 target 最小的旋转次数就相当于在图上搜索最短距离/路径,故可用 BFS 实现,并以 0000 为起始节点开始搜索。对于每个状态,它可以扩展到最多 8 个状态,即将其第 i = 0, 1, 2, 3 位进行 +1 或 -1,将这些状态中没有搜索过且不在数组 deadends 中的状态全部加入队列,并重复上述过程。注意 0000 本身也可能在 deadends 中。
2020/06/24 - 79.65%
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
# 生成器 - 生成相邻节点/数字字符串
def neighbors(node):
# 每个节点有至多4*2=8个相邻节点
for i in range(4): # 4个拨轮
x = int(node[i]) # 取整
for d in (-1, 1): # 拨轮前转/后转1位
y = (x + d) % 10 # 拨轮位数循环0~9
yield node[:i] + str(y) + node[i+1:] # 生成新数字字符串
dead = set(deadends) # 死亡数字集合 (用 set 比 list 查找效率更高)
queue = collections.deque([('0000', 0)]) # 已发现未探索队列 (双端队列更普适)
seen = {'0000'} # 已探索集合
while queue: # 只要队列中还有节点, 就不断搜索直至队空
node, depth = queue.popleft() # 出队, 取出节点/数字字符串 + 深度/旋转次数
if node == target: # 到达目标数字
return depth
if node in dead: # 到达死亡数字
continue
for nei in neighbors(node): # 正常搜索
if nei not in seen: # 若不在已探索集合中
seen.add(nei) # 便加入已探索集合
queue.append((nei, depth+1)) # 入队
return -1 # 无论如何不能解锁则返回 -1
事实上,BFS 算法还有另一种优化思路:双向 BFS,能够进一步提高算法效率。简言之,传统的 BFS 从起点开始向四周扩散,直至遇到终点停止,相当于单向 BFS;而双向 BFS 则 从起点和终点同时开始扩散,当两边有交集时停止。
参考文献
https://leetcode-cn.com/leetbook/read/queue-stack/kj48j/
https://leetcode-cn.com/problems/open-the-lock/solution/da-kai-zhuan-pan-suo-by-leetcode/
https://leetcode-cn.com/problems/open-the-lock/solution/python-bfs-qing-xi-ti-jie-by-knifezhu/
六、完全平方数
6.1 题目要求
6.2 解决过程
官方实现与说明
class Solution(object):
def numSquares(self, n):
square_nums = [i**2 for i in range(1, int(math.sqrt(n))+1)] # 平方数列表
def minNumSquares(k):
""" recursive solution """
# bottom cases: find a square number
if k in square_nums:
return 1
min_num = float('inf')
# Find the minimal value among all possible solutions
for square in square_nums:
if k < square:
break
new_num = minNumSquares(k-square) + 1 # 关键语句
min_num = min(min_num, new_num)
return min_num
return minNumSquares(n)
class Solution:
def numSquares(self, n: int) -> int:
square_nums = [i**2 for i in range(0, int(math.sqrt(n))+1)] # 可选平方数
dp = [float('inf') for _ in range(n+1)] # dp数组 - index:需要凑齐的目标值, elem:最少个数
dp[0] = 0 # bottom case
for i in range(1, n+1): # 需要凑齐的目标值 i
for square in square_nums: # 遍历可选平方数列表 square_nums
if i < square:
break
# 若最后一个数选择 square, 则共需 dp[i-square]+1 个
dp[i] = min(dp[i], dp[i-square] + 1) # 关键语句
return dp[-1]
2020/08/30 - 38.08% (5276ms) - 这和凑钱问题 (322.零钱兑换) 十分相似 (均采用 DP 解法)
class Solution:
def numSquares(self, n: int) -> int:
def is_divided_by(n, count):
"""
return: true if "n" can be decomposed into "count" number of perfect square numbers.
e.g. n=12, count=3: true.
n=12, count=2: false
"""
if count == 1:
return n in square_nums
for k in square_nums:
if is_divided_by(n - k, count - 1): # 贪心算法, 最前找到的就是最少的
return True
return False
square_nums = set([i * i for i in range(1, int(n**0.5)+1)]) # 可选平方数
for count in range(1, n+1): # count 为最少需要个数
if is_divided_by(n, count):
return count
2020/08/30 - 92.99% (80ms) - 质的飞跃
class Solution:
def numSquares(self, n: int) -> int:
# list of square numbers that are less than `n`
square_nums = [i * i for i in range(1, int(n**0.5)+1)] # 可选平方数
level = 0 # 层级
queue = {n} # 当前层
while queue:
level += 1
# Important: use set() instead of list() to eliminate the redundancy,
# which would even provide a 5-times speedup, 200ms vs 1000ms.
next_queue = set() # 下一层
# construct the queue for the next level
for remainder in queue: # 遍历当前层
for square_num in square_nums: # 遍历可选平方数
if remainder == square_num:
return level # find the node!
elif remainder < square_num:
break
else:
next_queue.add(remainder - square_num) # 下一层加入
queue = next_queue # 当前层下移
return level
2020/08/30 - 90.70% (184ms) - 虽然性能差些,但思想很好
class Solution:
def isSquare(self, n: int) -> bool:
sq = int(math.sqrt(n))
return sq*sq == n
def numSquares(self, n: int) -> int:
# four-square and three-square theorems
while (n & 3) == 0:
n >>= 2 # reducing the 4^k factor from number
if (n & 7) == 7: # mod 8
return 4
if self.isSquare(n):
return 1
# check if the number can be decomposed into sum of two squares
for i in range(1, int(n**(0.5)) + 1):
if self.isSquare(n - i*i):
return 2
# bottom case from the three-square theorem
return 3
2020/08/30 - 99.32% (44ms) - 最佳
参考文献
https://leetcode-cn.com/leetbook/read/queue-stack/kfgtt/
https://leetcode-cn.com/problems/perfect-squares/solution/wan-quan-ping-fang-shu-by-leetcode/