标签 回溯算法 下的文章

题目描述

地上有⼀个 m ⾏和 n 列的⽅格。⼀个机器⼈从坐标(0,0) 的格⼦开始移动,每⼀次只能向左,右,上,下四个⽅向移动⼀格,但是不能进⼊⾏坐标和列坐标的数位之和⼤于 k 的格⼦。 例如,当k 为 18 时,机器⼈能够进⼊⽅格(35,37) ,因为 3+5+3+7 = 18 。但是,它不能进⼊⽅格(35,38) ,因为 3+5+3+8 = 19 。请问该机器⼈能够达到多少个格⼦?

示例1

输⼊:5,10,10
返回值:21

示例2

输⼊:10,1,100
返回值:29

说明:[0,0],[0,1],[0,2],[0,3],[0,4],[0,5],[0,6],[0,7],[0,8],[0,9],[0,10],[0,11],[0,12],[0,13],[0,14],[0,15],[0,16],[0,17],[0,18],[0,19],[0,20],[0,21],[0,22],[0,23],[0,24],[0,25],[0,26],[0,27],[0,28] 这29种,后⾯的[0,29] , [0,30] 以及[0,31] 等等是⽆法到达的。

思路及解答

DFS(深度优先搜索)

深度优先搜索算法,也就是 DFS ,⾸先需要初始化数组,注意是 boolean 类型的⼆元数组。边初始化
边计算位数的和,判断如果⼤于等于阈值的话,就直接置为 true ,也就是已经被访问到(但是这⼀部分计⼊结果)。

然后遍历每⼀个元素,只要 i , j 不在合法的索引范围或者是已经被访问过,都会直接返回
false 。

否则的话,可访问的数量 +1 ,并且递归遍历上下左右四个元素,返回最终的可访问的个数。

DFS 会优先同⼀个⽅向,⼀直⾛下去,不撞南墙不回头,直到条件不满⾜的时候,才会回头。回头之后,每次只会回头⼀步,往另外⼀个⽅向去,同样是⼀头扎进去。

假设有⼀个 4 x 4 的⽅格,从第⼀个开始遍历,假设遍历顺序是上,右,下,左,那么遍历的顺序如下:

public class Solution {
    public int movingCount(int threshold, int rows, int cols) {
        if (rows > 0 && cols > 0) {
            boolean[][] visited = new boolean[rows][cols];
            for (int i = 0; i < rows; i++) {
                for (int j = 0; j < cols; j++) {
                    // 如果⼤于阈值,设置已被访问过
                    visited[i][j] = ((getSum(i) + getSum(j)) > threshold);
                }
            }
            return getNum(visited, 0, 0, 0);
        }
        return 0;
    }
    
   // 获取可以被访问的个数
   private int getNum(boolean[][] visited, int i, int j, int count) {
        if (i < 0 || j < 0 || i >= visited.length || j >= visited[0].length ||
            visited[i][j]) {
            return count;
        }
        count++;
        visited[i][j] = true;
        count = getNum(visited, i, j + 1, count);
        count = getNum(visited, i, j - 1, count);
        count = getNum(visited, i + 1, j, count);
        count = getNum(visited, i - 1, j, count);
        return count;
   }
   
    // 计算位数之和
   private int getSum(int num) {
        int result = 0;
        while (num > 0) {
            result = result + num % 10;
            num = num / 10;
        }
        return result;
    }
}
  • 时间复杂度:最坏的情况是将所有的格⼦都遍历⼀遍, O(m*n) 。
  • 空间复杂度:借助了额外的空间保存是否被访问过,同样为O(m*n) 。

BFS(⼴度优先搜索)

⼴度优先搜索,也就是没进⾏⼀步,优先搜索当前点的各个⽅向上的点,不急着往下搜索,等搜索完当前点的各个⽅向的点,再依次把之前搜索的点,取出来,同样先搜索周边的点...

这样直到所有都被搜索完成。

同样有⼀个 4 x 4 的⽅格,从第⼀个开始遍历,假设遍历顺序是上,右,下,左,那么遍历的顺序如下:

在上⾯的过程图示中,我们可以发现,访问是有顺序的,每遍历⼀个新的⽅块,都会标⼀个顺序,然后按照顺序遍历其四个⽅向。

这也就是⼴度优先搜索的本质,我们需要⼀个队列,来保存遍历的顺序,每次都从队列⾥⾯取出⼀个位置,遍历其四周的⽅块,每次遍历到的点,都会放到队列⾥⾯,这样直到队列为空的时候,也就是全部遍历完成。

import java.util.LinkedList;
import java.util.Queue;

public class Solution13 {
    public int movingCount(int threshold, int rows, int cols) {
        boolean[][] visited = new boolean[rows][cols];
        int count = 0;
        
        Queue<int[]> queue = new LinkedList<>();
        // 把第⼀个点加到队列⾥⾯
        queue.add(new int[]{0, 0});
        
        while (queue.size() > 0) {
            // ⼀直取数据,直到队列为空
            int[] x = queue.poll();
            // 取出来的数据,包含x,y坐标
            int i = x[0], j = x[1];
            // 如果访问过或者不符合,直接下⼀个
            if (i >= rows || j >= cols || threshold < getSum(i) + getSum(j) || visited[i][j]) continue;
            
            // 置为访问过
            visited[i][j] = true;
            // 数量增加
            count++;
            // 右
            queue.add(new int[]{i + 1, j});
            // 下
            queue.add(new int[]{i, j + 1});
       }
       return count;
   }
   
    // 计算位数之和
   private int getSum(int num) {
        int result = 0;
        while (num > 0) {
            result = result + num % 10;
            num = num / 10;
        }
        return result;
    }
}
  • 时间复杂度:最坏的情况是将所有的格⼦都遍历⼀遍, O(m*n) 。
  • 空间复杂度:借助了额外的空间保存是否被访问过,同样为O(m*n) 。

动态规划(最优解)

利用递推关系式,避免重复计算。

  • 格子(i,j)可达 ⇔ 数位和满足条件 ∧ (左边格子可达 ∨ 上边格子可达)
  • dpi表示(i,j)是否可达,基于左边和上边格子的状态:dp[i][j] = (digitSum(i) + digitSum(j) ≤ k) && (dp[i-1][j] || dp[i][j-1])
public class Solution {
    public int movingCount(int m, int n, int k) {
        if (k == 0) return 1;
        
        // dp[i][j]表示格子(i,j)是否可达
        boolean[][] dp = new boolean[m][n];
        dp[0][0] = true;  // 起点可达
        int count = 1;     // 起点已计入
        
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 跳过起点和数位和超限的情况
                if ((i == 0 && j == 0) || digitSum(i) + digitSum(j) > k) {
                    continue;
                }
                
                // 检查是否可以从左边或上边到达当前格子
                if (i - 1 >= 0) {
                    dp[i][j] |= dp[i - 1][j];  // 从上边来
                }
                if (j - 1 >= 0) {
                    dp[i][j] |= dp[i][j - 1];  // 从左边来
                }
                
                // 如果当前格子可达,计数加1
                count += dp[i][j] ? 1 : 0;
            }
        }
        
        return count;
    }
    
    private int digitSum(int num) {
        int sum = 0;
        while (num > 0) {
            sum += num % 10;
            num /= 10;
        }
        return sum;
    }
}
  • 时间复杂度:O(mn),双重循环遍历所有格子
  • 空间复杂度:O(mn),dp数组的空间

题目描述

请设计⼀个函数,⽤来判断在⼀个矩阵中是否存在⼀条包含某字符串所有字符的路径。路径可以从矩阵中的任意⼀个格⼦开始,每⼀步可以在矩阵中向左,向右,向上,向下移动⼀个格⼦。如果⼀条路径经过了矩阵中的某⼀个格⼦,则该路径不能再进⼊该格⼦。 例如矩阵:

中包含⼀条字符串 " bcced " 的路径,但是矩阵中不包含 " abcb " 路径,因为字符串的第⼀个字符 b占据了矩阵中的第⼀⾏第⼆个格⼦之后,路径不能再次进⼊该格⼦。

示例1

输⼊:[[a,b,c,e],[s,f,c,s],[a,d,e,e]],"abcced"
返回值:true

思路及解答

DFS回溯

主要的思路是对于每⼀个字符为起点,递归向四周拓展,然后遇到不匹配返回 false ,匹配则接着匹配直到完成,⾥⾯包含了 回溯 的思想。步骤如下:

针对每⼀个字符为起点,初始化⼀个和矩阵⼀样⼤⼩的标识数组,标识该位置是否被访问过,⼀开始默认是false 。

  1. 如果当前的字符索引已经超过了字符串⻓度,说明前⾯已经完全匹配成功,直接返回 true
  2. 如果⾏索引和列索引,不在有效的范围内,或者改位置已经标识被访问,直接返回 false
  3. 否则将当前标识置为已经访问过
  4. 如果矩阵当前位置的字符和字符串相等,那么就字符串的索引加⼀,递归判断周边的四个,只要⼀个的结果为 true ,就返回 true ,否则将该位置置为没有访问过(相当于回溯,退回上⼀步),返回 false 。矩阵当前位置的字符和字符串不相等,否则同样也是将该位置置为没有访问过(相当于回溯,退回上⼀步),返回 false 。

⽐如查找 bcced :

public class Solution {
    public boolean hasPath(char[][] matrix, String word) {
        // write code here
        if (matrix == null || word == null || word.length() == 0) {
            return false;
        }
        
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[0].length; j++) {
                boolean[][] flags = new boolean[matrix.length][matrix[0].length];
                
                boolean result = judge(i, j, matrix, flags, word, 0);
                if (result) {
                    return true;
                }
            }
        }
        return false;
   }
    
   public boolean judge(int i, int j, char[][] matrix, boolean[][] flags, String words, int index) {
        if (index >= words.length()) {
            return true;
        }
       
        if (i < 0 || j < 0 || i >= matrix.length || j >= matrix[0].length || flags[i][j]) {
            return false;
        }
        flags[i][j] = true;
        
        if (matrix[i][j] == words.charAt(index)) {
            if (judge(i - 1, j, matrix, flags, words, index + 1)
            || judge(i + 1, j, matrix, flags, words, index + 1)
            || judge(i, j + 1, matrix, flags, words, index + 1)
            || judge(i, j - 1, matrix, flags, words, index + 1)) {
                return true;
            } else {
                flags[i][j] = false;
                return false;
            }
        } else {
            flags[i][j] = false;
            return false;
        }
    }
}
  • 时间复杂度:O(3^k × m × n),其中k为单词长度,m、n为矩阵尺寸。每个点有3个方向可选(不能回退)
  • 空间复杂度:O(k),递归栈深度和visited数组空间

方向数组优化

使用额外的访问标记数组来记录路径状态。

public class Solution {
    public boolean exist(char[][] board, String word) {
        if (board == null || board.length == 0 || board[0].length == 0 || word == null) {
            return false;
        }
        
        int m = board.length, n = board[0].length;
        boolean[][] visited = new boolean[m][n];
        char[] words = word.toCharArray();
        
        // 遍历矩阵中的每个单元格作为起始点
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (dfs(board, visited, words, i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    private boolean dfs(char[][] board, boolean[][] visited, char[] word, int i, int j, int index) {
        // 终止条件1:找到完整路径
        if (index == word.length) {
            return true;
        }
        
        // 终止条件2:越界或已访问或字符不匹配
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || 
            visited[i][j] || board[i][j] != word[index]) {
            return false;
        }
        
        // 标记当前单元格为已访问
        visited[i][j] = true;
        
        // 向四个方向进行DFS搜索
        boolean found = dfs(board, visited, word, i + 1, j, index + 1) ||  // 向下
                       dfs(board, visited, word, i - 1, j, index + 1) ||  // 向上
                       dfs(board, visited, word, i, j + 1, index + 1) ||  // 向右
                       dfs(board, visited, word, i, j - 1, index + 1);    // 向左
        
        // 回溯:恢复访问状态
        visited[i][j] = false;
        
        return found;
    }
}
  • 时间复杂度:O(3^k × m × n),其中k为单词长度,m、n为矩阵尺寸。每个点有3个方向可选(不能回退)
  • 空间复杂度:O(k),递归栈深度和visited数组空间

时间空间复杂度与方法一相同,但代码更易扩展(如需要八方向移动时只需修改DIRECTIONS数组)

原地标记优化(最优)

通过修改原矩阵来标记访问状态,节省空间。

public class Solution {
    public boolean exist(char[][] board, String word) {
        if (board == null || board.length == 0 || word == null || word.length() == 0) {
            return false;
        }
        
        char[] words = word.toCharArray();
        
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                if (dfsOptimized(board, words, i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    private boolean dfsOptimized(char[][] board, char[] word, int i, int j, int index) {
        // 边界检查和字符匹配检查
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || 
            board[i][j] != word[index]) {
            return false;
        }
        
        // 找到完整路径
        if (index == word.length - 1) {
            return true;
        }
        
        // 原地标记:将当前字符临时替换为特殊标记(不能出现的字符)
        char temp = board[i][j];
        board[i][j] = '#';  // 标记为已访问
        
        // 四个方向搜索
        boolean res = dfsOptimized(board, word, i + 1, j, index + 1) ||
                     dfsOptimized(board, word, i - 1, j, index + 1) ||
                     dfsOptimized(board, word, i, j + 1, index + 1) ||
                     dfsOptimized(board, word, i, j - 1, index + 1);
        
        // 回溯:恢复原始字符
        board[i][j] = temp;
        
        return res;
    }
}

关键技巧:

  1. 临时修改:将访问过的board[i][j]改为'#'(或其他不在字母表中的字符)
  2. 自动避障:后续搜索遇到'#'会因字符不匹配而自动跳过
  3. 状态恢复:回溯时恢复原始字符,确保不影响其他路径搜索

算法分析:

  • 时间复杂度:O(3^k × m × n),与前述方法相同
  • 空间复杂度:O(1),显著优化!仅使用常数空间(递归栈空间不可避免)