LeetCode题解图论篇(TS版)

解题技巧

深度优先搜索(DFS,Depth-First Search)和广度优先搜索(BFS,Breadth-First Search)都是用来遍历或搜索树或图的算法。它们的主要区别体现在搜索顺序和使用的数据结构上。

  1. 搜索顺序

    • DFS:深度优先搜索会尽可能深地搜索树的分支,直到到达给定分支的末端,然后再回溯继续下一分支。换句话说,DFS优先沿着某一支路线深入,直到不能再深入为止,然后再按路径回溯。
    • BFS:广度优先搜索则是从根节点开始,先访问其所有相邻的节点,然后再按照访问顺序访问这些节点的子节点。简而言之,BFS是按层次依次访问的。
  2. 使用的数据结构

    • DFS:通常使用堆栈实现。在递归实现中,系统的调用栈本身可以作为堆栈。
    • BFS:通常使用队列实现,将相邻节点逐个加入队列中,然后逐个访问。
  3. 空间复杂度

    • DFS:空间复杂度与深度有关,最坏情况下可能需要存储所有节点,因此是O(|V|),其中V是节点数量。
    • BFS:在最坏的情况下,整个图的所有节点都可能同时在队列中,因此空间复杂度也是O(|V|)。
  4. 时间复杂度

    • 两者的时间复杂度在无权图中通常相同,都是O(|V| + |E|),其中V是顶点数量,E是边的数量。
  5. 是否找到最短路径

    • DFS:不一定能找到最短路径。
    • BFS:对于无权图,总是能找到最短路径。
  6. 适用场景

    • DFS:适用于目标明确,需要深入探索的场景,或者需要找到所有可能解的问题。
    • BFS:适用于最短路径问题或者问题的解在浅层次上。
  7. 其他特性

    • DFS:可能导致路径很深时探索耗时,且可能陷入死循环(在非树结构中)。
    • BFS:在内存限制严格的场合下可能不适用,因为在存储宽度较大的层时可能消耗大量内存。

DFS的代码框架:

void dfs(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本节点所连接的其他节点) {
        处理节点;
        dfs(图,选择的节点); // 递归
        回溯,撤销处理结果
    }
}

BFS:

使用队列来存储节点

797.所有可能的路径

给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序

graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)。

示例 1:

img
输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3

示例 2:

img
输入:graph = [[4,3,1],[3,2,4],[3],[4],[]]
输出:[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]

提示:

  • n == graph.length
  • 2 <= n <= 15
  • 0 <= graph[i][j] < n
  • graph[i][j] != i(即不存在自环)
  • graph[i] 中的所有元素 互不相同
  • 保证输入为 有向无环图(DAG)

DFS-回溯

  • 时间复杂度: O ( n × 2 n ) O(n × 2^n) O(n×2n),n为图中点的数量。
  • 空间复杂度: O ( n ) O(n) O(n)
function allPathsSourceTarget(graph: number[][]): number[][] {
    const res: number[][] = [];
    const path: number[] = [];
    const dfs = (x: number) => {
        if (x === graph.length - 1) { // 终止条件,题目要求 0 到 n-1 的节点路径
            res.push(path.slice());
            return;
        }
        for (let i = 0; i < graph[x].length; i++) {
            path.push(graph[x][i]);
            dfs(graph[x][i])
            path.pop();
        }
    }
    path.push(0);
    dfs(0);
    return res;
};

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'

DFS

解题思路:

  1. 遍历网格:遍历整个二维网格,一旦找到一个’1’,就从那里开始执行深度优先搜索,并将岛屿数量加1。

  2. 深度优先搜索:在深度优先搜索中,我们会访问当前’1’所连接的所有’1’,并将它们标记为已访问(例如,可以将它们标记为’2’或者使用另一个同样大小的二维数组来跟踪访问状态)。

    a. 递归调用:对于当前’1’,我们将查看它上下左右四个方向的邻居,并递归地对每一个未访问过的’1’邻居进行深度优先搜索。

    b. 边界条件:在查看邻居时,要确保不会走出网格的边界,并且仅在找到’1’时才递归。

    c. 标记访问:在访问一个’1’之后,要立即将其标记为已访问,以避免重复访问。

  3. 返回岛屿数量:在遍历完整个网格并进行了深度优先搜索之后,返回岛屿数量。

iShot_2023-08-09_07.11.23

iShot_2023-08-09_07.30.23

  • 时间复杂度: O ( m ∗ n ) O(m*n) O(mn),m和n分别为行数和列数
  • 空间复杂度: O ( m ∗ n ) O(m*n) O(mn)
function numIslands(grid: string[][]): number {
    const dfs = (i: number, j: number) => {
        if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) return; // 超出边界
        if (grid[i][j] === '0' || grid[i][j] === '2') return;

        grid[i][j] = '2'

        dfs(i - 1, j);
        dfs(i + 1, j);
        dfs(i, j - 1);
        dfs(i, j + 1);
    }

    let count = 0;
    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[i].length; j++) {
            if (grid[i][j] === '1') {
                dfs(i, j);
                count++;
            }
        }
    }
    return count;
};

BFS

解题思路:

广度优先搜索

a. 出队列并访问:从队列中取出一个坐标,并检查其上下左右四个方向的邻居。

b. 检查邻居:对于每个未访问过的’1’邻居,将其坐标添加到队列中,并将其标记为已访问(可以通过将其值改为’2’来实现)。

c. 继续迭代:重复上述过程,直到队列为空。此时,一个岛屿的所有部分都已被访问。

iShot_2023-08-09_07.31.08

  • 时间复杂度: O ( m ∗ n ) O(m*n) O(mn),m和n分别为行数和列数
  • 空间复杂度: O ( m i n ( ( m ∗ n ) ) ) O(min((m*n))) O(min((mn)))
function numIslands(grid: string[][]): number {
    const bfs = (i: number, j: number) => {
        const queue: number[][] = [];
        queue.push([i, j]);
        while (queue.length) {
            const [r, c] = queue.shift();
            if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) continue;
            if (grid[r][c] === '0' || grid[r][c] === '2') continue;
            grid[r][c] = '2';
            queue.push([r + 1, c]);
            queue.push([r - 1, c]);
            queue.push([r, c + 1]);
            queue.push([r, c - 1]);
        }
    }

    let count = 0;
    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[i].length; j++) {
            if (grid[i][j] === '1') {
                bfs(i, j);
                count++
            }
        }
    }
    return count;
};

695.岛屿的最大面积

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

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

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

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

示例 1:

img
输入: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 。

示例 2:

输入:grid = [[0,0,0,0,0,0,0,0]]
输出:0

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 50
  • grid[i][j]01

DFS

  • 时间复杂度: O ( m ∗ n ) O(m*n) O(mn),m和n分别为行数和列数
  • 空间复杂度: O ( m ∗ n ) O(m*n) O(mn)
function maxAreaOfIsland(grid: number[][]): number {
    const dfs = (i: number, j: number) => {
        if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) return 0;
        if (grid[i][j] === 0 || grid[i][j] === 2) return 0;
        grid[i][j] = 2;
        let area = 1;
        area += dfs(i + 1, j);
        area += dfs(i - 1, j)
        area += dfs(i, j + 1);
        area += dfs(i, j - 1);
        return area;
    }

    let maxArea = 0;
    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[i].length; j++) {
            if (grid[i][j] === 1) {
                maxArea = Math.max(maxArea, dfs(i, j))
            }
        }
    }
    return maxArea;
};

BFS

  • 时间复杂度: O ( m ∗ n ) O(m*n) O(mn),m和n分别为行数和列数
  • 空间复杂度: O ( m i n ( ( m ∗ n ) ) ) O(min((m*n))) O(min((mn)))
function maxAreaOfIsland(grid: number[][]): number {
    const bfs = (i: number, j: number) => {
        const queue: number[][] = [];
        queue.push([i, j]);
        let area = 0
        while (queue.length) {
            const [r, c] = queue.shift();
            if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) continue;
            if (grid[r][c] === 0 || grid[r][c] === 2) continue;
            area++;
            grid[r][c] = 2;
            queue.push([r + 1, c]);
            queue.push([r - 1, c]);
            queue.push([r, c + 1]);
            queue.push([r, c - 1]);
        }
        return area;
    }

    let maxArea = 0;
    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[i].length; j++) {
            if (grid[i][j] === 1) {
                maxArea = Math.max(maxArea, bfs(i, j));
            }
        }
    }
    return maxArea;
};

1020.飞地的数量

给你一个大小为 m x n 的二进制矩阵 grid ,其中 0 表示一个海洋单元格、1 表示一个陆地单元格。

一次 移动 是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过 grid 的边界。

返回网格中 无法 在任意次数的移动中离开网格边界的陆地单元格的数量。

示例 1:

img
输入:grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]]
输出:3
解释:有三个 1 被 0 包围。一个 1 没有被包围,因为它在边界上。

示例 2:

img
输入:grid = [[0,1,1,0],[0,0,1,0],[0,0,1,0],[0,0,0,0]]
输出:0
解释:所有 1 都在边界上或可以到达边界。

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 500
  • grid[i][j] 的值为 01

DFS

  • 时间复杂度: O ( m ∗ n ) O(m*n) O(mn),m和n分别为行数和列数
  • 空间复杂度: O ( m ∗ n ) O(m*n) O(mn)
function numEnclaves(grid: number[][]): number {
    const dfs = (i: number, j: number) => {
        if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) return Infinity;
        if (grid[i][j] === 0 || grid[i][j] === 2) return 0;

        grid[i][j] = 2;
        let count = 1;
        count += dfs(i + 1, j);
        count += dfs(i - 1, j);
        count += dfs(i, j + 1);
        count += dfs(i, j - 1);

        return count;
    }

    let count = 0;
    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[i].length; j++) {
            if (grid[i][j] === 1) {
                const curCount = dfs(i, j);
                if (curCount !== Infinity) {
                    count += curCount;
                }
            }
        }
    }

    return count;
};

BFS

  • 时间复杂度: O ( m ∗ n ) O(m*n) O(mn),m和n分别为行数和列数
  • 空间复杂度: O ( m i n ( ( m ∗ n ) ) ) O(min((m*n))) O(min((mn)))
function numEnclaves(grid: number[][]): number {
    const bfs = (i: number, j: number) => {
        const queue: number[][] = [];
        queue.push([i, j]);
        let count = 0;
        let isEdge = false
        while (queue.length) {
            const [r, c] = queue.shift();
            if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) {
                isEdge = true;
                continue;
            }
            if (grid[r][c] === 0 || grid[r][c] === 2) continue;
            grid[r][c] = 2;
            count++;
            queue.push([r + 1, c]);
            queue.push([r - 1, c]);
            queue.push([r, c + 1]);
            queue.push([r, c - 1]);
        }
        return isEdge ? 0 : count;
    }

    let ans = 0;
    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[i].length; j++) {
            if (grid[i][j] === 1) {
                ans += bfs(i, j);
            }
        }
    }
    return ans;
};

130.被围绕的区域

给你一个 m x n 的矩阵 board ,由若干字符 'X''O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O''X' 填充。

示例 1:

img
输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]]
输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。

示例 2:

输入:board = [["X"]]
输出:[["X"]]

提示:

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 200
  • board[i][j]'X''O'

DFS

解题思路:

从边缘开始,将边缘相连的全部置为#,之后再遍历一次,遇到O置为X,遇到#置为O

  • 时间复杂度: O ( m ∗ n ) O(m*n) O(mn),m和n分别为行数和列数
  • 空间复杂度: O ( m ∗ n ) O(m*n) O(mn)
/**
 Do not return anything, modify board in-place instead.
 */
function solve(board: string[][]): void {
    const dfs = (i: number, j: number) => {
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length) return;
        if (board[i][j] === '#' || board[i][j] === 'X') return;
        board[i][j] = '#';
        dfs(i + 1, j);
        dfs(i - 1, j);
        dfs(i, j + 1);
        dfs(i, j - 1);
    }

    for (let i = 0; i < board.length; i++) {
        for (let j = 0; j < board[i].length; j++) {
            const isEdge = i === 0 || j === 0 || i === board.length - 1 || j === board[i].length - 1
            if (isEdge && board[i][j] === 'O') {
                dfs(i, j);
            }
        }
    }

    for (let i = 0; i < board.length; i++) {
        for (let j = 0; j < board[i].length; j++) {
            if (board[i][j] === '#') {
                board[i][j] = 'O'
            } else if (board[i][j] === 'O') {
                board[i][j] = 'X'
            }
        }
    }
};

417.太平洋大西洋水流问题

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

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

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

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

示例 1:

img
输入: 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]]

示例 2:

输入: heights = [[2,1],[1,2]]
输出: [[0,0],[0,1],[1,0],[1,1]]

提示:

  • m == heights.length
  • n == heights[r].length
  • 1 <= m, n <= 200
  • 0 <= heights[r][c] <= 105

DFS

解题思路:

水往高处流,计算两个洋所能到达的点,重叠的点就是最终的结果。

8e9c842a24968824d18c4de2c520a6e.png 521bfa8063d14254466a5d7f6600ae9.png 06ce3f99a8742231c3f7d42dcac0c69.png
  • 时间复杂度: O ( m ∗ n ) O(m*n) O(mn)
  • 空间复杂度: O ( m ∗ n ) O(m*n) O(mn)
function pacificAtlantic(M: number[][]): number[][] {
    const n = M.length;
    const m = M[0].length;
    const P = Array.from({ length: n }, () => Array(m).fill(0));
    const A = Array.from({ length: n }, () => Array(m).fill(0));
    const ans: number[][] = [];

    function dfs(
        visited: number[][],
        i: number,
        j: number,
    ): void {
        if (visited[i][j]) return;
        visited[i][j] = 1;

        if (A[i][j] && P[i][j]) ans.push([i, j]);

        if (i - 1 >= 0 && M[i - 1][j] >= M[i][j]) dfs(visited, i - 1, j,);
        if (i + 1 < n && M[i + 1][j] >= M[i][j]) dfs(visited, i + 1, j,);
        if (j - 1 >= 0 && M[i][j - 1] >= M[i][j]) dfs(visited, i, j - 1,);
        if (j + 1 < m && M[i][j + 1] >= M[i][j]) dfs(visited, i, j + 1,);
    }

    for (let i = 0; i < n; ++i) {
        dfs(P, i, 0,);
        dfs(A, i, m - 1,);
    }
    for (let j = 0; j < m; ++j) {
        dfs(P, 0, j,);
        dfs(A, n - 1, j,);
    }

    return ans;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值