力扣100题解及笔记 图论

目录

1.力扣100题解及笔记 哈希-CSDN博客

2.力扣100题解及笔记 堆-CSDN博客

3.力扣100题解及笔记 栈-CSDN博客

4.力扣100题解及笔记 双指针-CSDN博客

5.力扣100题解及笔记 链表-CSDN博客

6.力扣100题解及笔记 二叉树-CSDN博客

7.力扣100题解及笔记 二分查找-CSDN博客

8.力扣100及题解 滑动窗口&子串-CSDN博客

9.力扣100题解及笔记 回溯-CSDN博客

10.力扣100题解及笔记 dp&多维dp-CSDN博客

11.力扣100题解及笔记 贪心-CSDN博客

12.力扣100题解及笔记 数组-CSDN博客

13.力扣100题解及笔记 技巧-CSDN博客

14.力扣100题解及笔记 矩阵-CSDN博客

15.力扣100题解及笔记 图论-CSDN博客

理论基础

详细的去看离散数学,以下快速回忆概念

有向图、无向图、加权图

入度、出度

强连通图、连通分量

邻接矩阵(二维矩阵)、邻接表(数组 + 链表)

深搜&广搜

深搜:二叉树递归遍历、回溯等等,原理通用

广搜:二叉树层序遍历,一般用队列,原理通用

depth = 0 # 记录遍历到第几层
while queue 非空:
    depth++
    n = queue 中的元素个数
    循环 n 次:
        node = queue.pop()
        for node 的所有相邻结点 m:
            if m 未访问过:
                queue.push(m)

并查:两个元素在不在同一个集合

拓扑:一系列依赖关系,有向图转成线性的排序 

最小生成树(p和k)、最短路径(d)hot100没有涉及

200.岛屿

200. 岛屿数量

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

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

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

 

示例 1:

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1
示例 2:

输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3
 

提示:

m == grid.length
n == grid[i].length
1 <= m, n <= 300
grid[i][j] 的值为 '0' 或 '1'

class Solution {
    public int numIslands(char[][] grid) {
        int count = 0;
        for(int i = 0; i < grid.length; i++) {  // i: 当前行的索引
            for(int j = 0; j < grid[0].length; j++) {  // j: 当前列的索引
                if(grid[i][j] == '1'){// 如果当前位置是 '1',说明找到了一个新的岛屿
                    dfs(grid, i, j);// 使用DFS将整个岛屿标记为访问过
                    count++;
                }
            }
        }
        return count;
    }
    //DFS
    private void dfs(char[][] grid, int i, int j) {
        // 检查边界条件,防止越界,以及是否遇到 '0'(水域),如果是,直接返回
        if(i < 0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == '0') return;
        grid[i][j] = '0';// 将当前格子标记为 '0',表示已访问
        dfs(grid, i + 1, j);  // 向下
        dfs(grid, i, j + 1);  // 向右
        dfs(grid, i - 1, j);  // 向上
        dfs(grid, i, j - 1);  // 向左
    }
    // BFS
    private void bfs(char[][] grid, int i, int j){
        Queue<int[]> list = new LinkedList<>(); // 使用队列存储需要访问的格子的坐标
        list.add(new int[] { i, j });
        while(!list.isEmpty()){
            int[] cur = list.remove(); // 取出队列头部的元素,表示当前格子的坐标
            i = cur[0]; j = cur[1];  // 获取当前格子的行和列索引
            // 检查边界条件,防止越界,以及是否遇到 '0'(水域),如果是,则跳过
            if(0 <= i && i < grid.length && 0 <= j && j < grid[0].length && grid[i][j] == '1') {
                grid[i][j] = '0';// 将当前格子标记为 '0',表示已访问
                list.add(new int[] { i + 1, j });  // 向下
                list.add(new int[] { i - 1, j });  // 向上
                list.add(new int[] { i, j + 1 });  // 向右
                list.add(new int[] { i, j - 1 });  // 向左
            }
        }
    }
}

994.烂橘子

994. 腐烂的橘子

在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:

值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。

返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。

提示:

m == grid.length
n == grid[i].length
1 <= m, n <= 10
grid[i][j] 仅为 0、1 或 2

为什么用广搜,因为相邻的

class Solution {
    // DIRECTIONS: 四个方向的坐标偏移数组,分别表示上、下、左、右
    private static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; 
    public int orangesRotting(int[][] grid) {
        int m = grid.length; // 行
        int n = grid[0].length; // 列
        int fresh = 0; // 记录新鲜橘子的数量
        List<int[]> q = new ArrayList<>(); // q: 用于存储当前腐烂橘子的队列
        for (int i = 0; i < m; i++) { 
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) fresh++; // 统计新鲜橘子的数量
                else if (grid[i][j] == 2) q.add(new int[]{i, j}); // 将腐烂橘子的坐标加入队列
            }
        }
        int ans = -1; // ans: 记录经过的分钟数,初始化为-1,因为每轮开始时都会加1
        while (!q.isEmpty()) {
            ans++; // 每次进入新一轮,经过一分钟
            List<int[]> tmp = q; // tmp: 暂存当前轮次腐烂的橘子位置
            q = new ArrayList<>(); // 开始新一轮传播,初始化新的队列
            for (int[] pos : tmp) {  // 遍历当前轮次所有腐烂的橘子
                for (int[] d : DIRECTIONS) {  // 遍历四个方向
                    int i = pos[0] + d[0];  // 新的行坐标
                    int j = pos[1] + d[1];  // 新的列坐标
                    // 如果相邻位置在网格范围内且是新鲜橘子
                    if (0 <= i && i < m && 0 <= j && j < n && grid[i][j] == 1) {
                        fresh--;  // 新鲜橘子减少
                        grid[i][j] = 2;  // 将新鲜橘子变成腐烂橘子
                        q.add(new int[]{i, j});  // 将新腐烂的橘子加入队列
                    }
                }
            }
        }
        // 如果还有新鲜橘子剩余,则返回-1,否则返回经过的分钟数
        return fresh > 0 ? -1 : Math.max(ans, 0);
    }
}

207.课程表

207. 课程表

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程  bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

 
示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
 

提示:

1 <= numCourses <= 2000
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i] 中的所有课程对 互不相同

想要学习课程 0 ,你需要先完成课程 1   ->马上想到拓扑

通过拓扑排序判断此课程安排图是否是有向无环图

//BFS入度表
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        int[] indegrees = new int[numCourses]; // indegrees: 每门课程的入度,表示有多少前置课程
        List<List<Integer>> adjacency = new ArrayList<>(); // adjacency: 邻接表,用来存储每门课程的后继课程列表
        Queue<Integer> queue = new LinkedList<>();// queue: 用于BFS的队列,存储入度为0的课程       
        // 初始化邻接表,每个课程对应一个空的列表
        for(int i = 0; i < numCourses; i++)
            adjacency.add(new ArrayList<>());
        // 遍历先决条件数组,构建入度数组和邻接表
        for(int[] cp : prerequisites) {
            indegrees[cp[0]]++; // cp[0] 这门课的入度加1,表示它有一个前置课程
            adjacency.get(cp[1]).add(cp[0]); // 在邻接表中,把 cp[1] 指向 cp[0],表示 cp[0] 是 cp[1] 的后继课程
        }
        // 找到所有入度为0的课程,加入队列
        for(int i = 0; i < numCourses; i++)
            if(indegrees[i] == 0) queue.add(i);
        // BFS拓扑排序
        while(!queue.isEmpty()) {
            int pre = queue.poll(); // 取出一个入度为0的课程
            numCourses--; // 已完成的课程数,减少总的课程数量
            // 遍历这门课程的所有后继课程
            for(int cur : adjacency.get(pre)) {
                indegrees[cur]--; // 当前课程的入度减1,表示前置课程减少
                if(indegrees[cur] == 0) queue.add(cur); // 如果入度减为0,加入队列
            }
        }
        // 如果所有课程都被完成(即 numCourses == 0),返回true,否则返回false
        return numCourses == 0;
    }
}

//DFS 有环图
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<List<Integer>> adjacency = new ArrayList<>();// adjacency: 邻接表,用来存储每门课程的后继课程列表(依赖关系)
        // 初始化邻接表,每门课程对应一个空的列表
        for(int i = 0; i < numCourses; i++)
            adjacency.add(new ArrayList<>());   
        // flags: 标记数组,用来记录每门课程的访问状态
        // 0 表示未被访问,1 表示当前路径上正在访问,-1 表示该课程及其后继课程已访问完毕且无环
        int[] flags = new int[numCourses];
        // 构建邻接表,表示课程之间的依赖关系
        // prerequisites[i] = [a, b] 表示必须先完成课程 b 才能学习课程 a
        for(int[] cp : prerequisites)
            adjacency.get(cp[1]).add(cp[0]);
        // 对每门课程进行DFS遍历,检测是否存在环
        for(int i = 0; i < numCourses; i++)
            if(!dfs(adjacency, flags, i)) return false; // 如果某门课有环,返回 false
        return true; // 如果所有课程都能完成,返回 true
    }
    // dfs: 深度优先搜索检测是否存在环
    private boolean dfs(List<List<Integer>> adjacency, int[] flags, int i) {
        // 如果该课程正在访问(即已进入当前递归路径),说明存在环,返回 false
        if(flags[i] == 1) return false;
        // 如果该课程已经访问过且无环,直接返回 true
        if(flags[i] == -1) return true;
        // 标记该课程正在访问
        flags[i] = 1;
        // 遍历当前课程的所有后继课程
        for(Integer j : adjacency.get(i))
            if(!dfs(adjacency, flags, j)) return false; // 如果后继课程存在环,返回 false
        // 该课程及其所有后继课程已访问完毕且无环,标记为 -1
        flags[i] = -1;
        return true;
    }
}

208.前缀树

208. 实现 Trie (前缀树)

Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。

请你实现 Trie 类:

Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
 

示例:

输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]

解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // 返回 True
trie.search("app");     // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app");     // 返回 True
 

提示:

1 <= word.length, prefix.length <= 2000
word 和 prefix 仅由小写英文字母组成
insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次

class Trie {
    private Node root; // 根节点,Trie的起始点
    public Trie() {
        root = new Node(); // 初始化根节点
    }
    // 插入一个单词到Trie中
    public void insert(String word) {
        Node node = root; // 从根节点开始构造这个word对应的路径节点
        int n = word.length(); // 获取单词的长度
        for(int i = 0; i < n; i++){
            // 将当前字符添加到当前节点对应的子节点位置,然后递归更新
            int id = word.charAt(i) - 'a'; // 计算当前字符相对于'a'的索引
            if(node.children[id] == null){ // 如果该字符的子节点不存在
                node.children[id] = new Node(); // 创建新的子节点
            }
            node = node.children[id]; // 移动到当前字符对应的子节点
        }
        node.isEnd = true; // 最后一个节点的isEnd置为true,表示一个完整的字符串
    }
    // 查找是否存在完整的单词
    public boolean search(String word) {
        Node node = searchPrefix(word); // 查找单词的前缀
        return node != null && node.isEnd; // 返回不为空且节点标记为尾节点,则包含word这个完整的字符串
    }
    // 查找是否存在以指定前缀开头的单词
    public boolean startsWith(String prefix) {
        return searchPrefix(prefix) != null; // 返回不为空,则包含了prefix前缀
    }
    
    // 查找字典树是否包含word前缀
    private Node searchPrefix(String word){
        Node node = root; // 从根节点依次开始匹配每个字符
        int n = word.length(); // 获取前缀的长度
        for(int i = 0; i < n; i++){
            // 根据当前字符获取对应的子节点
            node = node.children[word.charAt(i) - 'a'];
            if(node == null){ // 如果当前节点为空,则不包含这个字符串,直接返回空指针
                return null;
            }
        }
        return node; // 否则匹配成功返回当前节点
    }
}

//字典树节点
class Node{
    Node[] children; // 子节点列表,存储每个字符的子节点
    boolean isEnd;   // 标记是否为尾节点,表示一个完整单词的结束
    public Node(){
        children = new Node[26]; // 初始化26个字母的子节点
        isEnd = false; // 初始时,节点不是尾节点
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值