回溯算法是一种解决问题的算法,通常用于在一个大的问题空间中搜索所有可能的解,直到找到满足特定条件的解为止。它通常用于解决组合优化、排列组合、棋盘游戏、图搜索等问题。
回溯算法的基本思想是通过递归地尝试所有可能的解,当发现当前解无法满足条件时,就回溯到上一个状态,尝试其他可能的解。其核心是”试错”,在搜索过程中,不断地选择一个候选解,并检查是否满足问题的条件,如果满足则继续搜索下去,如果不满足则进行回溯,撤销上一步的选择,尝试其他的选择。
通常,回溯算法包括三个重要部分:
回溯算法的框架通常是递归的,每次递归调用代表一次选择过程。在递归调用中,需要在每一步做出选择,并进行约束检查。当找到符合条件的解时,算法停止,并返回结果;当遍历完所有可能的选择后,仍未找到满足条件的解时,算法也会停止。
虽然回溯算法通常是一种暴力搜索方法,但是在实际应用中,通过一些剪枝策略可以减少搜索空间,提高算法的效率。但是回溯方法比暴力破解更好,因为我们不必生成所有可能的解决方案并从中选择所需的解决方案。它为我们提供了在每次可能的递归调用时检查所需条件的选项。如果满足条件,我们将继续探索这条道路。如果不是,我们会退后一步,探索另一条路。通过这种方式,我们可以避免生成冗余的解决方案。
这个问题也可以归类为图结构:矩阵和邻接表的DFS和BFS问题。
Flood Fill(洪水填充)是一种经典的图像处理算法,用于将图像中的连通区域填充为指定的颜色。该算法通常用于图像编辑软件中的填充工具(interesting),也用于计算机图形学中的图像处理和区域分割任务。
在 Flood Fill 算法中,给定一个起始点(seed point)和目标颜色,该算法会将起始点所在的连通区域中所有的像素都填充为目标颜色。填充过程是通过搜索相邻像素的方式进行的,当相邻像素的颜色与起始点的颜色相同,并且未被填充过时,就将其填充为目标颜色,并将其加入到填充队列中继续搜索。
Flood Fill 算法可以用递归或非递归的方式实现。在递归实现中,从起始点开始向四个方向(上、下、左、右)搜索相邻像素,并对相邻像素进行颜色填充,然后递归调用自身来处理相邻像素的连通区域。而非递归实现通常采用队列数据结构来存储待处理的像素,通过循环遍历队列中的像素并进行颜色填充,直到队列为空为止。
这道题中是一个矩阵,给定的填充是一个target的数字。以下是力扣给的例子,源点是(1,1)的坐标,目标颜色的数字是 2,最终结果是和源点坐标邻接的位置,都转换为 2。
Input: image = [
[1,1,1],
[1,1,0],
[1,0,1]
], sr = 1, sc = 1, color = 2
Output: [
[2,2,2],
[2,2,0],
[2,0,1]]
解题思路:
代码尝试:
def flood_fill(grid, sr, sc, target):
source = grid[sr][sc]
visit = set()
def dfs(grid, r, c, source, target):
if r < 0 or c < 0 \
or r >= len(grid) or c >= len(grid[0]) \
or grid[r][c] != source \
or (r, c) in visit:
return
grid[r][c] = target
visit.add((r, c))
dfs(grid, r - 1, c, source, target)
dfs(grid, r + 1, c, source, target)
dfs(grid, r, c - 1, source, target)
dfs(grid, r, c + 1, source, target)
dfs(grid, sr, sc, source, target)
return grid
参考答案题解:同样是深度优先搜索,遍历的时候将坐标提取出来了。
def flood_fill(grid, sr, sc, target):
if grid[sr][sc] == target:
return grid
else:
old_target = grid[sr][sc]
grid[sr][sc] = target
dfs(grid, sr, sc, old_target, target)
return grid
def dfs(grid, row, col, old_target, new_target):
adjacent_cells = [[0, 1], [1, 0], [-1, 0], [0, -1]]
grid_length = len(grid)
total_cells = len(grid[0])
for cell_value in adjacent_cells:
i = row + cell_value[0]
j = col + cell_value[1]
if i < grid_length and i >= 0 and j < total_cells and j >= 0 and grid[i][j] == old_target:
grid[i][j] = new_target
dfs(grid, i, j, old_target, new_target)
补充,这道题还可以用 stack 进行迭代的方法,是一种优化的方法,尝试一下:
def flood_fill(grid, sr, sc, target):
source = grid[sr][sc]
stack = [(sr, sc)]
visit = set()
while stack:
r, c = stack.pop()
if r < 0 or c < 0 \
or r >= len(grid) or c >= len(grid[0]) \
or grid[r][c] != source \
or (r, c) in visit:
continue
grid[r][c] = target
visit.add((r, c))
stack.append((r - 1, c))
stack.append((r + 1, c))
stack.append((r, c - 1))
stack.append((r, c + 1))
return grid
顺利通过,是一种优化的方法。还是很喜欢stack。
学习笔记:时间和空间复杂度都是O(m x n)。
经典的搜索问题,在一个二维字符网格中查找给定的单词是否存在。单词可以在网格中按顺序横向或纵向连续出现,但不能跨越网格中的字符。
具体来说,给定一个二维字符网格和一个单词,需要判断是否能在网格中找到单词的完整路径。路径的方向可以是水平或垂直的,但不能是对角线方向。每个单元格中的字符只能使用一次。
例如,给定一个网格和单词 “ABCCED”:
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
可以看到单词 “ABCCED” 存在于网格中,路径为 (0,0) -> (0,1) -> (0,2) -> (1,2) -> (2,2) -> (2,1) -> (2,0)。
Word Search问题的解法通常使用深度优先搜索(DFS)算法来搜索可能的路径。我们从网格中的每个字符开始,尝试构建以该字符为起点的路径,递归地向四个方向探索,直到找到完整的单词或无法继续探索为止。这就是完整了解题思路了。回溯问题的解决思路很好说清楚,剩下的就是逻辑处理了。
代码尝试:本着先尝试再修改的原则。
def word_search(grid, word):
def dfs(grid, visit, r, c, idx, word):
if r < 0 or c < 0 \
or r >= len(grid) or c >= len(grid[0]) \
or (r, c) in visit \
or idx > len(word) - 1:
return
if grid[r][c] == word[idx] and idx == len(word) - 1:
return True
visit.add((r, c))
res = dfs(grid, visit, r - 1, c, idx + 1, word) or \
dfs(grid, visit, r + 1, c, idx + 1, word) or \
dfs(grid, visit, r, c - 1, idx + 1, word) or \
dfs(grid, visit, r, c + 1, idx + 1, word)
if res:
return True
visit.remove((r, c)) # 回溯时要移除访问标记
return False
for row in range(len(grid)):
for col in range(len(grid[0])):
if grid[row][col] == word[0]:
print(row, col)
if not dfs(grid, set(), row, col, 0, word):
continue
else:
return True
return False
grid = [
["N", "W", "L", "I", "M"],
["V", "I", "L", "Q", "O"],
["O", "L", "A", "T", "O"],
["R", "T", "A", "I", "N"],
["O", "I", "T", "N", "C"]
]
word = "LATIN"
res = word_search(grid, word)
print(res)
然后进行代码优化:
def word_search(grid, word):
def dfs(grid, visit, r, c, idx, word):
if idx == len(word) - 1:
return True
visit.add((r, c))
for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
nr, nc = r + dr, c + dc
if 0 <= nr < len(grid) and 0 <= nc < len(grid[0]) and (nr, nc) not in visit and grid[nr][nc] == word[idx + 1]:
if dfs(grid, visit, nr, nc, idx + 1, word):
return True
visit.remove((r, c))
return False
for row in range(len(grid)):
for col in range(len(grid[0])):
if grid[row][col] == word[0]:
if len(word) <= 1 or dfs(grid, set(), row, col, 0, word):
return True
return False
参考答案给的代码:结构和上面的一样。
# Function to search a specific word in the grid
def word_search(grid, word):
n = len(grid)
m = len(grid[0])
for row in range(n):
for col in range(m):
if depth_first_search(row, col, word, 0, grid):
return True
return False
# Apply backtracking on every element to search the required word
def depth_first_search(row, col, word, index, grid):
if len(word) == index:
return True
if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]) \
or grid[row][col] != word[index]:
return False
temp = grid[row][col]
grid[row][col] = '*'
for rowOffset, colOffset in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
if depth_first_search(row + rowOffset, col + colOffset, word, index + 1, grid):
return True
grid[row][col] = temp
return False
学习笔记:
这道题的时间复杂度取决于两个方面:
遍历整个二维网格:这一步的时间复杂度为 O(m*n),其中 m 和 n 分别是网格的行数和列数。
在每个可能的起始位置上进行深度优先搜索:最坏情况下,我们需要在网格的每个位置上都进行深度优先搜索。在每个搜索过程中,我们最多需要搜索目标单词的长度个位置。因此,深度优先搜索的时间复杂度为 O(mnl),其中 l 是目标单词的长度。
综合考虑以上两个方面,整体的时间复杂度为 O(mnl)。
至于空间复杂度,主要取决于深度优先搜索的递归调用栈以及记录已访问位置的集合。在最坏情况下,递归调用栈的深度为目标单词的长度,因此空间复杂度为 O(l)。另外,记录已访问位置的集合也会消耗额外的空间,其大小最多为网格的大小,即 O(mn)。因此,整体的空间复杂度为 O(mn + l)。
N皇后问题是一个经典的组合问题,旨在找到在N×N的棋盘上放置N个皇后,使得它们互相不受攻击。在国际象棋中,皇后可以沿着水平线、垂直线和对角线移动,因此在解决N皇后问题时,需要确保任何两个皇后都不在同一行、同一列或同一对角线上。
这个问题是一个NP难问题,在一般情况下没有有效的多项式时间算法来解决。通常采用回溯算法来解决N皇后问题。回溯算法是一种深度优先搜索算法,它尝试每一种可能的解决方案,并在遇到无效解时进行回溯,尝试其他的路径。
N皇后问题的解决方案通常包括以下步骤:
N皇后问题的解决方案数量随着N的增加呈指数级增长,因此对于较大的N,求解可能会非常耗时。对于较小的N,可以通过回溯算法在合理的时间内找到所有解决方案。
代码尝试:
def solve_n_queens(n):
def is_safe(row, col, queens):
# queues is the list for all the quenes that has been added
# check if put a new queue at (row, col) is safe
for r, c in queens:
if row == r or col == c or abs(row - r) == abs(col - c):
return False
return True
def backtracking(row, queens):
# check from rowth
if row == n:
res.append(queens[:])
return
for col in range(n):
if is_safe(row, col, queens):
queens.append((row, col))
backtracking(row + 1, queens)
queens.pop()
res = []
backtracking(0, [])
return len(res)
给出的题解参考和我上述的方法类似,使用回溯试错重来。
def is_valid_move(proposed_row, proposed_col, solution):
for i in range(0, proposed_row):
old_row = i
old_col = solution[i]
diagonal_offset = proposed_row - old_row
if (old_col == proposed_col or
old_col == proposed_col - diagonal_offset or
old_col == proposed_col + diagonal_offset):
return False
return True
def solve_n_queens_rec(n, solution, row, results):
if row == n:
results.append(solution[:])
return
for i in range(0, n):
valid = is_valid_move(row, i, solution)
if valid:
solution[row] = i
solve_n_queens_rec(n, solution, row + 1, results)
# Function to solve N-Queens problem
def solve_n_queens(n):
results = []
solution = [-1] * n
solve_n_queens_rec(n, solution, 0, results)
return len(results)
学习笔记:
is_safe
函数中,需要遍历已经放置的皇后位置,时间复杂度为 O(n)。queens
,其中保存了当前已经放置的皇后的位置。这些列表的长度最大为 n,因此递归过程中的栈空间最大为 O(n)。总的来说,这个算法的时间复杂度是 O(n^3),空间复杂度是 O(n^2 x n! + n)。虽然时间复杂度相对较高,但是由于 N 皇后问题的规模通常较小(通常 n 值不会很大),因此这个算法在实践中是可行的。