目录
搜索与回溯算法
1 矩阵中的路径
剑指 Offer 12. 矩阵中的路径https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof/
DFS 解析:
- 递归参数: 当前元素在矩阵
board
中的行列索引i
和j
,当前目标字符在word
中的索引k
。 - 终止条件:
- 返回 false :
- 行或列索引越界
- 当前矩阵元素与目标字符不同
- 当前矩阵f已访问过 ( 通过赋值为空字符 3 可合并至 2 ) 。
- 返回 true :
k = len(word) - 1
,即字符串word
已全部匹配。
- 返回 false :
- 递推工作:
- 标记当前矩阵元素: 将
board[i][j]
修改为 空字符''
,代表此元素已访问过,防止之后搜索时重复访问。 - 搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用
或
连接 (代表只需找到一条可行路径就直接返回,不再做后续 DFS ),并记录结果至res
。 - 还原当前矩阵元素: 将
board[i][j]
元素还原至初始值,即word[k]
。
- 标记当前矩阵元素: 将
- 返回值: 返回布尔量
res
,代表是否搜索到目标字符串。
使用空字符(Python: '' , Java/C++: '\0' )做标记是为了防止标记字符与矩阵原有字符重复。当存在重复时,此算法会将矩阵原有字符认作标记字符,从而出现错误。
1. DFS 传入String
class Solution {
public boolean exist(char[][] board, String word) {
Boolean[][] visited;
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (search(board, i, j, word,0))
return true;
}
}
return false;
}
public boolean search(char[][] board, int i, int j,String word,int n) {
if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word.charAt(n)) return false;
if (n == word.length() - 1)
return true;
board[i][j] = '\0';
boolean res = search(board, i + 1, j, word, n + 1) || search(board, i - 1, j, word, n + 1)
|| search(board, i, j + 1, word, n + 1) || search(board, i, j - 1, word, n + 1);
board[i][j] = word.charAt(n);
return res;
}
}
2. 优化传入String为字符数组
字符数组索引起来效率更高!
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for(int i = 0; i < board.length; i++) {
for(int j = 0; j < board[0].length; j++) {
if(dfs(board, words, i, j, 0)) return true;
}
}
return false;
}
boolean dfs(char[][] board, char[] word, int i, int j, int k) {
if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) return false;
if(k == word.length - 1) return true;
board[i][j] = '\0';
boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) ||
dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);
board[i][j] = word[k];
return res;
}
}
2 机器人的运动范围
剑指 Offer 13. 机器人的运动范围https://leetcode-cn.com/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/
将行坐标和列坐标数位之和大于 k
的格子看作障碍物,那么这道题就是一道很传统的搜索题目,可以使用广度优先搜索或者深度优先搜索来解决它。
1.1 DFS (自己写的)
深度优先搜索思路解析
- 深度优先搜索: 可以理解为暴力法模拟机器人在矩阵中的所有路径。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
- 剪枝: 在搜索中,遇到数位和超出目标值、此元素已访问,则应立即返回,称之为
可行性剪枝
。
算法解析:
- 递归参数: 当前元素在矩阵中的行列索引
i
和j
,两者的数位和si
,sj
。 - 终止条件: 当 ① 行列索引越界 或 ② 数位和超出目标值
k
或 ③ 当前元素已访问过 时,返回 0 ,代表不计入可达解。 - 递推工作:
- 标记当前单元格 :将索引
(i, j)
存入visited
中,代表此单元格已被访问过。 - 搜索下一单元格: 计算当前元素的 下、右 两个方向元素的数位和,并开启下层递归 。
- 标记当前单元格 :将索引
- 回溯返回值: 返回
1 + 右方搜索的可达解总数 + 下方搜索的可达解总数
,代表从本单元格递归搜索的可达解总数。
class Solution {
public int movingCount(int m, int n, int k) {
int[][] visited = new int[m][n];
DFS(visited, m, n,k,0,0);
int num = 0;
for (int i = 0; i <m; i++) {
for (int j = 0; j < n; j++) {
if(visited[i][j] == 1) num++;
// System.out.print(visited[i][j] +" ");
}
// System.out.println("");
}
return num;
}
public void DFS(int[][] visited, int m, int n,int k, int i, int j){
int sum = 0;
int a = i, b = j;
sum+=a%10;
while(a/10!=0){
a = a/10;
sum+=a%10;
}
sum+=b%10;
while(b/10!=0){
b = b/10;
sum+=b%10;
}
if(i > m-1 || i < 0 || j > n-1 || j < 0 || (sum>k)||visited[i][j] == 1) return;
// System.out.println("i: "+i+" j:"+j);
visited[i][j] = 1;
// if(i == m-1 && j == n-1) return;
DFS(visited, m,n,k,i+1, j);
DFS(visited, m,n,k,i-1, j);
DFS(visited, m,n,k,i,j+1);
DFS(visited, m,n,k,i,j-1);
// visited[i][j] = 0;
}
}
1.2 DFS返回格子数
DFS优化为返回值,省去双重遍历visited数组。
class Solution {
public int movingCount(int m, int n, int k) {
int[][] visited = new int[m][n];
return DFS(visited, m, n,k,0,0);
}
public int DFS(int[][] visited, int m, int n,int k, int i, int j){
int sum = 0;
int a = i, b = j;
sum+=a%10;
while(a/10!=0){
a = a/10;
sum+=a%10;
}
sum+=b%10;
while(b/10!=0){
b = b/10;
sum+=b%10;
}
if(i > m-1 || i < 0 || j > n-1 || j < 0 || (sum>k)||visited[i][j] == 1) return 0;
visited[i][j] = 1;
return DFS(visited, m,n,k,i+1, j) + DFS(visited, m,n,k,i-1, j) + DFS(visited, m,n,k,i,j+1) + DFS(visited, m,n,k,i,j-1)+1;
}
}
1.3 DFS进一步优化
根据题解的思路,仅仅需要保留向下、向右的搜索即可。
class Solution {
public int movingCount(int m, int n, int k) {
int[][] visited = new int[m][n];
return DFS(visited, m, n,k,0,0);
}
public int DFS(int[][] visited, int m, int n,int k, int i, int j){
int sum = 0;
int a = i, b = j;
sum+=a%10;
while(a/10!=0){
a = a/10;
sum+=a%10;
}
sum+=b%10;
while(b/10!=0){
b = b/10;
sum+=b%10;
}
if(i > m-1 || i < 0 || j > n-1 || j < 0 || (sum>k)||visited[i][j] == 1) return 0;
visited[i][j] = 1;
return DFS(visited, m,n,k,i+1, j) + DFS(visited, m,n,k,i,j+1) +1;
}
}
1.4 递归参数优化
class Solution {
int m, n, k;
boolean[][] visited;
public int movingCount(int m, int n, int k) {
this.m = m; this.n = n; this.k = k;
this.visited = new boolean[m][n];
return dfs(0, 0, 0, 0);
}
public int dfs(int i, int j, int si, int sj) {
if(i >= m || j >= n || k < si + sj || visited[i][j]) return 0;
visited[i][j] = true;
return 1 + dfs(i + 1, j, (i + 1) % 10 != 0 ? si + 1 : si - 8, sj) + dfs(i, j + 1, si, (j + 1) % 10 != 0 ? sj + 1 : sj - 8);
}
}
效率和我上面1.3的相差无几。写法上的不同!
- 首先m,n,k,visited 都设为了全局参数,不需要在dfs中传递。
- 其次利用了数位和的递推原理简化了while求数位和的过程直接传递。
x⊙10 :得到 x 的个位数字;
复杂度分析:
设矩阵行列数分别为 M, N 。
- 时间复杂度 O(MN): 最差情况下,机器人遍历矩阵所有单元格,此时时间复杂度为 O(MN) 。
- 空间复杂度 O(MN) : 最差情况下,
visited
内存储矩阵所有单元格的索引,使用 O(MN) 的额外空间。
2. BFS
- 首先判断k为0的时候返回1
- new一个队列,进行BFS,vis作为访问标志new为Boolean型
- 先将new int[]{0,0}offer进队列,设置vis为true,ans赋值为1;
- BFS开始 while条件为队列不为空
- 队列poll出第一个,通过x=cell[0],y=cell[1]获取坐标
- 利用定义好的方向数组进行更新两次坐标,不满足条件或被访问过则continue;
- 反之入队列,访问标志置为true,ans+1
关键点是如何计算一个数的数位之和呢?
只需要将
x
每次对10
取余,就能知道数x
的个位数是多少,然后再将x
除10
,这个操作等价于将x
的十进制数向右移一位,删除个位数(类似于二进制中的>>
右移运算符),不断重复直到x
为0
时结束。
同时这道题还有一个隐藏的优化:
在搜索的过程中搜索方向可以缩减为向右和向下,而不必再向上和向左进行搜索。
class Solution {
public int movingCount(int m, int n, int k) {
if (k == 0) {
return 1;
}
Queue<int[]> queue = new LinkedList<int[]>();
// 向右和向下的方向数组
int[] dx = {0, 1};
int[] dy = {1, 0};
boolean[][] vis = new boolean[m][n];
queue.offer(new int[]{0, 0});
vis[0][0] = true;
int ans = 1;
while (!queue.isEmpty()) {
int[] cell = queue.poll();
int x = cell[0], y = cell[1];
for (int i = 0; i < 2; ++i) {
int tx = dx[i] + x;
int ty = dy[i] + y;
if (tx < 0 || tx >= m || ty < 0 || ty >= n || vis[tx][ty] || get(tx) + get(ty) > k) {
continue;
}
queue.offer(new int[]{tx, ty});
vis[tx][ty] = true;
ans++;
}
}
return ans;
}
private int get(int x) {
int res = 0;
while (x != 0) {
res += x % 10;
x /= 10;
}
return res;
}
}
复杂度分析
-
时间复杂度:O(mn),其中
m
为方格的行数,n
为方格的列数。考虑所有格子都能进入,那么搜索的时候一个格子最多会被访问的次数为常数,所以时间复杂度为 O(2mn)=O(mn) 。 -
空间复杂度:O(mn),其中
m
为方格的行数,n
为方格的列数。搜索的时候需要一个大小为 O(mn)的标记结构用来标记每个格子是否被走过。
3. 递推
算法
定义 vis[i][j]
为 (i, j)
坐标是否可达,如果可达返回 1
,否则返回 0
。
首先 (i, j)
本身需要可以进入,因此需要先判断 i
和 j
的数位之和是否大于 k
,如果大于的话直接设置 vis[i][j]
为不可达即可。否则,前面提到搜索方向只需朝下或朝右,因此 (i, j)
的格子只会从 (i - 1, j)
或者 (i, j - 1)
两个格子走过来(不考虑边界条件),那么 vis[i][j]
是否可达的状态则可由如下公式计算得到:
即只要有一个格子可达,那么 (i, j)
这个格子就是可达的,因此我们只要遍历所有格子,递推计算出它们是否可达然后用变量 ans
记录可达的格子数量即可。
初始条件 vis[i][j] = 1
,递推计算的过程中注意边界的处理。
class Solution {
public int movingCount(int m, int n, int k) {
if (k == 0) {
return 1;
}
boolean[][] vis = new boolean[m][n];
int ans = 1;
vis[0][0] = true;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if ((i == 0 && j == 0) || get(i) + get(j) > k) {
continue;
}
// 边界判断
if (i - 1 >= 0) {
vis[i][j] |= vis[i - 1][j];
}
if (j - 1 >= 0) {
vis[i][j] |= vis[i][j - 1];
}
ans += vis[i][j] ? 1 : 0;
}
}
return ans;
}
private int get(int x) {
int res = 0;
while (x != 0) {
res += x % 10;
x /= 10;
}
return res;
}
}
复杂度分析
-
时间复杂度:O(MN),其中
M
为方格的行数,N
为方格的列数。一共有 O(MN) 个状态需要计算,每个状态递推计算的时间复杂度为 O(1),所以总时间复杂度为 O(MN)。 -
空间复杂度:O(MN),其中
M
为方格的行数,N 为方格的列数。我们需要 O(MN) 大小的结构来记录每个位置是否可达。