力扣Hot100系列24(Java)——[回溯]总结(下)(括号生成,单词搜索,分割回文串)

文章目录


前言

本文记录力扣Hot100里面关于回溯的四道题,包括常见解法和一些关键步骤理解,也有例子便于大家理解


一、括号生成

1.题目

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3

输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

输入:n = 1

输出:["()"]

提示:

  • 1 <= n <= 8

2.代码

java 复制代码
class Solution {
    // 生成n对有效括号
    public List<String> generateParenthesis(int n) {
        List<String> ans = new ArrayList<>();
        // 回溯:结果、当前字符串、左括号数、右括号数、最大对数n
        backtrack(ans, new StringBuilder(), 0, 0, n);
        return ans;
    }

    // 回溯函数
    void backtrack(List<String> ans, StringBuilder cur, int open, int close, int max) {
        // 终止:字符串长度达到2n,加入结果
        if (cur.length() == max * 2) {
            ans.add(cur.toString());
            return;
        }
        // 左括号没满,添加左括号
        if (open < max) {
            cur.append('(');
            backtrack(ans, cur, open + 1, close, max);
            cur.deleteCharAt(cur.length() - 1); // 回溯撤销
        }
        // 右括号少于左括号,添加右括号
        if (close < open) {
            cur.append(')');
            backtrack(ans, cur, open, close + 1, max);
            cur.deleteCharAt(cur.length() - 1); // 回溯撤销
        }
    }
}

3.例子

n=3 为例:

生成 3对有效括号

正确答案:
["((()))", "(()())", "(())()", "()(())", "()()()"]


  1. 左括号 ( :只要少于3个,就能加
  2. 右括号 ) :必须少于左括号,才能加
  3. 长度到 6 就是一个答案

初始状态:
cur = ""open=0close=0


路径 1:一直加左,直到加满

  1. open < 3 → 加 (
    cur=(, open=1, close=0

  2. open < 3 → 加 (
    cur=((, open=2, close=0

  3. open < 3 → 加 (
    cur=(((, open=3, close=0

左括号满了,只能加右括号

  1. close < open → 加 )
    cur=((()

  2. close < open → 加 )
    cur=((())

  3. close < open → 加 )
    cur=((()))

长度=6 → 加入答案!


开始回溯(撤销,尝试另一条路)

删掉最后一个 )((())

再删 → ((()

再删 → ((

回到这里:cur=( (open=2close=0


路径 2:左2个 → 加右,再加左

  1. close < open → 加 )
    cur=((),open=2, close=1

  2. open < 3 → 加 (
    cur=(()(,open=3, close=1

  3. close < open → 加 )
    cur=(())

  4. close < open → 加 )
    cur=(()())

长度=6 → 加入答案!


再回溯

一直撤销,回到:cur=( (open=2close=0

继续走:加右 → 再加右


路径 3:(()) 再加左

  1. cur=(()),open=2, close=2

  2. open <3 → 加 (
    cur=(())(

  3. )
    cur=(())()
    加入答案!


再回溯,回到最开始:cur=open=1

路径 4:先 ()(( ))

  1. cur=(),open=1, close=1

  2. (()(

  3. (()((

  4. )()(())
    加入答案!


路径 5:()()()

  1. 一路交替加
    ()()()
    加入答案!

最终结果

复制代码
["((()))", "(()())", "(())()", "()(())", "()()()"]

注意:

  1. 左括号能加就加
  2. 右括号必须比左少才能加
  3. 一条路走到底 → 记录答案 → 回头撤销 → 走另一条路

二、单词搜索

1.题目

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

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

示例 1:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCCED"

输出:true

示例 2:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "SEE"

输出:true

示例 3:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCB"

输出:false

提示:

  • m == board.length
  • n = board[i].length
  • 1 <= m, n <= 6
  • 1 <= word.length <= 15
  • board 和 word 仅由大小写英文字母组成

2.代码

java 复制代码
class Solution  {
    public boolean exist(char[][] board, String word) {
        int rows = board.length;    // 矩阵的行数
        int cols = board[0].length; // 矩阵的列数
        boolean[][] visited = new boolean[rows][cols]; // 标记矩阵元素是否被访问过

        // 遍历矩阵的每个位置,作为DFS的起始点
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                // 若当前位置字符DFS后能找到匹配的答案,直接返回true
                if (dfs(board, word, i, j, 0, visited)) {
                    return true; 
                }
            }
        }
        // 所有位置都尝试过,未找到匹配路径
        return false;
    }
    private boolean dfs(char[][] board, String word, int row, int col, int index, boolean[][] visited) {
        // 终止条件:单词所有字符匹配完成
        if (index == word.length()) {
            return true;
        }

        int rows = board.length;
        int cols = board[0].length;
        // 无效情况判断:越界、字符不匹配、已访问过 → 此路径无效
        if (row < 0 || row >= rows || col < 0 || col >= cols 
                || board[row][col] != word.charAt(index) 
                || visited[row][col]) {
            return false;
        }

        // 标记当前位置为已访问,避免同一路径重复使用
        visited[row][col] = true;

        // 依次尝试向上、下、左、右四个方向递归探索
        boolean up = dfs(board, word, row - 1, col, index + 1, visited);    // 向上
        boolean down = dfs(board, word, row + 1, col, index + 1, visited);  // 向下
        boolean left = dfs(board, word, row, col - 1, index + 1, visited);  // 向左
        boolean right = dfs(board, word, row, col + 1, index + 1, visited); // 向右

        // 只要有一个方向匹配成功,整体结果为true
        boolean found = up || down || left || right;

        // 回溯:撤销当前位置的访问标记,供其他路径复用
        visited[row][col] = false;

        return found;
    }
}

3.例子

输入信息

复制代码
board = [
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]
word = "ABCCED"

单词下标:

0:A, 1:B, 2:C, 3:C, 4:E, 5:D


1. 遍历起点,找到 (0,0) 字符为 A

进入DFS,index = 0

字符匹配,标记 visited[0][0] = true

向上下左右搜索下一个字符 index=1(B)

2. 从 (0,0) 向右走到 (0,1)

字符为 B,匹配 index=1

标记 visited[0][1] = true

搜索下一个字符 index=2(C)

3. 从 (0,1) 向右走到 (0,2)

字符为 C,匹配 index=2

标记 visited[0][2] = true

搜索下一个字符 index=3(C)

4. 从 (0,2) 向下走到 (1,2)

字符为 C,匹配 index=3

标记 visited[1][2] = true

搜索下一个字符 index=4(E)

5. 从 (1,2) 向下走到 (2,2)

字符为 E,匹配 index=4

标记 visited[2][2] = true

搜索最后一个字符 index=5(D)

6. 从 (2,2) 向左走到 (2,1)

字符为 D,匹配 index=5

标记 visited[2][1] = true

7. 进入下一层递归

此时 index = 6,等于单词长度,返回 true


最终路径

复制代码
(0,0)A → (0,1)B → (0,2)C → (1,2)C → (2,2)E → (2,1)D

匹配成功,返回 true


回溯逻辑说明

  1. 每走到一个格子,先标记为已访问
  2. 向四个方向递归查找下一个字符
  3. 无论查找成功或失败,都会取消当前标记,恢复状态,供其他路径使用

三、分割回文串

1.题目

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

示例 1:

输入:s = "aab"

输出:[["a","a","b"],["aa","b"]]

示例 2:

输入:s = "a"

输出:[["a"]]

提示:

  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成

2.代码

java 复制代码
class Solution {
    boolean[][] f;         // f[i][j]:s[i..j] 是否是回文串
    List<List<String>> ret = new ArrayList<>(); // 最终结果
    List<String> ans = new ArrayList<>();       // 当前分割方案
    int n;                 // 字符串长度

    public List<List<String>> partition(String s) {
        n = s.length();
        f = new boolean[n][n];
        
        // 初始化:所有单个字符都是回文
        for (int i = 0; i < n; ++i) {
            Arrays.fill(f[i], true);
        }

        // DP 预处理:快速判断任意子串是不是回文
        for (int i = n - 1; i >= 0; --i) {
            for (int j = i + 1; j < n; ++j) {
                f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
            }
        }

        dfs(s, 0); // 回溯搜索所有分割方案
        return ret;
    }
 
    // 回溯:从位置 i 开始分割
    void dfs(String s, int i) {
        if (i == n) {         // 分割到末尾,记录方案
            ret.add(new ArrayList<>(ans));
            return;
        }
        // 尝试所有可能的结束位置 j
        for (int j = i; j < n; ++j) {
            if (f[i][j]) {    // 如果 s[i..j] 是回文
                ans.add(s.substring(i, j + 1)); // 选它
                dfs(s, j + 1);                  // 继续分割后面
                ans.remove(ans.size() - 1);     // 回溯撤销
            }
        }
    }
} 

3.例子

以 **s = "aab"**为例

输入

s = "aab"

字符下标:0:a,1:a,2:b

n = 3


第一步:初始化 DP 数组 f

复制代码
f = new boolean[3][3];

执行:

复制代码
for (int i = 0; i < 3; i++)
    Arrays.fill(f[i], true);

结果:

复制代码
f[0][0] = true   f[0][1] = true   f[0][2] = true
f[1][0] = true   f[1][1] = true   f[1][2] = true
f[2][0] = true   f[2][1] = true   f[2][2] = true

第二步:DP 预处理(i 倒序,j 正序)

复制代码
for (i = 2; i >= 0; i--)
    for (j = i+1; j < 3; j++)
        f[i][j] = (s[i]==s[j]) && f[i+1][j-1];

运行过程:

i=2

j 从 3 开始,无循环

i=1

j=2

s[1] = a

s[2] = b

不相等

f[1][2] = false

i=0

j=1

s[0]=a,s[1]=a 相等

f[1][0] 为 true

所以 f[0][1] = true

j=2

s[0]=a,s[2]=b 不相等

f[0][2] = false

最终 f 数组:

复制代码
f[0][0] = true
f[0][1] = true
f[0][2] = false

f[1][1] = true
f[1][2] = false

f[2][2] = true

第三步:DFS 回溯搜索分割方案

从 i=0 开始


第一层:i=0

遍历 j=0、1、2

j=0

f[0][0] = true

把 s[0:0] = "a" 加入 ans

ans = ["a"]

递归进入 i=1


第二层:i=1

遍历 j=1、2

j=1

f[1][1] = true

加入 s[1:1] = "a"

ans = ["a","a"]

递归进入 i=2


第三层:i=2

遍历 j=2

j=2

f[2][2] = true

加入 "b"

ans = ["a","a","b"]

递归进入 i=3

i == n → 把 ans 加入结果

得到第一个方案:["a","a","b"]


回溯:

删除 "b"

回到 i=2,结束

删除 "a"

回到 i=1


回到 i=1,j=2

f[1][2] = false → 跳过


回溯到 i=0

删除 "a"

ans = []

继续 j=1

j=1

f[0][1] = true

加入 s[0:1] = "aa"

ans = ["aa"]

递归进入 i=2


进入 i=2

j=2

f[2][2] = true

加入 "b"

ans = ["aa","b"]

i=3 → 加入结果

得到第二个方案:["aa","b"]


最终结果

复制代码
[["a","a","b"],["aa","b"]]

总结

  1. DP 预处理:提前算出所有区间是不是回文
  2. DFS:从 i 开始,尝试所有能切的 j
  3. 回溯:加入 → 递归 → 删除,尝试所有可能
  4. 最终得到所有合法分割方案

四、红皇后

1.题目

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

示例 1:

输入:n = 4

输出:[[".Q...","...Q","Q...","...Q."],["...Q.","Q...","...Q",".Q..."]]

解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

输入:n = 1

输出:[["Q"]]

提示:

  • 1 <= n <= 9

2.代码

java 复制代码
class Solution {
    // 存储所有合法的N皇后摆放方案结果集
    private List<List<String>> res = new ArrayList<>();
    
    public List<List<String>> solveNQueens(int n) {
        // 初始化n×n的字符数组棋盘,char数组默认值为'\u0000'(空字符)
        char[][] board = new char[n][n];
        // 遍历每一行,将整行填充为'.'(表示空位)
        for (char[] row : board) {
            Arrays.fill(row, '.'); // 利用Arrays.fill快速填充数组元素
        }
        // 从第0行开始回溯,尝试摆放皇后
        backtrack(board, 0);
        return res;
    }

    private void backtrack(char[][] board, int row) {
        int n = board.length; // 棋盘的边长(皇后数量)
        // 终止条件:当处理完所有行(row == n),说明找到一个合法方案
        if (row == n) {
            // 将字符数组表示的棋盘转换为List<String>格式,存入结果集
            List<String> validBoard = new ArrayList<>();
            for (char[] r : board) {
                validBoard.add(new String(r)); // 字符数组转字符串
            }
            res.add(validBoard); // 加入结果集
            return; // 结束当前递归分支,返回上一层
        }

        // 遍历当前行的每一列(0~n-1),尝试在该列摆放皇后
        for (int col = 0; col < n; col++) {
            // 检查当前位置(row, col)是否可以安全摆放皇后(无冲突)
            if (isValid(board, row, col)) {
                // 做出选择:在(row, col)位置放置皇后(标记为'Q')
                board[row][col] = 'Q';
                // 递归处理下一行的皇后摆放
                backtrack(board, row + 1);
                // 回溯:撤销选择,将(row, col)位置恢复为'.'(空位)
                board[row][col] = '.';
            }
        }
    }

    //辅助方法:检查指定位置(row, col)是否可以安全摆放皇后
    private boolean isValid(char[][] board, int row, int col) {
        int n = board.length; // 棋盘边长
        // 1. 检查当前列是否已有皇后(遍历当前行上方的所有行)
        for (int i = 0; i < row; i++) {
            if (board[i][col] == 'Q') {
                return false; // 同一列存在皇后,冲突
            }
        }
        // 2. 检查左上方对角线(行号递减,列号递减,直到边界)
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] == 'Q') {
                return false; // 冲突
            }
        }
        // 3. 检查右上方对角线(行号递减,列号递增,直到边界)
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (board[i][j] == 'Q') {
                return false; // 冲突
            }
        }
        // 所有攻击方向均无皇后,位置安全
        return true;
    }
}

3.例子

下面用n=4为例子

初始状态

n=4,棋盘初始化为4×4,全部为.

复制代码
. . . .
. . . .
. . . .
. . . .

调用 backtrack(board, 0)


第1层递归:row = 0

遍历列 0、1、2、3

尝试 col = 0

isValid 检查通过,放置 Q

复制代码
Q . . .
. . . .
. . . .
. . . .

进入下一层:backtrack(board, 1)


第2层递归:row = 1

遍历列 0、1、2、3

  • col=0:同列有 Q,不合法

  • col=1:对角线冲突,不合法

  • col=2:合法,放置 Q

    Q . . .
    . . Q .
    . . . .
    . . . .

进入下一层:backtrack(board, 2)


第3层递归:row = 2

遍历所有列

所有列均被攻击,无合法位置

回溯,撤销 row=1,col=2 的 Q

回到 row=1,继续尝试下一列

尝试 col=3

合法,放置 Q

复制代码
Q . . .
. . . Q
. . . .
. . . .

进入下一层:backtrack(board, 2)


第3层递归:row = 2

遍历列

  • col=0:冲突

  • col=1:合法,放置 Q

    Q . . .
    . . . Q
    . Q . .
    . . . .

进入下一层:backtrack(board, 3)


第4层递归:row = 3

遍历所有列

均被攻击,无合法位置

逐层回溯,撤销所有 Q,回到 row=0


回到 row=0,尝试 col=1

放置 Q

复制代码
. Q . .
. . . .
. . . .
. . . .

进入 row=1


row=1 遍历列

找到合法位置 col=3,放置 Q

复制代码
. Q . .
. . . Q
. . . .
. . . .

进入 row=2


row=2 遍历列

找到合法位置 col=0,放置 Q

复制代码
. Q . .
. . . Q
Q . . .
. . . .

进入 row=3


row=3 遍历列

找到合法位置 col=2,放置 Q

复制代码
. Q . .
. . . Q
Q . . .
. . Q .

进入下一层:backtrack(board, 4)

此时 row == n,满足终止条件

将当前棋盘加入结果集,得到第一个解。


继续回溯、尝试

代码会继续撤销、尝试其他列,最终找到第二个解:

复制代码
. . Q .
Q . . .
. . . Q
. Q . .

最终结果

4皇后问题共有两组解,代码执行结束后返回这两个方案。


总结

好啦,到现在这篇文章为止,hot100系列已经正式结束了,感谢大家的支持呀!如果对您有帮助,可以点赞收藏评论哦!也可以关注主包,主包以后也会分享其他方面的知识呀!!!

相关推荐
升鲜宝供应链及收银系统源代码服务2 小时前
生鲜配送供应链管理系统源代码之升鲜宝社区团购商城小程序(一)
java·前端·数据库·小程序·notepad++·供应链系统源代码·多门店收银系统
tankeven2 小时前
HJ150 全排列
c++·算法
Q741_1472 小时前
每日一题 力扣 2946. 循环移位后的矩阵相似检查 力扣 155. 最小栈 数学 数组 模拟 C++ 题解
c++·算法·leetcode·矩阵·模拟·数组·
墨香幽梦客2 小时前
大数据环境下的BI架构:Hadoop与Spark的企业级应用整理
java·开发语言
handsomethefirst2 小时前
【算法与数据结构】【面试经典150题】【题41-题45】
数据结构·算法·leetcode
2301_810160952 小时前
C++中的状态模式
开发语言·c++·算法
xrgs_shz2 小时前
图像的点运算(线性点运算和非线性点运算)
人工智能·算法·机器学习
lulu12165440782 小时前
IDEA+Claude Code智能辅助:保姆级高效开发教程
java·人工智能·intellij-idea·ai编程
曹牧2 小时前
Java:解析Json字符串格式要求
java·linux·运维·前端