跳至主要內容

力扣Hot100-图论篇

chenxi编程算法大约 6 分钟

力扣Hot100图论篇-图1

200.岛屿数量

原题链接open in new window

前置知识

网格图:
不同于普通图,网格图一般直接以二维数组的方式给出,例如该题:char[][] grid
针对网格图,我们无需记录其边,对于每个结点其能移动的方向一般是固定的,例如上下左右(四连通)。
岛屿问题:
本题还是一道标准的岛屿问题,即网格图中每一个结点若是为 1,则代表一个陆地格子,相邻的陆地格子则构成岛屿;若是为 0,则代表海洋格子。
DFS 板子:
回想下,在二叉树问题中,是如何使用 DFS 的。

void traverse(TreeNode root) {
    // 剪枝,不必再向下遍历了
    if (root == null) {
        return;
    }
    // 继续向下遍历,访问左右儿子
    traverse(root.left);
    traverse(root.right);
}

其中的两个关键点:

  • 剪枝条件(二叉树中结点为空则进行剪枝)
  • 向哪里访问(二叉树中是对左右儿子进行访问)

对应到岛屿问题:

  • 剪枝条件(结点超出图的范围,结点为海洋格子,结点已遍历过,都需要进行剪枝)
  • 向哪里访问(结点的上下左右四个方向的格子)

代码实现

class Solution {
    private int dx[] = {0, 1, 0, -1};
    private int dy[] = {1, 0, -1, 0};
    private int n, m;

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

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

        return count;
    }

    private void dfs(char[][] grid, int x, int y) {
        // 结点超出图的范围,剪枝
        if (x < 0 || x >= n || y < 0 || y >= m) return;
        // 结点为海洋格子或已经遍历过,则也剪枝
        if (grid[x][y] == '0' || grid[x][y] == '2') return;
        // 遍历过的岛屿将其值设为 2
        grid[x][y] = '2';
        // 对当前结点的上下左右四个方向进行访问
        for (int i = 0; i < 4; i++) {
            int xx = x + dx[i], yy = y + dy[i];
            dfs(grid, xx, yy);
        }
    }
}

994.腐烂的橘子

原题链接open in new window

前置知识

BFS 中记录层数
本题要求返回:单元格中没有新鲜橘子为止所必须经过的最小分钟数。
要求遍历完整张图,且涉及到 最短/最小 这种字样,很容易让人想到 BFS
而这道题也确实是使用 BFS 来完成,但有一个问题:

  • 如何记录时间

解决方法:

  • 在 BFS 中一次遍历一层,记录层数,也就对应腐烂橘子污染新鲜橘子所用的时间

代码实现

class Solution {
    public int orangesRotting(int[][] grid) {
        Queue<int[]> queue = new LinkedList<>();
        int n = grid.length, m = grid[0].length;
        // 记录新鲜橘子树
        int count = 0;
        // 1.将最初腐烂的橘子加入队列
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == 2) queue.offer(new int[]{i, j});
                if (grid[i][j] == 1) count++;
            }
        }
        // 记录时间,亦是层数
        int time = 0;
        // 2.利用 bfs 进行遍历
        while (!queue.isEmpty()) {
            if (count == 0) break;
            // 一次取出当前的一层
            int size = queue.size();
            time++;
            for (int i = 0; i < size; i++) {
                int[] org = queue.poll();
                int x = org[0], y = org[1];
                // 腐烂新鲜橘子,并将腐烂后的橘子加入队列
                if (y + 1 < m && grid[x][y + 1] == 1) {
                    grid[x][y + 1] = 2;
                    queue.offer(new int[]{x, y + 1});
                    count--;
                }
                if (x + 1 < n && grid[x + 1][y] == 1) {
                    grid[x + 1][y] = 2;
                    queue.offer(new int[]{x + 1, y});
                    count--;
                }
                if (y - 1 >= 0 && grid[x][y - 1] == 1) {
                    grid[x][y - 1] = 2;
                    queue.offer(new int[]{x, y - 1});
                    count--;
                }
                if (x - 1 >= 0 && grid[x - 1][y] == 1) {
                    grid[x - 1][y] = 2;
                    queue.offer(new int[]{x - 1, y});
                    count--;
                }
            }
        }

        return count == 0 ? time : -1;
    }
}

207.课程表

原题链接open in new window

前置知识

拓扑序列
拓扑序列是对一个有向无环图而言的,有向图的拓扑序列是其顶点的线性排序,使得对每条有向边(u, v),u 在序列中出现在 v 之前,对于一个有向无环图而言,拓扑序列并不是唯一的。
举个例子吧:
力扣Hot100图论篇-图2
有了拓扑序列的前置知识,再回头看本题:

  • 我们每次只能选当前能上的课,即入度为 0 的课,因为它不依赖其他课
  • 选了入度为 0 的课后,依赖该课的课的入度要减 1,因为我们已经上了前置课程
  • 以此类推,直至选不了入度为 0 的课

也就是说本题的本质就是让我们求图是否存在一个拓扑序列,如何求拓扑序列?通过 BFS,每次让入度为 0 的结点入队,以此来进行遍历。

代码实现

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 记录结点的入度
        int[] inDegree = new int[numCourses];
        // 记录映射,val 为依赖于 key 的课程集合
        Map<Integer, List<Integer>> hashTable = new HashMap<>();
        // 初始化
        for (int i = 0; i < prerequisites.length; i++) {
            int a = prerequisites[i][0], b = prerequisites[i][1];
            if (hashTable.containsKey(b)) {
                hashTable.get(b).add(a);
                inDegree[a]++;
            } else {
                hashTable.put(b, new ArrayList<>(Arrays.asList(a)));
                inDegree[a]++;
            }
        }

        Queue<Integer> queue = new LinkedList<>();
        // 将入度为 0 的结点入队
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }
        int count = numCourses;
        // BFS
        while (!queue.isEmpty()) {
            int course = queue.poll();
            count--;
            if (count == 0) break;

            List<Integer> list = hashTable.get(course);
            if (list != null && list.size() > 0) {
                for (Integer a : list) {
                    inDegree[a]--;
                    // 将入度为 0 的结点入队
                    if (inDegree[a] == 0) {
                        queue.offer(a);
                    } 
                }
            }
        }

        return count == 0;
    }
}

200.实现 Trie(前缀树)

前置知识

前缀树
先吐槽下,这道题真的能被划为图论么。。。
回想下,二叉树结点是怎么储存的:

public class TreeNode {
    int val; // 当前结点值
    TreeNode left; // 左孩子
    TreeNode right; // 有孩子
}

那针对该题的前缀树结点又该如何储存呢?

class Trie {
    private boolean isEnd; // 当前结点储存的值是否是一个完整单词的结束
    private Trie[] next; // 字母映射,对于本题,next 的大小应为 26
}

关键点:

  • 对于每一个前缀树结点,其孩子最多有 26 个,也有可能一个没有
  • 每一个前缀树结点,我们并不知道其储存的值是多少,所以进行单词查找操作,都是从其孩子开始进行

代码实现

class Trie {
    private boolean isEnd;
    private Trie[] next;

    public Trie() {
        isEnd = false;
        next = new Trie[26];
    }
    
    public void insert(String word) {
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            if (node.next[ch - 'a'] == null) node.next[ch - 'a'] = new Trie();
            node = node.next[ch - 'a'];
        }
        node.isEnd = true;
    }
    
    public boolean search(String word) {
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            if (node.next[ch - 'a'] == null) return false;
            node = node.next[ch - 'a'];
        }

        return node.isEnd == true;
    }
    
    public boolean startsWith(String prefix) {
        Trie node = this;
        for (int i = 0; i < prefix.length(); i++) {
            char ch = prefix.charAt(i);
            if (node.next[ch - 'a'] == null) return false;
            node = node.next[ch - 'a'];
        }

        return true;
    }
}

参考资料:
岛屿类问题的通用解法、DFS 遍历框架open in new window
「图解」拓扑排序 | 课程表问题open in new window
Trie Tree 的实现 (适合初学者)🌳open in new window

上次编辑于:
贡献者: chenxi