LeetCode 中等难度 | 回溯法进阶题解:单词搜索 & 分割回文串

目录

[一、单词搜索(LeetCode 79)](#一、单词搜索(LeetCode 79))

题目描述

解题思路

[Java 代码实现](#Java 代码实现)

复杂度分析

[易错点 & 优化点](#易错点 & 优化点)

[二、分割回文串(LeetCode 131)](#二、分割回文串(LeetCode 131))

题目描述

解题思路

[Java 代码实现](#Java 代码实现)

复杂度分析

[易错点 & 优化点](#易错点 & 优化点)

三、回溯法进阶技巧总结


一、单词搜索(LeetCode 79)

题目描述

给定一个 m x n 二维字符网格 board 和一个字符串单词 word。如果 word 存在于网格中,返回 true;否则,返回 false

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中 "相邻" 单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

plaintext

复制代码
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

解题思路

这道题的核心是DFS + 回溯,本质是在二维网格中进行深度优先搜索,寻找符合条件的路径:

  1. 遍历起点:遍历网格中的每个单元格,若单元格字符与单词首字符相同,则以该单元格为起点开始 DFS;
  2. DFS 过程:从当前单元格出发,向上下左右四个方向搜索,匹配单词的下一个字符;
  3. 标记访问 :为避免重复使用单元格,在访问时将单元格临时标记为'#'(或其他特殊字符),回溯时恢复;
  4. 终止条件
    • 若当前匹配的字符索引等于单词长度,说明已找到完整单词,返回true
    • 若单元格越界、字符不匹配或已被访问,返回false

Java 代码实现

java

运行

复制代码
public class WordSearch {
    public boolean exist(char[][] board, String word) {
        int rows = board.length;
        int cols = board[0].length;
        // 遍历网格中的每个单元格作为起点
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                // 若起点字符与单词首字符相同,开始DFS
                if (board[i][j] == word.charAt(0) && dfs(board, i, j, word, 0)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * DFS搜索单词路径
     * @param board 二维网格
     * @param i 当前行索引
     * @param j 当前列索引
     * @param word 目标单词
     * @param index 当前匹配的单词字符索引
     * @return 是否找到单词路径
     */
    private boolean dfs(char[][] board, int i, int j, String word, int index) {
        // 终止条件:匹配到单词末尾,返回true
        if (index == word.length()) {
            return true;
        }
        // 终止条件:越界、字符不匹配或已被访问,返回false
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length 
            || board[i][j] != word.charAt(index)) {
            return false;
        }

        // 标记当前单元格为已访问(临时修改为特殊字符)
        char temp = board[i][j];
        board[i][j] = '#';

        // 向上下左右四个方向递归搜索
        boolean found = dfs(board, i + 1, j, word, index + 1)
                     || dfs(board, i - 1, j, word, index + 1)
                     || dfs(board, i, j + 1, word, index + 1)
                     || dfs(board, i, j - 1, word, index + 1);

        // 回溯:恢复当前单元格的字符
        board[i][j] = temp;
        return found;
    }
}

复杂度分析

  • 时间复杂度:O(m×n×4L),其中m和n是网格的行数和列数,L是单词长度。每个单元格作为起点,最多有 4 个方向的分支,递归深度为L;
  • 空间复杂度:O(L),递归栈的深度最多为单词长度L。

易错点 & 优化点

  1. 访问标记:必须临时修改单元格字符标记访问,回溯时恢复,否则会影响后续搜索;
  2. 剪枝优化 :若当前字符不匹配,直接返回false,无需继续搜索;
  3. 提前终止 :找到完整单词后,直接返回true,避免不必要的搜索。

二、分割回文串(LeetCode 131)

题目描述

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。回文串是正着读和反着读都一样的字符串。

示例:

plaintext

复制代码
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

解题思路

这道题的核心是回溯 + 预处理回文判断,通过枚举分割点,生成所有可能的回文分割方案:

  1. 预处理回文 :先用动态规划预处理字符串,记录任意子串s[i..j]是否为回文串,避免每次递归都判断回文,提升效率;
  2. 回溯枚举分割点 :从字符串s的起始位置出发,枚举每个位置作为分割点,判断子串s[start..i]是否为回文;
  3. 递归与回溯:若子串是回文,将其加入当前路径,递归处理剩余子串;递归结束后,回溯移除路径中的子串;
  4. 终止条件:当分割点到达字符串末尾时,将当前路径加入结果集。

Java 代码实现

java

运行

复制代码
import java.util.ArrayList;
import java.util.List;

public class PartitionPalindrome {
    List<List<String>> result = new ArrayList<>();
    List<String> path = new ArrayList<>();
    boolean[][] isPalindrome; // 预处理回文数组

    public List<List<String>> partition(String s) {
        int n = s.length();
        // 预处理:判断所有子串是否为回文
        isPalindrome = new boolean[n][n];
        preprocessPalindrome(s, n);
        // 回溯枚举分割点
        backtrack(s, 0);
        return result;
    }

    /**
     * 预处理回文子串
     * @param s 原字符串
     * @param n 字符串长度
     */
    private void preprocessPalindrome(String s, int n) {
        // 单个字符都是回文
        for (int i = 0; i < n; i++) {
            isPalindrome[i][i] = true;
        }
        // 长度为2的子串
        for (int i = 0; i < n - 1; i++) {
            isPalindrome[i][i + 1] = (s.charAt(i) == s.charAt(i + 1));
        }
        // 长度大于2的子串
        for (int len = 3; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 1;
                isPalindrome[i][j] = (s.charAt(i) == s.charAt(j)) && isPalindrome[i + 1][j - 1];
            }
        }
    }

    /**
     * 回溯枚举分割点
     * @param s 原字符串
     * @param start 当前分割的起始位置
     */
    private void backtrack(String s, int start) {
        // 终止条件:分割点到达字符串末尾,保存当前路径
        if (start == s.length()) {
            result.add(new ArrayList<>(path));
            return;
        }

        // 枚举分割点,从start到s.length()-1
        for (int i = start; i < s.length(); i++) {
            // 若子串s[start..i]是回文串
            if (isPalindrome[start][i]) {
                // 做选择:将回文子串加入路径
                path.add(s.substring(start, i + 1));
                // 递归处理剩余子串(分割点更新为i+1)
                backtrack(s, i + 1);
                // 回溯:移除路径中的最后一个子串
                path.remove(path.size() - 1);
            }
        }
    }
}

复杂度分析

  • 时间复杂度:O(n×2n),其中n是字符串长度。预处理回文的时间为O(n2),回溯过程中,字符串有2n−1种分割方式,每种方式需要O(n)时间生成子串;
  • 空间复杂度:O(n2+n),预处理回文数组占用O(n2)空间,递归栈深度和路径存储的最大长度为O(n)。

易错点 & 优化点

  1. 预处理回文:动态规划预处理可将回文判断的时间复杂度从O(n)降至O(1),大幅提升整体效率;
  2. 分割点枚举 :必须从start开始枚举分割点,避免重复分割;
  3. 路径拷贝 :将路径加入结果集时,必须创建新的ArrayList,否则后续修改会影响结果。

三、回溯法进阶技巧总结

这两道题让我们看到了回溯法的更多应用场景,也掌握了两个进阶技巧:

  1. 结合 DFS:在二维网格、图等结构中,通过 DFS 实现回溯,标记访问状态避免重复;
  2. 预处理优化:对于重复判断的条件(如回文),先通过动态规划预处理,将单次判断的时间复杂度降至O(1),减少无效递归。

回溯法的核心依然是 "枚举 + 剪枝",但在不同场景下,需要结合问题特点灵活调整:

  • 网格 / 图搜索:需要标记访问状态,防止循环和重复访问;
  • 字符串分割:可通过预处理减少重复判断,提升效率。
相关推荐
QH_ShareHub2 小时前
反正态分布算法
算法
float_com2 小时前
LeetCode 27. 移除元素
leetcode
王老师青少年编程2 小时前
csp信奥赛c++中的递归和递推研究
c++·算法·递归·递推·csp·信奥赛
Bczheng12 小时前
五.serialize.h中的CDataStream类
算法·哈希算法
小O的算法实验室2 小时前
2025年SEVC,考虑组件共享的装配混合流水车间批量流调度的多策略自适应差分进化算法,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
汀、人工智能2 小时前
[特殊字符] 第36课:柱状图最大矩形
数据结构·算法·数据库架构·图论·bfs·柱状图最大矩形
List<String> error_P3 小时前
蓝桥杯最后冲刺(三)
算法
样例过了就是过了3 小时前
LeetCode热题100 跳跃游戏
c++·算法·leetcode·贪心算法·动态规划
无限进步_3 小时前
【C++&string】寻找字符串中第一个唯一字符:两种经典解法详解
开发语言·c++·git·算法·github·哈希算法·visual studio