数据结构与算法 -- 使用DFS算法处理路径问题(二维)

相关文章:

数据结构与算法 -- 使用DFS算法处理组合类和排列类问题(一维)

在上一篇文章中,我们介绍了通过DFS算法解决组合类以及排列类的问题,主要用于解决一维数组的问题,例如字符串数组、int数组的排列组合问题,那么如果数据集是二维数组,例如矩阵,通常会用来解决路径问题,那么如何通过DFS算法解决此类问题,接下来会详细介绍。

1 矩阵基础知识

1.1 矩阵的遍历

矩阵通常为一个n*m的数组结构,我们可以理解为是多维的数组,首先我们先需要知道,如何遍历矩阵。

java 复制代码
public static int[][] matrix = {{2, 3, 5}, {4, 7, 9}};


public static void matrixForeach() {
    for (int i = 0; i < matrix.length; i++) {
        for (int j = 0; j < matrix[i].length; j++) {
            Log.d(TAG, "matrixForeach: i = " + i + "j = " + j + " " + matrix[i][j]);
        }
    }
}

如果学习过线性代数,我们知道行列式和矩阵的区别在于,行列式是n行n列,但是矩阵没有这个限制,所以在遍历的时候,我们可以先拿到有多少行,即matrix的长度;拿到某一行的数组之后,这一行的数组元素个数就是列数。

java 复制代码
matrixForeach: i = 0 j = 0 2
matrixForeach: i = 0 j = 1 3
matrixForeach: i = 0 j = 2 5
matrixForeach: i = 1 j = 0 4
matrixForeach: i = 1 j = 1 7
matrixForeach: i = 1 j = 2 9

1.2 矩阵内部的移动

回到问题的本身,如果我们想要做二维数组内部的DFS路径问题,如果必须得知道如何在矩阵内部做移动,如下图所示:

元素2可以上下左右移动(理论上),但图中2仅可以向右或者向下移动,因此2会有两条路线,如果选择向右到达下一个点之后,依然采取同样的策略,最终会拿到一条线路;此时回溯到2可以选择向下继续寻找路径。

从上图中我们可以看到,元素2的位置为(0,0),其左侧的元素(0,-1),右侧的元素为(0,1),上方的元素为(-1,0),下方的元素为(1,0),所以左右方向的查找是列数的改动,上下方向的查找是行数的改动。

java 复制代码
//0 上 1 下 2 左 3 右
private static int dx(int orientation) {
    if (orientation == 0) {
        return -1;
    } else if (orientation == 1) {
        return 1;
    }
    return 0;
}

private static int dy(int orientation) {
    if (orientation == 2) {
        return -1;
    } else if (orientation == 3) {
        return 1;
    }
    return 0;
}

所以二维数组的查询跟一维数组的不同之处在于,二维的需要上下左右的找,而一维数组只需要从左向右找即可,也就是说控制startIndex的方式不一样,但递归回溯的思想不变。

因为矩阵中每一个元素都可以作为起点,所以需要遍历整个数组元素,对每一个元素执行上下左右的深度优先搜索。

java 复制代码
public static void findPath() {

    boolean[][] visited = new boolean[matrix.length][matrix[0].length];
    List<String> result = new ArrayList<>();
    for (int i = 0; i < matrix.length; i++) {
        for (int j = 0; j < matrix[i].length; j++) {
            visited[i][j] = true;
            move(matrix, i, j, visited,matrix[i][j]+"", result);
            visited[i][j] = false;
        }
    }
    Log.d(TAG, "matrixForeach: result" + result);
}

/**
 * 二维数组路径查找
 *
 * @param nums    二维数组
 * @param x       行数
 * @param y       列数
 * @param visited 当前节点是否被访问过了
 */
public static void move(int[][] nums,
                        int x,
                        int y,
                        boolean[][] visited,
                        String subset,
                        List<String> result) {

    //出口
    Log.d(TAG, "move: " + subset);

    //什么样的标准可以找到全部路径呢


    //上下左右找
    for (int i = 0; i < 4; i++) {

        int dx = x + dx(i);
        int dy = y + dy(i);

        //如果没有在界内 或者已经被访问过了,就不管
        if (!isInside(nums, dx, dy) || visited[dx][dy]) {
            continue;
        }

        visited[dx][dy] = true;
        move(nums, dx, dy, visited, subset + nums[dx][dy], result);
        visited[dx][dy] = false;
    }
}

当然在上下左右遍历元素的时候,如果出现数组越界或者已经访问了这个元素,那么就不需要走这个方向了,换其他的方向继续查找即可。

java 复制代码
//判断是否越界
private static boolean isInside(int[][] nums, int x, int y) {
    return (x >= 0 && x < nums.length) && (y >= 0 && y < nums[x].length);
}

2 路径问题 - 无固定起点

那么在了解了矩阵的基础知识之后,我们可以使用深度优先搜素路径这个模板,解决一系列的路径问题。

2.1 单词搜索

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

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

示例 1:

arduino 复制代码
输入: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出: true

示例 2:

arduino 复制代码
输入: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出: true

这道题是要我们查找二维矩阵中是否通过某个路径形成一个单词,这道题其实就是经典的路径问题,我们可以通过搜索全部的路径,来匹配要查询的单词,模板依然是1.2小节中。

java 复制代码
public boolean exist(char[][] board, String word) {
    if(board == null || board.length == 0){
        return false;
    }

    boolean[][] visited = new boolean[board.length][board[0].length];
    //构建前缀集合
    List<String> prefix = new ArrayList();
    for(int i = 1;i<=word.length();i++){
        prefix.add(word.substring(0,i));
    }
    List<String> results = new ArrayList();

    //遍历矩阵
    for(int i = 0;i < board.length;i++){
        for(int j = 0;j < board[i].length;j++){
            visited[i][j] = true;
            dfs(board,i,j,visited,board[i][j]+"",results,word,prefix);
            visited[i][j] = false;
        }
    }

    return results.size() > 0;

}

private void dfs(char[][] board,
                 int x,
                 int y,
                 boolean[][] visited,
                 String subset,
                 List<String> results,
                 String word,
                 List<String> prefix){

    //如果前缀不一致,直接return
    if(!prefix.contains(subset)){
        return;
    }

    if(subset.equals(word)){
        results.add(subset);
    }

    for(int i = 0;i<4;i++){
        int dx = x + dx(i);
        int dy = y + dy(i);
        //如果越界 或者 访问过元素,跳过即可
        if(!inside(board,dx,dy) || visited[dx][dy]){
            continue;
        }
        visited[dx][dy] = true;
        dfs(board,dx,dy,visited,subset+board[dx][dy],results,word,prefix);
        visited[dx][dy] = false;
    }
}


//基础方法

// 0 上 1 下 移动
private int dx(int orientation){
    if(orientation == 0){
        return -1;
    }else if(orientation == 1){
        return 1;
    }
    return 0;
}

// 2 左 3 右 移动
private int dy(int orientation){
    if(orientation == 2){
        return -1;
    }else if(orientation == 3){
        return 1;
    }
    return 0;
}

//判断是否越界
public boolean inside(char[][] arr,int x,int y){
    return (x >= 0 && x < arr.length) && (y >= 0 && y < nums[x].length);
}

这里我们先构建了搜索单词的前缀数组,如果路径与前缀不匹配,那么就停止搜索;如果匹配到了单词,那么就存到一个数组中,最终判断数组是否为空。

当然这个算法存在优化的空间,因为我们找到单词之后后续的路径可以停止寻找。

3 路径问题 - 有固定起点

3.1 LeetCode64 - 最小路径和问题

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明: 每次只能向下或者向右移动一步。

示例 1:

lua 复制代码
输入: grid = [[1,3,1],[1,5,1],[4,2,1]]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。

像这道题,因为题目要求是要从左上角出发,终点是左下角;而且只能向下或者向右移动,只有2个方向,那么就可以归结为经典的二叉树问题,采用二叉树的深度优先算法,如下图。

这里需要注意一点,就是当一直往下的时候,例如(2,0)的位置,此时在递归的出口处无法拦截,此时会继续进入到dfs中,此时需要做边界的判断,如果超出行、或者列数时,和不变即可,此后进入到出口时会被return,回溯转向右侧分支,此时右侧分支的行数一定不会越界,只需要关注列数的边界即可。

java 复制代码
public int minPathSum(int[][] grid) {

    if(grid == null || grid.length == 0){
        return 0;
    }

    int[] min = {Integer.MAX_VALUE};
    dfs(grid,0,0,grid[0][0],min);
    return min[0];
}

private void dfs(int[][] grid,
                 int x,
                 int y,
                 int sum,
                 int[] min){

    if (x < 0 || y < 0 || x >= grid.length || y >= grid[x].length) {
        return;
    }

    if (x == grid.length - 1 && y == grid[x].length - 1) {
        //到了右下角了
        min[0] = Math.min(min[0], sum);
        return;
    }

    //只能移动两个方向
    for (int i = 0; i < 2; i++) {
        int dx = x + dx3(i);
        int dy = y + dy3(i);
        dfs(grid, dx, dy, sum + ((dx == grid.length || dy == grid[dx].length) ? 0 : grid[dx][dy]), min);
    }

}

// 0 下 1 右
// 当向下走时,x + 1,y 不变
// 当向右走时,x 不变, y + 1
private static int dx3(int orientation) {
    
    if (orientation == 0) {
        // 下
        return 1;
    } else {
        // 右
        return 0;
    }

}

private static int dy3(int orientation) {

    if (orientation == 0) {
        return 0;
    } else {
        return 1;
    }

}

3.2 LeetCode120 - 三角形最小路径和

给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 ii + 1

示例 1:

lua 复制代码
输入: triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出: 11
解释: 如下面简图所示:
   2
  3 4
 6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

同样,这道题我们也要求我们从原点出发,而且只能走到相邻节点上,如下图所示:

像这种标准的平衡二叉树,对于边界问题的处理,只需要判断当前x,也就是行数是否超过二维数组的行数作为出口即可

java 复制代码
public int minimumTotal(List<List<Integer>> triangle) {
    if(triangle == null || triangle.size() == 0){
        return 0;
    }
    int[] min = {Integer.MAX_VALUE};
    dfs(triangle,0,0,triangle.get(0).get(0),min);
    return min[0];
}

private void dfs(List<List<Integer>> triangle,
                 int x,
                 int y,
                 int sum,
                 int[] min){

    if (x == triangle.size() - 1) {
        min[0] = Math.min(min[0], sum);
        return;
    }

    if (!inside(triangle,x,y)){
        return;
    }

    //只有两步路可以走
    dfs(triangle,x+1,y,sum+triangle.get(x+1).get(y),min);
    dfs(triangle,x+1,y+1,sum+triangle.get(x+1).get(y+1),min);
}
相关推荐
重生之我要进大厂12 分钟前
LeetCode 876
java·开发语言·数据结构·算法·leetcode
Happy鱿鱼1 小时前
C语言-数据结构 有向图拓扑排序TopologicalSort(邻接表存储)
c语言·开发语言·数据结构
KBDYD10101 小时前
C语言--结构体变量和数组的定义、初始化、赋值
c语言·开发语言·数据结构·算法
Crossoads1 小时前
【数据结构】排序算法---桶排序
c语言·开发语言·数据结构·算法·排序算法
自身就是太阳1 小时前
2024蓝桥杯省B好题分析
算法·职场和发展·蓝桥杯
孙小二写代码2 小时前
[leetcode刷题]面试经典150题之1合并两个有序数组(简单)
算法·leetcode·面试
QXH2000002 小时前
数据结构—单链表
c语言·开发语言·数据结构
imaima6662 小时前
数据结构----栈和队列
开发语言·数据结构
little redcap2 小时前
第十九次CCF计算机软件能力认证-1246(过64%的代码-个人题解)
算法
David猪大卫2 小时前
数据结构修炼——顺序表和链表的区别与联系
c语言·数据结构·学习·算法·leetcode·链表·蓝桥杯