阿翰 剑指offer 之 Day 14 搜索与回溯算法 3 中等

 目录

搜索与回溯算法 

1 矩阵中的路径

1. DFS 传入String

2. 优化传入String为字符数组

2 机器人的运动范围

1.1 DFS (自己写的)

​1.2 DFS返回格子数

1.3 DFS进一步优化

1.4 递归参数优化

2. BFS

3. 递推


搜索与回溯算法 

1 矩阵中的路径

剑指 Offer 12. 矩阵中的路径icon-default.png?t=LA92https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof/

DFS 解析:

  • 递归参数: 当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。
  • 终止条件:
    1. 返回 false :
      1. 行或列索引越界
      2. 当前矩阵元素与目标字符不同
      3. 当前矩阵f已访问过 ( 通过赋值为空字符 3 可合并至 2 ) 。
    2. 返回 true : k = len(word) - 1 ,即字符串 word 已全部匹配。
  • 递推工作:
    1. 标记当前矩阵元素: 将 board[i][j] 修改为 空字符 '' ,代表此元素已访问过,防止之后搜索时重复访问。
    2. 搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用  连接 (代表只需找到一条可行路径就直接返回,不再做后续 DFS ),并记录结果至 res 。
    3. 还原当前矩阵元素: 将 board[i][j] 元素还原至初始值,即 word[k] 。
  • 返回值: 返回布尔量 res ,代表是否搜索到目标字符串。

使用空字符(Python: '' , Java/C++: '\0' )做标记是为了防止标记字符与矩阵原有字符重复。当存在重复时,此算法会将矩阵原有字符认作标记字符,从而出现错误。

来源:力扣(LeetCode) jyd

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. 机器人的运动范围icon-default.png?t=LA92https://leetcode-cn.com/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/

将行坐标和列坐标数位之和大于 k 的格子看作障碍物,那么这道题就是一道很传统的搜索题目,可以使用广度优先搜索或者深度优先搜索来解决它。 

1.1 DFS (自己写的)

深度优先搜索思路解析

  • 深度优先搜索: 可以理解为暴力法模拟机器人在矩阵中的所有路径。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
  • 剪枝: 在搜索中,遇到数位和超出目标值、此元素已访问,则应立即返回,称之为 可行性剪枝 

算法解析:

  • 递归参数: 当前元素在矩阵中的行列索引 i 和 j ,两者的数位和 sisj 。
  • 终止条件: 当 ① 行列索引越界  ② 数位和超出目标值 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的相差无几。写法上的不同!

  1. 首先m,n,k,visited 都设为了全局参数,不需要在dfs中传递。
  2. 其次利用了数位和的递推原理简化了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) 大小的结构来记录每个位置是否可达。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值