leetcode hot100 79. 单词搜索 medium 递归回溯


切片回溯(时间超出限制)

bash 复制代码
tmp | {(i,j)}

| 是 Python 集合的并集运算符,表示把 tmp 和 {(i,j)} 合并成一个新的集合

bash 复制代码
class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        m = len(board)
        n = len(board[0])

        def backtrack(i, j, word, tmp): #(word待选列表,已访问过格子的坐标)

            # 匹配完所有字符
            if not word:
                return True

            if i < 0 or i >= m or j < 0 or j >= n:
                return False
            if (i, j) in tmp:
                return False
            if board[i][j] != word[0]:
                return False

            # 四个方向搜索
            for dx, dy in [(-1,0),(1,0),(0,-1),(0,1)]:
                if backtrack(i+dx, j+dy, word[1:], tmp | {(i, j)}):
                    return True

            return False

        # 遍历每个起点
        for i in range(m):
            for j in range(n):
                if backtrack(i, j, word, set()):
                    return True

        return False

代码在每一层递归都做了两件"昂贵"的事:

  1. word[1:]:复制字符串,O(L)O(L)O(L)
  2. tmp | {(i, j)}:复制集合,O(L)O(L)O(L)

时间复杂度:O(M⋅N⋅3L⋅L)O(M \cdot N \cdot 3^L \cdot L)O(M⋅N⋅3L⋅L)
M,NM, NM,N 为网格大小,LLL 为单词长度。

  1. 额外的 LLL 开销:字符串切片:word[1:] 每次都会创建一个长度为 L−1L-1L−1 的新字符串
  2. 集合拷贝:tmp | {(i, j)} 每次都会复制当前集合中的所有坐标。集合大小最大为 LLL,复制操作耗时 O(L)O(L)O(L)。
  3. 对比:如果用"原地修改字符"的方法,这部分 O(L)O(L)O(L) 的额外开销可以降到 O(1)O(1)O(1)。空间复杂度:O(L2)O(L^2)O(L2)

空间复杂度:O(L2)O(L^2)O(L2)

  1. 递归栈深度:O(L)O(L)O(L)
  2. 堆内存占用:由于每一层都创建了新的 set 和 string,在最深的一条搜索路径上,内存中同时存在着长度分别为 1,2,...,L1, 2, \dots, L1,2,...,L 的集合。总空间消耗约为 1+2+⋯+L=O(L2)1+2+\dots+L = O(L^2)1+2+⋯+L=O(L2)

切片回溯(优化,不超时间)

为什么是if k== len(word):,不是if k== len(word)-1:
k 的语义是:"下一个要匹配的位置"

k== len(word):

已经没有下一个字符需要匹配了,之前所有字符都匹配成功

不标记会走回头路!

coffeescript 复制代码
tmp = board[i][j]
board[i][j] = '#'

暂时把当前格子变成一个"不可能匹配任何字符"的值,防止下一层递归再次走回这个格子

为什么必须"回溯恢复"?

coffeescript 复制代码
board[i][j] = tmp

那后面从别的起点开始搜索时,这个格子永远是 #,会破坏搜索。

bash 复制代码
class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        m = len(board)
        n = len(board[0])
        word_len = len(word)


        def backtrack(i, j, k): # k 代表当前匹配到 word 的第几个索引

            # k 到达最后一位且匹配成功
            if k == word_len:
                return True

            if i < 0 or i >= m or j < 0 or j >= n:   # 往上下左右搜,出界
                return False

            if board[i][j] != word[k]: # 往上下左右搜,匹配不到下一个word字母
                return False
            
            # 标记访问(原地修改)
            tmp = board[i][j]
            board[i][j] = '#'

            # 四个方向搜索  
            for dx, dy in [(-1,0),(1,0),(0,-1),(0,1)]:  # 往上下左右搜
                if backtrack(i+dx, j+dy, k+1):  # 搜到了
                    return True

            # 走到这里,是上下左右,都没搜到

            # 撤销标记,回溯恢复
            board[i][j] = tmp
            return False

        # 遍历每个起点
        for i in range(m):
            for j in range(n):
                # 只有首字母匹配才开始搜索
                if board[i][j] == word[0]:  
                    if backtrack(i, j, 0):
                        return True

        return False

为什么这样就不会超时?

  1. 指针替代切片:通过 k 传递索引,不再产生新字符串。
  2. 原地修改替代集合:直接在 board 上改字符(标记为 #),不再创建和拷贝set, 修改一个字符的时间复杂度是 O(1)O(1)O(1)
  3. 短路返回:if backtrack(...): return
    True。一旦找到答案,递归会像多米诺骨牌一样迅速结束,不再继续无谓的搜索。

递归调用栈变化

  1. 成功路径时栈一层层 return
  2. 失败路径时逐层撤销

成功路径时栈一层层 return,没有执行恢复操作

因为一旦找到答案,搜索立刻终止,后续路径不再执行

coffeescript 复制代码
backtrack(0,0,k=0)
    标记 A → '#'
    ↓
    backtrack(0,1,k=1)
        标记 B → '#'
        ↓
        backtrack(0,2,k=2)
            标记 C → '#'
            ↓
            backtrack(..., k=3)
                k == len(word)
                return True

失败路径时逐层撤销

coffeescript 复制代码
backtrack(A)
    标记 A
    ↓
    backtrack(B)
        标记 B
        ↓
        尝试四个方向
            全部失败
        ↓
        恢复 B
        return False
相关推荐
Rhystt3 小时前
代码随想录第二十六天|669. 修剪二叉搜索树、108.将有序数组转换为二叉搜索树、538.把二叉搜索树转换为累加树
数据结构·c++·算法·leetcode
TracyCoder1234 小时前
LeetCode Hot100(57/100)——5. 最长回文子串
算法·leetcode·职场和发展
WZ188104638694 小时前
LeetCode第20题
算法·leetcode
吕司5 小时前
LeetCode Hot Code——三数之和
数据结构·算法·leetcode
YGGP5 小时前
【Golang】LeetCode 54. 螺旋矩阵
算法·leetcode·矩阵
TracyCoder1235 小时前
LeetCode Hot100(58/100)——138. 随机链表的复制
leetcode·链表
Frostnova丶6 小时前
LeetCode 868. 二进制间距
算法·leetcode
WZ188104638696 小时前
LeetCode第54题
算法·leetcode
丰海洋6 小时前
Leetcode-hot100-136只出现一次的数字
算法·leetcode·职场和发展