在解决算法问题时,我们的第一个想法可能就是暴力法,比如暴力枚举所有可能的结果,但是暴力法的效率太低了,有时候达不到我们的要求。在这里我们介绍一种比暴力法稍微高级一点的算法,回溯法。回溯法是在多个可能的分支中选择一支,如果满足条件则继续查找,如果不满足结果,则回溯到上一个节点。

    假如我们有下图所示结构
    回溯法 - 图1
    我们想在该树形结构中找到 "abc" 的路径,过程如下所示
    回溯法 - 图2

    题目:请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。

    这是一个可以使用回溯法解决的问题。首先判断矩阵的某个位置是否为字符串的第 回溯法 - 图3 个字符,如果是继续向四周查找第 回溯法 - 图4 个字符,并以此类推,直到找到最后一个字符;如果不是第 回溯法 - 图5 个字符,则回溯到上一个位置去查找第 回溯法 - 图6 个字符。因为已经经过了的路径不能重复经过,所以我们使用一个布尔矩阵 visited 来表示是否访问过矩阵中的某个位置。代码如下:

    1. public class Path {
    2. // matrix:字符矩阵
    3. // str:要查找的字符串
    4. public static boolean hasPath(char[][] matrix, char[] str) {
    5. // 对输入参数合法性的判断
    6. if (matrix == null || matrix.length < 1 || matrix[0].length < 1) {
    7. return false;
    8. }
    9. // 获得矩阵的行数和列数
    10. int rows = matrix.length;
    11. int columns = matrix[0].length;
    12. // 定义一个布尔矩阵,用以确定矩阵某个元素已经被访问过
    13. boolean[][] vistied = new boolean[rows][columns];
    14. // 初始化布尔矩阵
    15. for (int row = 0; row < rows; row++) {
    16. for (int column = 0; column < columns; column++) {
    17. vistied[row][column] = false;
    18. }
    19. }
    20. // pathLength表示以匹配的字符串的长度
    21. int pathLength = 0;
    22. // 遍历矩阵进行匹配
    23. for (int row = 0; row < rows; row++) {
    24. for (int column = 0; column < columns; column++) {
    25. if (hasPathCore(matrix, row, column, str, pathLength, vistied)) {
    26. return true;
    27. }
    28. }
    29. }
    30. return false;
    31. }
    32. // 判断位置 (row, column) 是否有第 pathLength 个字符后的路径
    33. public static boolean hasPathCore(char[][] martix, int row, int column, char[] str, int pathLength, boolean[][] visited) {
    34. // pathLength 表示以匹配的字符的个数,如果个数大于等于(其实这里等于就可以)字符串的长度,则匹配成功
    35. if (pathLength >= str.length) {
    36. return true;
    37. }
    38. // 后续是否匹配
    39. boolean hasPath = false;
    40. // 如果该位置匹配,且该位置在矩阵内
    41. if (row >= 0 && row < martix.length && column >= 0 && column < martix[0].length && martix[row][column] == str[pathLength] && !visited[row][column]) {
    42. // 匹配长度+1
    43. pathLength++;
    44. // 设置为已访问
    45. visited[row][column] = true;
    46. // 在四周进行匹配
    47. hasPath = hasPathCore(martix, row, column - 1, str, pathLength, visited)
    48. || hasPathCore(martix, row - 1, column, str, pathLength, visited)
    49. || hasPathCore(martix, row + 1, column, str, pathLength, visited)
    50. || hasPathCore(martix, row, column + 1, str, pathLength, visited);
    51. // 如果四周的路径都不匹配,进行回溯,匹配的长度和已访问设置为false
    52. if (!hasPath) {
    53. pathLength--;
    54. visited[row][column] = false;
    55. }
    56. }
    57. return hasPath;
    58. }
    59. public static void main(String[] args) {
    60. char[][] matrix = {
    61. {'a', 'b', 't', 'g'},
    62. {'c', 'f', 'c', 's'},
    63. {'j', 'd', 'e', 'h'}
    64. };
    65. char[] str = {'t', 'c', 'f', 'd', 'j'};
    66. System.out.println(hasPath(matrix, str)); // true
    67. }
    68. }

    题目:地上有一个 回溯法 - 图7回溯法 - 图8 列的方格。一个机器人从坐标 回溯法 - 图9#card=math&code=%280%2C%200%29&height=20&width=37) 的格子开始移动,它每次可以向左、右、上、下移动一格,但不能进入行坐标和列坐标的数位之和大于 回溯法 - 图10 的格子。例如当 回溯法 - 图11回溯法 - 图12 时,机器人能进入 回溯法 - 图13#card=math&code=%2835%2C%2037%29&height=20&width=53),因为 回溯法 - 图14。但它不能进入方格 回溯法 - 图15#card=math&code=%2835%2C%2038%29&height=20&width=53),因为 回溯法 - 图16。请问该机器人能够到达多少个格子?

    这道题目和上道题很相似,同样可以使用回溯法进行解决。当机器人进入 回溯法 - 图17 的格子时,判断它是否能进入,如果能进入,则继续判断相邻的四个格子是否能进入。同理,我们也会用一个布尔矩阵来标记是否访问过该矩阵,以防重复统计,完整代码如下:

    1. public class RobotCount {
    2. // threshold:阈值,即题目中的 k 值
    3. // rows, columns 矩阵的行数和列数
    4. public static int movingCount(int threshold, int rows, int columns) {
    5. // 输入参数合法性验证
    6. if (threshold <= 0 || rows <= 0 || columns <= 0) {
    7. return 0;
    8. }
    9. // 布尔矩阵
    10. boolean[][] visited = new boolean[rows][columns];
    11. // 布尔矩阵初始化
    12. for (int row = 0; row < rows; row++) {
    13. for (int column = 0; column < columns; column++) {
    14. visited[row][column] = false;
    15. }
    16. }
    17. // 从坐标 (0, 0) 开始移动
    18. return movingCountCore(threshold, rows, columns, 0, 0, visited);
    19. }
    20. // 从坐标 (row, column) 开始移动能够访问的格子数
    21. private static int movingCountCore(int threshold, int rows, int columns, int row, int column, boolean[][] visited) {
    22. int count = 0;
    23. // 如果能进入坐标 (row, column)
    24. if (check(threshold, rows, columns, row, column, visited)) {
    25. // 标记以访问
    26. visited[row][column] = true;
    27. // 进入相邻的四个格子
    28. count = 1 + movingCountCore(threshold, rows, columns, row - 1, column, visited)
    29. + movingCountCore(threshold, rows, columns, row, column - 1, visited)
    30. + movingCountCore(threshold, rows, columns, row + 1, column, visited)
    31. + movingCountCore(threshold, rows, columns, row, column + 1, visited);
    32. }
    33. return count;
    34. }
    35. // 判断是否能进入坐标 (row, column)
    36. private static boolean check(int threshold, int rows, int columns, int row, int column, boolean[][] visited) {
    37. if (row >= 0 && row < rows
    38. && column >= 0 && column < columns
    39. && (getDigitalNumber(row) + getDigitalNumber(column)) <= threshold
    40. && !visited[row][column]) {
    41. return true;
    42. }
    43. return false;
    44. }
    45. // 获得数字 n 的数位和
    46. private static int getDigitalNumber(int number) {
    47. int sum = 0;
    48. while (number > 0) {
    49. sum += number % 10;
    50. number = number / 10;
    51. }
    52. return sum;
    53. }
    54. public static void main(String[] args) {
    55. System.out.println(movingCount(10, 100, 100)); // 309
    56. }
    57. }

    总结:

    • 通常在二维矩阵中查找路径这类问题都可以使用回溯法解决。
    • 通常物体或人在二维方格运动这类问题都可以使用回溯法解决。