200. 岛屿数量

695. 岛屿的最大面积

精品题解 https://leetcode.cn/problems/number-of-islands/solution/dao-yu-lei-wen-ti-de-tong-yong-jie-fa-dfs-bian-li-/

注意深度优先遍历,对一格陆地(=='1')遍历, 就会把与它连通的所有陆地遍历到,全部标记为2, 完成一个岛屿。从而这一次标记到的作为一个岛屿数量 +1

for (int i=0; i<grid.length;i++) {
            for (int j=0;j<grid[0].length;j++) {
                // 深度优先遍历,对一格陆地(=='1')遍历,
                // 就会把与它连通的所有陆地遍历到,全部标记为2。从而这一次标记到的作为一个岛屿数量 +1
                if (grid[i][j] == '1') {
                    dfs(grid, i, j);
                    islandNum++;
                }
            }
        }

岛屿数量

class Solution {
    public int numIslands(char[][] grid) {
        int islandNum = 0;
        for (int i=0; i<grid.length;i++) {
            for (int j=0;j<grid[0].length;j++) {
                // 深度优先遍历,对一格陆地(=='1')遍历,
                // 就会把与它连通的所有陆地遍历到,全部标记为2。从而这一次标记到的作为一个岛屿数量 +1
                if (grid[i][j] == '1') {
                    dfs(grid, i, j);
                    islandNum++;
                }
            }
        }
        return islandNum;
    }

    private void dfs(char[][] grid, int x, int y) {
        if (!isInGrid(grid, x, y)) {
            return;
        }
        if (grid[x][y] != '1') {
            return;
        }
        // 访问过的标记为2,不再重复访问
        grid[x][y] = '2';
        // 对这个格子的上下左右分别dfs
        //
        dfs(grid, x, y+1);
        //
        dfs(grid, x, y-1);
        //
        dfs(grid, x-1, y);
        //
        dfs(grid, x+1, y);
    }

   // 判断这个格子是否已经超出了 grid 的范围
    private boolean isInGrid(char[][] grid, int x, int y) {
        // 注意是 >= 0 不是 >0
        return x >= 0 && x < grid.length && y >= 0 && y < grid[0].length;
    }
}

岛屿的最大面积

class Solution {
    public int maxAreaOfIsland(int[][] grid) {
        int maxAreaOfIsland = 0;
        for (int i=0;i<grid.length;i++) {
            for (int j=0;j<grid[0].length;j++) {
                // 每次 dfs 完的都是一整块岛屿
                if (grid[i][j] == 1) {
                    int thisIslandArea = dfs(grid, i, j);
                    maxAreaOfIsland = Math.max(maxAreaOfIsland, thisIslandArea);
                }
            }
        }
        return maxAreaOfIsland;
    }

    private int dfs(int[][] grid, int x, int y) {
        if (!isInGrid(grid, x, y)) {
            return 0;
        }
        if (grid[x][y] != 1) {
            return 0;
        }
        grid[x][y] = 2;

        // 自己1 + 上下左右
        return 1 + dfs(grid, x, y+1)
                + dfs(grid, x, y-1)
                + dfs(grid, x-1, y)
                + dfs(grid, x+1, y);
    }

    private boolean isInGrid(int[][] grid, int x, int y) {
        return x>=0 && x<grid.length && y>=0 && y<grid[0].length;
    }
}

 

 

207. 课程表

DFS 解法

关于 dfs,请看这个题解里的说明图

https://leetcode.cn/problems/number-of-islands/solution/number-of-islands-shen-du-you-xian-bian-li-dfs-or-/

1、怎么样的有向图可以有一个拓扑排序?即怎样的有向图可以学完所有课程?

  • 没有环的有向图都可以有一个拓扑排序
  • 只要有环,就没法得到一个拓扑排序(因为环里一定存在相互依赖的两个课程,A 需要先学完 B 才能学,B 又需要先学完 A 才能学)

2、怎么表示图?

  • 用 List<List<Integer>> graph 表示,index 是起始节点的课程的标志 
  • graph.get(index) 这个 List 就是这个起始课程可以指向的课程
  • 用 prerequisites 初始化这个 graph ,[ai, bi] 学习 ai 必须要先学课程 bi,即有一条从 bi (prerequisites[i][1]) 指向 ai (prerequisites[i][0]) 的有向边

3、visited 访问数组有什么用?visited = new int[numCourses];

  • visited = [0] ,这个课程节点还未被访问过,可以进行 dfs 访问
  • visited = [1] ,dfs 刚进来时会置为 1。表示正在被本轮 dfs 访问,如果在一轮 dfs 中访问到了 visited 为 1 的,说明它在一轮中被访问了两次,说明图里存在环,将结果置为 false ,并返回
  • visited = [2] ,dfs 末尾时会置为 2(题解的说明图里为-1)。本轮 dfs 访问完了置为 2,之后的 dfs 访问到,就表示它已经被别的轮次的 dfs 访问过,不必再重复访问。

4、整个过程是怎么样的?

先初始化好邻接表

从 0 开始遍历 numCourses(条件要 && res,res 一有 false 就可以停了)当 visited = 0 时进行 dfs,表示对以每个没访问过的课程节点为起点,开始进行 dfs

在 dfs 里面:

    • 一进来,先将 visited 置为 1
    • 对每个分支进行 dfs,在这里即为对当前节点邻接表里的所有边进行 dfs
      • 如果 visited = 0 ,没被访问过,进行 dfs
      • 如果 visited = 1,表示有环,结果置为 false 并返回
      • 如果 visited = 2,表示已经被别的轮次的 dfs 访问过了,不必再访问,什么都不用做 
    • dfs 结束,将 visited 置为 2 ,本轮次已经结束。
class Solution {

      // 邻接表
    List<List<Integer>> graph;
    // 表示是否访问过?
    int[] visited;
    boolean res;

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        graph = new ArrayList();
        visited = new int[numCourses];
        // 只有这个有向图存在环,就不存在拓扑排序(因为有相互依赖的)
        // 只有这个有向图没有环,就存在拓扑排序
        // 所以默认为 true,只在判断存在环的地方改为 false
        res = true;

        // 初始化邻接表
        for(int i=0;i<numCourses;i++) {
            visited[i] = 0;
            List<Integer> courseList = new ArrayList();
            graph.add(courseList);
        }
        for(int[] link: prerequisites) {
            // [ai, bi] 学习 ai 必须要先学课程 bi
            // 即有一条从 bi (prerequisites[i][1])指向 ai (prerequisites[i][0]) 的有向边
            graph.get(link[1]).add(link[0]);
        }
        
        // 只要有一个环,结果就为 false,不用再继续遍历了。所以要加上条件 && res
        // 以所有课程为起点,进行 dfs
        for(int i=0;i<numCourses && res;i++) {
            if (visited[i] == 0) {
                dfs(i);
            }
        }
        return res;
    }

    private void dfs(int nowCourse) {
        // 被当前轮次的 dfs 访问这个节点
        visited[nowCourse] = 1;
        List<Integer> nextCourseList = graph.get(nowCourse);
        // 对于一个节点所有的分支进行 dfs,这里是对以这个节点为起点的所有有向边进行 dfs
        for (int i=0;i<nextCourseList.size();i++) {
            int nextCourse = nextCourseList.get(i);
            // 访问那些没有访问过的
            if (visited[nextCourse] == 0) {
                dfs(nextCourse);
            }
            else if (visited[nextCourse] == 1) {
                // 这个节点被当前轮次的 dfs 访问了两次,存在环,直接返回
                // 只有这个有向图存在环,就不存在拓扑排序(因为有相互依赖的)
                // 只有这个有向图没有环,就存在拓扑排序
                res = false;
                return;
            }
            // 对于 visited[i] == 2,被别的轮次的 dfs 访问过的,不必重复访问
        }
        // 只有一次DFS完整结束了,才能执行到这一步,这个 dfs 轮次已经结束。改为 2表示被别的轮次的 dfs 访问过了
        visited[nowCourse] = 2;
    }
}

 另一种写法,与之前 dfs 的风格更一致

class Solution {

      // 邻接表
    List<List<Integer>> graph;
    // 表示是否访问过?
    int[] visited;
    boolean res;


    public boolean canFinish(int numCourses, int[][] prerequisites) {
        graph = new ArrayList();
        visited = new int[numCourses];
        // 只有这个有向图存在环,就不存在拓扑排序(因为有相互依赖的)
        // 只有这个有向图没有环,就存在拓扑排序
        // 所以默认为 true,只在判断存在环的地方改为 false
        res = true;

        // 初始化邻接表
        for(int i=0;i<numCourses;i++) {
            visited[i] = 0;
            List<Integer> courseList = new ArrayList();
            graph.add(courseList);
        }
        for(int[] link: prerequisites) {
            // [ai, bi] 学习 ai 必须要先学课程 bi
            // 即有一条从 bi (prerequisites[i][1])指向 ai (prerequisites[i][0]) 的有向边
            graph.get(link[1]).add(link[0]);
        }
        
        // 只要有一个环,结果就为 false,不用再继续遍历了。所以要加上条件 && res
        // 以所有课程为起点,进行 dfs
        for(int i=0;i<numCourses && res;i++) {
            if (visited[i] == 0) {
                dfs(i);
            }
        }
        return res;
    }

    private void dfs(int nowCourse) {
        if (visited[nowCourse] == 1) {
            // 这个节点被当前轮次的 dfs 访问了两次,存在环,直接返回
            // 只有这个有向图存在环,就不存在拓扑排序(因为有相互依赖的)
            // 只有这个有向图没有环,就存在拓扑排序
            res = false;
            return;
        }
        else if (visited[nowCourse] == 2) {
            // 对于 visited[i] == 2,被别的轮次的 dfs 访问过的,不必重复访问
            return;
        }

        // 被当前轮次的 dfs 访问这个节点
        visited[nowCourse] = 1;
        List<Integer> nextCourseList = graph.get(nowCourse);
        // 对于一个节点所有的分支进行 dfs,这里是对以这个节点为起点的所有有向边进行 dfs
        for (int i=0;i<nextCourseList.size();i++) {
            int nextCourse = nextCourseList.get(i);
            dfs(nextCourse);
        }
        // 只有一次DFS完整结束了,才能执行到这一步,这个 dfs 轮次已经结束。改为 2表示被别的轮次的 dfs 访问过了
        visited[nowCourse] = 2;
    }
}

 

BFS 解法

关于 bfs,请看这个题解里的说明图

https://leetcode.cn/problems/number-of-islands/solution/number-of-islands-shen-du-you-xian-bian-li-dfs-or-/

1、怎么样的有向图可以有一个拓扑排序?即怎样的有向图可以学完所有课程?

  • 没有环的有向图都可以有一个拓扑排序
  • 只要有环,就没法得到一个拓扑排序(因为环里一定存在相互依赖的两个课程,A 需要先学完 B 才能学,B 又需要先学完 A 才能学)

2、怎么表示图?

  • 用 List<List<Integer>> graph 表示,index 是起始节点的课程的标志 
  • graph.get(index) 这个 List 就是这个起始课程可以指向的课程
  • 用 prerequisites 初始化这个 graph ,[ai, bi] 学习 ai 必须要先学课程 bi,即有一条从 bi (prerequisites[i][1]) 指向 ai (prerequisites[i][0]) 的有向边

3、InDegree 入度数组有什么用?队列怎么用?

  • 先初始化所有节点的入度,把入度为 0 的节点加入队列
  • 每次从队列取出(poll)一个入度为 0 的节点,将其指向的所有节点(从邻接表获得)的入度 -1,这时要判断如果有节点入度减为 0 了,要将其入队
  • 如果最后所有节点的入度都为 0,说明不存在环,可以有一个拓扑排序
  • 如果最后有节点的入度不为 0说明存在环,总有节点入度不为 0,不能有拓扑排序
  • 要有一个节点总数的计数,每次有节点入度减到0,除了要将其加入队列,还要将这个计数减一。这样在最后通过这个数值是否为0判断是都存在环。
class Solution {

      // 邻接表
    List<List<Integer>> graph;public boolean canFinish(int numCourses, int[][] prerequisites) {
        graph = new ArrayList();
        int[] inDegree = new int[numCourses];
        // 初始化邻接表
        for(int i=0;i<numCourses;i++) {
            inDegree[i] = 0;
            List<Integer> courseList = new ArrayList();
            graph.add(courseList);
        }
        for(int[] link: prerequisites) {
            // [ai, bi] 学习 ai 必须要先学课程 bi
            // 即有一条从 bi (prerequisites[i][1])指向 ai (prerequisites[i][0]) 的有向边
            graph.get(link[1]).add(link[0]);
            // bi 的入度 +1
            inDegree[link[0]] = ++inDegree[link[0]];
        }

        Queue<Integer> queue = new LinkedList();
        for (int i=0;i<inDegree.length;i++) {
            if (inDegree[i] == 0) {
                // 入度为 0 的节点入队
                queue.offer(i);
                // 只要有入度为0的节点,就减一
                numCourses--;
            }
        }

        while(!queue.isEmpty() && numCourses != 0) {
            // 队列里入度为0的节点出队
            int indegree0Course = queue.poll();
            List<Integer> nextCourseList = graph.get(indegree0Course);
            for (int nextCourse : nextCourseList) {
                // 对于入度为 0 的节点,将其指向的节点的入度 -1
                inDegree[nextCourse] = --inDegree[nextCourse];
                if (inDegree[nextCourse] == 0) {
                    queue.offer(nextCourse);
                    // 只要有入度为0的节点,就减一
                    numCourses--;
                }
            }
        }

        if (numCourses != 0) {
            // 最后还有入度不为 0 的节点,说明存在环,不能拓扑排序
            return false;
        }
        else {
            return true;
        }
    }
}

 

994. 腐烂的橘子

 

用 BFS 类似于树的层序遍历

刚开始腐烂的橘子是第一层,这第一层可以同时发起感染,如果是新鲜橘子,就会被感染到。这次感染到的就是第二层,第二层的再同时发起感染,这次感染到的就是第三层.........

所以最后的结果就是看,层序遍历至少到第几层的时候,全部的新鲜橘子都被感染了。

代码过程如下:

  • 先遍历一下grid(1)统计有新鲜橘子的数量 goodOrangeCnt(2)再把初始时的所有腐烂橘子加入 queue ,作为层序遍历的第一层
  • 开始层序遍历,注意要统计层数,所以每层的遍历要分开,外层 while 循环获取 queue.size ,然后再内层 for 循环 queue.size 次
  • 在四个方向上,如果是新鲜的橘子,(1)就进行感染(值设为2),(2)同时新鲜橘子计数 goodOrangeCnt--,(3)再将这个新被感染的加入队列queue,作为下一层
  • 最后如果新鲜橘子计数  goodOrangeCnt 大于0,说明有不能被感染的,返回 -1。否则就返回层数。

注意 while 循环的条件,还有一个goodOrangeCnt>0,也就是说如果橘子已经感染完了,但层序遍历还没完,也要退出循环

while (goodOrangeCnt>0 && !queue.isEmpty()) {
class Solution {

    private static int res = 0;

    Solution() {
        res = 0;
    }

    public int orangesRotting(int[][] grid) {
        int R = grid.length;
        int C = grid[0].length;
        int goodOrangeCnt = 0;
        Queue<int[]> queue = new LinkedList<>();
        for (int r=0;r<R;r++) {
            for (int c=0;c<C;c++) {
                // 统计所有新鲜橘子的数量
                if (grid[r][c]==1) {
                    goodOrangeCnt++;
                }
                // 将初始时就腐烂的橘子加入队列。它们是层序遍历的第一层
                if (grid[r][c]==2) {
                    queue.add(new int[]{r,c});
                }
            }
        }

        int layer = 0;
        // 注意条件还有一个goodOrangeCnt>0,也就是说如果橘子已经感染完了,但层序遍历还没完,也要退出循环
        while (goodOrangeCnt>0 && !queue.isEmpty()) {
            // 为了分开层序遍历的每一层,应该在开始时获得这一层的数量
            int curLayerSize = queue.size();
            // 同一层的腐烂橘子应该是一起去感染的,所以最后的结果就是层数
            layer++;
            // 然后每次循环是这一层的数量,把这一层搞完
            for (int i=0;i<curLayerSize;i++) {
                // 从队列里取出腐烂的橘子
                int[] rotOrange = queue.poll();
                int rotOrangeR = rotOrange[0];
                int rotOrangeC = rotOrange[1];
                // 它四周的四个橘子,如果是新鲜的,就去感染它,并且把新鲜橘子计数-1
                if (rotOrangeR-1>=0 && grid[rotOrangeR-1][rotOrangeC]==1) {
                    goodOrangeCnt--;
                    grid[rotOrangeR-1][rotOrangeC]=2;
                    queue.add(new int[] {rotOrangeR-1, rotOrangeC});
                }
                if (rotOrangeC-1>=0 && grid[rotOrangeR][rotOrangeC-1]==1) {
                    goodOrangeCnt--;
                    grid[rotOrangeR][rotOrangeC-1]=2;
                    queue.add(new int[] {rotOrangeR, rotOrangeC-1});
                }
                if (rotOrangeR+1<R && grid[rotOrangeR+1][rotOrangeC]==1) {
                    goodOrangeCnt--;
                    grid[rotOrangeR+1][rotOrangeC]=2;
                    queue.add(new int[] {rotOrangeR+1, rotOrangeC});
                }
                if (rotOrangeC+1<C && grid[rotOrangeR][rotOrangeC+1]==1) {
                    goodOrangeCnt--;
                    grid[rotOrangeR][rotOrangeC+1]=2;
                    queue.add(new int[] {rotOrangeR, rotOrangeC+1});
                }
            }
        }
        if (goodOrangeCnt>0) {
            return -1;
        }
        else {
            return layer;
        }
    }
}

 

208. 实现 Trie (前缀树)

 包含三个单词 "sea","sells","she" 的 Trie 会长啥样呢?

 

 下标隐式的定义了对应的字符,同时对于末端的字符结束,要有一个 isEnd 来表示是否是一个字符的结束

根节点,就是 Trie root = this

char c = word.charAt(i);
int cindex = c - 'a';
root.children[cindex]
class Trie {

    private Trie[] children;

    private boolean isEnd; 

    public Trie() {
        // 每个下标是否有 child 表示这个字符是否有
        children = new Trie[26];
        isEnd = false;
    }
    
    public void insert(String word) {
        // 从根节点开始,根节点 = this
        Trie root = this;
        for (int i=0;i<word.length();i++) {
            char c = word.charAt(i);
            int cindex = c - 'a';
            // 往下找,没有的话就添加新节点
            if (root.children[cindex] == null){
                Trie newTrie = new Trie();
                root.children[cindex] = newTrie;
            }
            root = root.children[cindex];
        }
        // 无论最后的这个是新加的,还是以前就有的。isEnd 都要新设为 true
        root.isEnd = true;
    }
    
    public boolean search(String word) {
        Trie node = searchNode(word);
        // 有这样的节点,并且该节点是最后一个
        return node != null && node.isEnd;
    }
    
    public boolean startsWith(String prefix) {
        return searchNode(prefix) != null;
    }

    public Trie searchNode(String prefix) {
        // 从根节点开始,根节点 = this
        Trie root = this;
        for (int i=0;i<prefix.length();i++) {
            char c = prefix.charAt(i);
            int cindex = c - 'a';
            if (root.children[cindex] == null){
                return null;
            }
            // 往下找
            root = root.children[cindex];
        }
        return root;
    }
}

/**
 * Your Trie object will be instantiated and called as such:
 * Trie obj = new Trie();
 * obj.insert(word);
 * boolean param_2 = obj.search(word);
 * boolean param_3 = obj.startsWith(prefix);
 */

 

 79. 单词搜索

 进行 dfs

  • dfs 的起点是什么?
    • 棋盘上所有元素值为 word.charAt(0) 的元素
  • 除了board和dfs到的当前元素i,j,还需要什么变量?
    • 由于同一个单元格里的字母不允许被重复使用,所以需要一个 boolean[][] visited 访问数组,来标志一次dfs中,这个元素有没有被访问过(初始false,访问过了标为true)
    • 需要一个变量k来表示前面已经完成了word从0到k-1的字母,现在需要寻找的是word.charAt(k)的字母
  • 怎样判断这一步的dfs不是有效的?
    1. 超过了board边界
    2. 已经被访问过
    3. 不等于当前期望的字符 board[i][j] != word.charAt(k)
  • 怎么判断当前dfs已经找到棋盘上等于word的路径?
    • 当 k==word.length 时
class Solution {

    Boolean res = false;

    Solution() {
        res = false;
    }

    public boolean exist(char[][] board, String word) {
        // 从那些和 word 第一个字符一样的元素开始 dfs
        for(int i=0;i<board.length;i++) {
            for(int j=0;j<board[0].length;j++) {
                if(word.charAt(0) == board[i][j]) {
                    dfs(board, getInitVisited(board), word, i, j, 0);
                    // 每次 dfs 完都判断一下,如果已经是true了,可以提前退出
                    if (res) {
                        break;
                    }
                }
            }
        }
        return res;
    }

    private void dfs(char[][] board, boolean[][] visited, String word, int i, int j, int k) {
        // 不是有效的,直接返回
        if (!isValid(board, visited, word, i, j, k)) {
            return;
        }
        // 到最后一个字符了,与word的相等, 就说明获得了结果,可以返回了
        if (k == word.length()-1) {
            res = true;
            return;
        }
        visited[i][j] = true;
        dfs(board, visited, word, i-1, j, k+1);
        dfs(board, visited, word, i+1, j, k+1);
        dfs(board, visited, word, i, j-1, k+1);
        dfs(board, visited, word, i, j+1, k+1);
        // 回溯
        visited[i][j] = false;
    }

    private boolean isValid(char[][] board, boolean[][] visited, String word,int i, int j, int k) {
        // 1.超出边界
        if (i<0 || i>=board.length) {
            return false;
        }
        else if (j<0 || j>=board[0].length) {
            return false;
        }
         // 2.已经参观过
        else if (visited[i][j]) {
            return false;
        }
        // 3.不是期望的那个字符
        else if (word.charAt(k) != board[i][j]) {
            return false;
        }
        return true;
    }

    private boolean[][] getInitVisited(char[][] board) {
        boolean[][] visited = new boolean[board.length][board[0].length];
        for (int i=0;i<board.length;i++) {
            for (int j=0;j<board[0].length;j++) {
                visited[i][j] = false;
            }
        }
        return visited;
    }
}