Leetcode 算法之深度优先搜索 —— Java 题解

264 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

所谓深度优先搜索,是我们在遍历到一个新节点时,立即对其相邻的某一个结点进行深度遍历。在处理“树”、“图”这种数据结构的时候,我们往往会采用深度优先搜索算法;当然也可以采用广度优先搜索算法。

深度优先搜索算法通常采用递归的形式,也可以使用栈来模拟这个过程。

考虑如下一个二叉树:

    1
   / \
  2   3
 /
4 

如果我们对其进行深度遍历,遍历顺序为根结点->左结点->右节点(即树的先序遍历),那么遍历过程为:1->2->4->3

深度优先搜索可以检测一个“图”中是否存在“环路”,具体表现为:记录遍历过的父级结点,当我们在遍历过程中,遍历到一个已经搜索过的父级结点时,表明存在环路。

下面挑选 4 道题目,主要是二维数组的“图”形式,练习深度优先搜索算法。

695. 岛屿的最大面积 - 简单

给你一个大小为 m x n 的二进制矩阵 grid 。

岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

岛屿的面积是岛上值为 1 的单元格的数目。

计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。

示例:

maxarea1-grid.jpg 输入:grid =

[[0,0,1,0,0,0,0,1,0,0,0,0,0],

[0,0,0,0,0,0,0,1,1,1,0,0,0],

[0,1,1,0,1,0,0,0,0,0,0,0,0],

[0,1,0,0,1,1,0,0,1,0,1,0,0],

[0,1,0,0,1,1,0,0,1,1,1,0,0],

[0,0,0,0,0,0,0,0,0,0,1,0,0],

[0,0,0,0,0,0,0,1,1,1,0,0,0],

[0,0,0,0,0,0,0,1,1,0,0,0,0]]

输出:6

解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。

题解:

本题可以用深度优先搜索解决,是一道很典型的题目。

首先,我们遍历“地图”——二维数组,如果遇到岛屿——即“1”,那么对其进行深度遍历,同时标记为“0”,表示该点已经搜索过。或者标记为“2”,一遍搜索结束后进行复原。

该点深度遍历后,我们得到该岛屿的面积,与最大面积比较更新。

深度遍历的具体过程如下:我们对该点的上下左右四个方向一次进行深度遍历

  1. 该点的面积 area 初始为 1
  2. 如果某个方向的点在地图内,并且其值为“1”,尚未搜索过,对其进行深度搜索,得到这个方向上的面积,累加到面积 area
  3. 返回面积

代码:

class Solution {
    // 确定搜索方向的辅助数组
    private int[] direction = {-1, 0, 1, 0, -1};

    public int maxAreaOfIsland(int[][] grid) {
        int row = grid.length;
        int col = grid[0].length;
        int max = 0;

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                // 该点尚未搜索过
                if (grid[i][j] == 1) {
                    // 标记为已搜索
                    grid[i][j] = 0;
                    // 深度遍历得到面积
                    int area = dfs(grid, i, j);
                    max = Math.max(area, max);
                }
            }
        }

        return max;
    }

    private int dfs(int[][] grid, int x, int y) {
        int row = grid.length;
        int col = grid[0].length;
        // 该点的初始面积为 1
        int area = 1;
        for (int i = 0; i < 4; i++) {
            // 确定搜索方向
            int newX = x+direction[i];
            int newY = y+direction[i+1];
            if (newX >= 0 && newX < row && newY >= 0 && newY < col && grid[newX][newY] == 1) {
                grid[newX][newY] = 0;
                // 累加该方向上的面积
                area += dfs(grid, newX, newY);
            }
        }

        return area;
    }
}

200. 岛屿数量 - 简单

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例:

输入:grid = [

["1","1","1","1","0"],

["1","1","0","1","0"],

["1","1","0","0","0"],

["0","0","0","0","0"] ]

输出:1

题解:

遍历地图(即二维数组grid)。

如果遍历到单元格的值为 '1',那么对当前单元格进行深度优先搜索:

  1. 将当前单元格置为 '0'
  2. 对当前单元格的上下左右四个方向进行深度优先搜索

题解:

class Solution {
    int[] direction = {-1, 0, 1, 0, -1};

    public int numIslands(char[][] grid) {
        int m = grid.length, n = grid[0].length;
        int count = 0;

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1') {
                    dfs(grid, i, j);
                    count++;
                }
            }
        }

        return count;
    }

    private void dfs(char[][] grid, int x, int y) {
        int m = grid.length, n = grid[0].length;
        grid[x][y] = '0';

        for (int i = 0; i < 4; i++) {
            int newX = x+direction[i], newY = y+direction[i+1];
            if (newX >= 0 && newX < m && newY >= 0 && newY < n && grid[newX][newY] == '1') {
                dfs(grid, newX, newY);
            }
        }
    }
}

417. 太平洋大西洋水流问题 - 中等

有一个 m × n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。

这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度 。

岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。

返回网格坐标 result 的 2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋 。

示例:

waterflow-grid.jpg

输入: heights =

[[1,2,2,3,5],

[3,2,3,4,4],

[2,4,5,3,1],

[6,7,1,4,5],

[5,1,1,2,4]]

输出: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]

题解:

题目要求我们找到水既可以流到太平洋也可以流到大西洋的单元格。

我们逆转下思路,从海边开始寻找,某个海洋的水可以从哪个单元格流过来。

  • 太平洋的水(即表格的左边界和上边界)来自哪些单元格
  • 大西洋的水(即表格的右边界和下边界)来自那些单元格

最后,如果某个单元格都可以流向两个海洋,那么将其坐标加入到结果集中。

我们如何确定某个海洋的水可以从哪个单元格流过来呢?我们可以采取深度优先遍历。

声明两个二维数组 boolean[][] 分别表示某个单元格能否流向太平洋和大西洋:

  • canReachP[i][j],表示单元格[i][j] 的水可以流向太平洋
  • canReachA[i][j],表示单元格[i][j] 的水可以流向大西洋

深度遍历过程如下:

  1. 从海边开始遍历,即从表格边界开始遍历
  2. 如果 canReachX[i][j]true,表示已经搜索过,不必再搜索;否则
  3. canReach[i][j] 标记为 true,对该单元格的上下左右四个方向进行深度遍历,条件是相邻单元格的高度 >= 当前单元格

代码:

class Solution {
    // 方向遍历辅助数组
    private int[] direction = {-1,0,1,0,-1};

    public List<List<Integer>> pacificAtlantic(int[][] heights) {
        int row = heights.length;
        int col = heights[0].length;
        // 标记数组,某单元格是否可以流向某个海洋
        boolean[][] canReachP = new boolean[row][col];
        boolean[][] canReachA = new boolean[row][col];

        // 从左边界开始搜索可以流向太平洋的单元格,从右边界搜索可以流向大西洋的表格
        for (int i = 0; i < row; i++) {
            dfs(heights, i, 0, canReachP);
            dfs(heights, i, col-1, canReachA);
        }

        // 从上边界开始搜索可以流向太平洋的单元格,从下边界搜索可以流向大西洋的表格
        for (int i = 0; i < col; i++) {
            dfs(heights, 0, i, canReachP);
            dfs(heights, row-1, i, canReachA);
        }

        // 结果集
        List<List<Integer>> res = new ArrayList<>();
        // 确定可以同时流向两个海洋的单元格
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (canReachP[i][j] && canReachA[i][j]) {
                    ArrayList<Integer> temp = new ArrayList<>();
                    temp.add(i);
                    temp.add(j);
                    res.add(temp);
                }
            }
        }

        return res;
    }

    private void dfs(int[][] heights, int x, int y, boolean[][] canReach) {
        if (canReach[x][y]) {
            return;
        }
        canReach[x][y] = true;

        int row = heights.length;
        int col = heights[0].length;
        for (int i = 0; i < 4; i++) {
            int newX = x+direction[i], newY = y+direction[i+1];
            if (newX >= 0 && newX < row && newY >= 0 && newY < col && heights[x][y] <= heights[newX][newY]) {
                dfs(heights, newX, newY, canReach);
            }
        }
    }
}

547. 省份数量 - 中等

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

示例:

graph1.jpg

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]

输出:2

题解:

我们可以声明一个标记数组 visited,标记某个城市是否已经搜索过。

遍历城市,如果某个城市未搜索过,那么对其进行深度遍历:

  1. 将该城市标记为已经搜索过;
  2. 根据 isConnected ,如果该城市与其他城市 X 相连,并且 X 城市尚未搜索过,对其进行深度遍历

对某个城市进行深度搜索后,省份数量加一。

最后搜索结束,返回省份数量。

代码:

class Solution {
    public int findCircleNum(int[][] isConnected) {
        int n = isConnected.length;
        boolean[] visited = new boolean[n];
        int count = 0;

        for (int i = 0; i < n; i++) {
            if (!visited[i]) {
                dfs(isConnected, visited, n, i);
                count++;
            }
        }
        return count;
    }

    private void dfs(int[][] isConnected, boolean[] visited, int cities, int i) {
        visited[i] = true;
        for (int j = 0; j < cities; j++) {
            if (!visited[j] && isConnected[i][j] == 1) {
                dfs(isConnected, visited, cities, j);
            }
        }
    }
}