代码随想录-图论(二)

72 阅读24分钟

图论-习题部分

T98-所有可达路径

见卡码网第98题[所有可达路径]

题目描述 给定一个有 n 个节点的有向无环图,节点编号从 1n。请编写一个函数,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。

输入

第一行包含两个整数 N,M,表示图中拥有 N 个节点,M 条边

后续 M 行,每行包含两个整数 s 和 t,表示图中的 s 节点与 t 节点中有一条路径

5 5
1 3
3 5
1 2
2 4
4 5

输出 输出所有的可达路径,路径中所有节点之间空格隔开,每条路径独占一行,存在多条路径,路径输出的顺序可任意。如果不存在任何一条路径,则输出 -1。

注意输出的序列中,最后一个节点后面没有空格!  例如正确的答案是 1 3 5,而不是 1 3 5 , 5后面没有空格!

1 3 5
1 2 4 5

我的思路

这一题主要是考察图的遍历。图的遍历有两种:DFS和BFS。当前题目求可达路径的数量,因此考虑使用图的DFS遍历。

回想DFS遍历的模板:

  1. 检查是否达到递归结束条件
  2. 对当前节点做处理
  3. 对子节点做处理
  4. 插销当前节点的处理

本题的答案也就呼之欲出了。

T200-岛屿数量

见LeetCode第200题[岛屿数量]

题目描述

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

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

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

示例 2

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

我的思路

岛屿位置的二维网格,可以当做是一张图。现在我们需要将这张无向图存储起来,然后去寻找其最大连通分量。

可以考虑使用广度搜索(BFS)寻找岛屿的数量。从一个为1的顶点开始,然后向四周扩散,直到所有的遍历所有相邻的顶点,这时岛屿数量加1。

寻找下一个未遍历到的顶点,重复进行广度优先搜索,直到所有的陆地点都被遍历到,返回结果即可。

[超时]

private boolean[][] isVisited;

private int counts;
/**
 * 岛屿数量
 * @param grid
 * @return
 */
public int numIslands(char[][] grid) {
    int m = grid.length;
    int n = grid[0].length;
    if (m == 1 && n == 1) return grid[0][0] == '0' ? 0 : 1;

    isVisited = new boolean[m][n];

    // 对图进行广度优先遍历
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            // 该顶点是陆地并且没有被访问过
            if (!isVisited[i][j] && grid[i][j] == '1') {
                bfs(grid, i, j);
            }
        }
    }
    return counts;
}

/**
 * 广度优先寻找岛屿数量
 * @param grid
 * @param row
 * @param col
 */
private void bfs(char[][] grid, int row, int col) {
    // 存储的是某个点的坐标
    Queue<int[]> q = new LinkedList<>();
    q.add(new int[]{row, col});

    while (!q.isEmpty()) {
        int[] point = q.poll();
        isVisited[point[0]][point[1]] = true;
        // 四周是否是为探访过的路地点
        // 上方
        if (point[0] - 1 >= 0 && grid[point[0] - 1][point[1]] == '1' && !isVisited[point[0] - 1][point[1]]) {
            q.add(new int[]{point[0] - 1, point[1]});
        }
        // 下方
        if (point[0] + 1 < grid.length && grid[point[0] + 1][point[1]] == '1' && !isVisited[point[0] + 1][point[1]]) {
            q.add(new int[]{point[0] + 1, point[1]});
        }
        // 左边
        if (point[1] - 1 >= 0 && grid[point[0]][point[1] - 1] == '1' && !isVisited[point[0]][point[1] - 1]) {
            q.add(new int[]{point[0], point[1] - 1});
        }
        // 右边
        if (point[1] + 1 < grid[0].length && grid[point[0]][point[1] + 1] == '1' && !isVisited[point[0]][point[1] + 1]) {
            q.add(new int[]{point[0], point[1] + 1});
        }
    }

    // 空了,岛屿数量加1
    counts++;
}

计算复杂度分析

  • 时间复杂度O(M×N)O(M \times N),最坏情况下,每一个节点都要访问一次
  • 空间复杂度O(min(M,N))O(\min(M, N)),最坏情况下,每个点都是陆地,需要队列的最大长度为对角线的长度

优化方案

在队里里头存储二维数组,还有创建二维数组的开销比较大,如何将一个点的坐标存储为一个整数,可以将其行坐标 row * N + col,即可。取出的时候为{point / N, point % N}

T695-岛屿的最大面积

见LeetCode第695题[岛屿的最大面积]

题目描述

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

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

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

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

示例

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

我的思路

岛屿的最大面积,同样可以使用广度优先搜索,计算每个岛屿的面积,然后更新最大面积。

可以原地修改数组,访问过的陆地点,直接置为0,不使用额外的数组来标记是否访问过了。此外,栈中存储的坐标使用row * N + col替代,进一步降低空间复杂度。

private int maxArea = 0;
/**
 * 岛屿的最大面积
 * @param grid
 * @return
 */
public int maxAreaOfIsland(int[][] grid) {

    if (grid.length == 1 && grid[0].length == 1) return grid[0][0] == 1 ? 1 : 0;

    int m = grid.length;
    int n = grid[0].length;
    // 对路地点进行广度优先搜索
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            // 如果没有被访问过并且是陆地面积,则对其进行bfs搜索
            if (grid[i][j] == 1) {
                bfs(i, j, grid, n);
            }
        }
    }
    return maxArea;
}

/**
 * 从当前节点开始对图进行BFS
 * @param row
 * @param col
 * @param grid
 * @param N 列数
 */
private void bfs(int row, int col, int[][] grid, int N) {
    int curArea = 1;
    // 将当前点加入队列
    Queue<Integer> neighbors = new LinkedList<>();
    neighbors.add(row * N + col);
    grid[row][col] = 0;

    // 处理队列中的元素
    while (!neighbors.isEmpty()) {
        int position = neighbors.poll();
        int i = position / N;
        int j = position % N;
        // 遍历四周,注意避免某个节点被重复加入
        if (i - 1 >= 0 && grid[i - 1][j] == 1) {
            grid[i - 1][j] = 0;
            neighbors.add((i - 1) * N + j);
            curArea++;
        }
        if (i + 1 < grid.length && grid[i + 1][j] == 1) {
            grid[i + 1][j] = 0;
            neighbors.add((i + 1) * N + j);
            curArea++;
        }
        if (j - 1 >= 0 && grid[i][j - 1] == 1) {
            grid[i][j - 1] = 0;
            neighbors.add(i * N + j - 1);
            curArea++;
        }
        if (j + 1 < N && grid[i][j + 1] == 1) {
            grid[i][j + 1] = 0;
            neighbors.add(i * N + j + 1);
            curArea++;
        }
    }
    // 更新最大面积
    maxArea = Math.max(maxArea, curArea);
}

深度优先搜索版本

private int maxArea = 0;
/**
 * 深度优先搜索版本
 * @param grid
 * @return
 */
public int maxAreaOfIsland(int[][] grid) {
    if (grid.length == 1 && grid[0].length == 1) return grid[0][0] == 1 ? 1 : 0;

    int m = grid.length;
    int n = grid[0].length;
    // 对路地点进行广度优先搜索
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            // 如果没有被访问过并且是陆地面积,则对其进行dfs搜索
            if (grid[i][j] == 1) {
                int curArea = dfs(i, j, grid, n);
                maxArea = Math.max(maxArea, curArea);
            }
        }
    }
    return maxArea;
}

/**
 * 岛屿深度搜索
 * @param row
 * @param col
 * @param grid
 * @param N
 * @return
 */
private int dfs(int row, int col, int[][] grid, int N) {
    if (row < 0 || row >= grid.length || col < 0 || col >= N || grid[row][col] == 0) return 0;

    // 当前节点已经访问,置为0
    grid[row][col] = 0;

    return dfs(row - 1, col, grid, N)
            + dfs(row + 1, col, grid, N)
            + dfs(row, col - 1, grid, N)
            + dfs(row, col + 1, grid, N)
            + 1;
}

T101-孤岛的总面积

见卡码网第101题[孤岛的总面积]

题目描述

给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被水域单元格包围。孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。 现在你需要计算所有孤岛的总面积,岛屿面积的计算方式为组成岛屿的陆地的总数。

输入描述

第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0。

输出描述

输出一个整数,表示所有孤岛的总面积,如果不存在孤岛,则输出 0。

示例

输入:
4 5 
1 1 0 0 0 
1 1 0 0 0 
0 0 1 0 0 
0 0 0 1 1
输出:
1

我的思路

本题的重点在于,对于靠边的岛屿,其面积不计入计算。因此,应该设置一个全局变量,标记当前的岛屿是否是接触大陆的。

使用深度优先遍历,得出岛屿面积。然后判断标记,如果毗邻大陆,则当前面积不计入。

private boolean isolated = true;

/**
 * 孤岛的总面积
 * @param grid
 * @return
 */
public int totalAreaOfIslands(int[][] grid) {
    if (grid.length == 1 || grid[0].length == 1) return 0;

    int totalArea = 0;
    int m = grid.length;
    int n = grid[0].length;

    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            int curArea = dfs(i, j, grid);
            if (isolated) {
                totalArea += curArea;
            } else {
                isolated = true;
            }
        }
    }

    return totalArea;
}

/**
 * 深度优先遍历
 * @param row
 * @param col
 * @param grid
 * @return
 */
private int dfs(int row, int col, int[][] grid) {
    if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length || grid[row][col] == 0) return 0;
    // 标记当前节点已经访问
    grid[row][col] = 0;
    // 确定是否是孤岛
    if (row == 0 || row == grid.length - 1 || col == 0 || col == grid[0].length) this.isolated = false;
    return dfs(row - 1, col, grid)
            + dfs(row + 1, col, grid)
            + dfs(row, col + 1, grid)
            + dfs(row, col - 1, grid)
            + 1;
}

T102-孤岛沉没

见卡码网第102题[孤岛沉没]

题目描述

给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被水域单元格包围。孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。 现在你需要将所有孤岛“沉没”,即将孤岛中的所有陆地单元格(1)转变为水域单元格(0)。

示例

输入:
1 1 0 0 0 
1 1 0 0 0 
0 0 1 0 0 
0 0 0 1 1
输出:
1 1 0 0 0 
1 1 0 0 0 
0 0[0]0 0 
0 0 0 1 1

我的思路

在遍历的时候,如何确定该点属于孤岛还是毗邻大陆的岛屿呢?

可以先将四周的土地(1)全部置为(2),然后从四周开始作深度优先遍历,将所有属于半岛的土地都置为(2),这样遍历完毕之后,再将所有的(1)置为(0),然后将所有的(2)置为(1)。

特别注意,递归终止条件为grid[row][col] != 1,防止改过 2 之后,递归无法正确终止致使栈溢出。

/**
 * 沉没孤岛
 * @param grid
 * @return
 */
public int[][] sunkIsland(int[][] grid) {
    if (grid.length == 1 || grid[0].length == 1) return grid;

    int m = grid.length;
    int n = grid[0].length;

    // 遍历四个边,将其置为 2
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if ((i == 0 || i == m - 1 || j == 0 || j == n - 1) && grid[i][j] == 1) {
                dfs(i, j, grid);
            }
        }
    }
    // 将所有的 2 都置为 1,将所有的 1 都置为 0
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == 2) grid[i][j] = 1;
            else if (grid[i][j] == 1) grid[i][j] = 0;
        }
    }
    return grid;
}

/**
 * 深度优先遍历
 * @param row
 * @param col
 * @param grid
 */
private void dfs(int row, int col, int[][] grid) {
    if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length || grid[row][col] != 1) return;

    // 将当前的值置为 2
    grid[row][col] = 2;

    // 寻找相邻的点
    dfs(row - 1, col, grid);
    dfs(row + 1, col, grid);
    dfs(row, col - 1, grid);
    dfs(row, col + 1, grid);
}

T103-水流问题

见卡码网第103题[水流问题]

题目描述

现有一个 N × M 的矩阵,每个单元格包含一个数值,这个数值代表该位置的相对高度。矩阵的左边界和上边界被认为是第一组边界,而矩阵的右边界和下边界被视为第二组边界。

矩阵模拟了一个地形,当雨水落在上面时,水会根据地形的倾斜向低处流动,但只能从较高或等高的地点流向较低或等高并且相邻(上下左右方向)的地点。我们的目标是确定那些单元格,从这些单元格出发的水可以达到第一组边界和第二组边界。

示例

输入
1, 3, 1, 2, 4
1, 2, 1, 3, 2
2, 4, 7, 2, 1
4, 5, 6, 1, 1
1, 4, 1, 2, 1
输出
[[0 4] [1 3] [2 2] [3 0] [3 1] [3 2] [4 0] [4 1]]

我的思路

根据题目的意思,就是是说,图中的任意一点,判断其是否能够达到左上边界和右下边界。

这题可以使用深度优先搜索,从四个边出发,给整个图进行涂色,最后左上和右下搜索的重合的格子,即为结果坐标。

具体的说,需要有一个标记数组int[][] isVisited,如果某个点遍历的时候,发现其已经被涂色,则可将其坐标添加至结果数组中。

private List<int[]> points = new ArrayList<>();
private int[][] isVisited;
/**
 * 判断能够流到边界的点的位置
 * @param grid
 * @return
 */
public List<int[]> availablePath(int[][] grid) {
    if (grid.length == 1 || grid[0].length == 1) {
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                points.add(new int[]{i, j});
            }
        }
        return points;
    }

    int m = grid.length;
    int n = grid[0].length;
    isVisited = new int[m][n];
    boolean isLU = true;

    // 先从左上开始dfs搜索可达点
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (i == 0 || j == 0 && isVisited[i][j] != 1) {
                dfs(i, j, grid, isLU);
            }
        }
    }

    // 再从右下开始dfs搜索可达点
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (i == m - 1 || j == n - 1 && isVisited[i][j] != 2) {
                dfs(i, j, grid, false);
            }
        }
    }

    return points;
}

/**
 *
 * @param row
 * @param col
 * @param grid
 * @param isLU 判断是否是左上来遍历
 */
private void dfs(int row, int col, int[][] grid, boolean isLU) {
    int curMark = isLU ? 1 : 2;
    // 不用判断边界条件,递归相邻节点的时候进行判断
    // 从右下开始 遍历,并且当前点已经被左上标记了
    if (!isLU && isVisited[row][col] == 1) {
        points.add(new int[]{row, col});
    }
    // 标记当前的点
    isVisited[row][col] = curMark;

    // 判断四周的点,不能越界,单调递增,没有被访问过
    if (row - 1 >= 0 && grid[row - 1][col] >= grid[row][col] && isVisited[row - 1][col] != curMark) {
        dfs(row - 1, col, grid, isLU);
    }
    if (col - 1 >= 0 && grid[row][col - 1] >= grid[row][col] && isVisited[row][col - 1] != curMark) {
        dfs(row, col - 1, grid, isLU);
    }

    if (col + 1 < grid[0].length && grid[row][col + 1] >= grid[row][col] && isVisited[row][col + 1] != curMark) {
        dfs(row, col + 1, grid, isLU);
    }
    if (row + 1 < grid.length && grid[row + 1][col] >= grid[row][col] && isVisited[row + 1][col] != curMark) {
        dfs(row + 1, col, grid, isLU);
    }
}

T104-建造最大人工岛屿

见卡码网第104题[建造最大人工岛屿]

题目描述

给定一个由 1(陆地)和 0(水)组成的矩阵,你最多可以将矩阵中的一格水变为一块陆地,在执行了此操作之后,矩阵中最大的岛屿面积是多少。

岛屿面积的计算方式为组成岛屿的陆地的总数。岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设矩阵外均被水包围。

示例

输入
1 1 0 0 0 
1 1 0 0 0 
0 0 1 0 0 
0 0 0 1 1
输出
6
解释
1 1 0 0 0 
1 1 0 0 0 
0[1]1 0 0 
0 0 0 1 1

我的思路

题目给出可以将一格水变为一格土地,我们可以遍历每一片水域,然后将其变为土地。然后深度搜索最大的岛屿面积。

这样,时间复杂度为O(N4)O(N^4),因为相当于从每个元素开始,对其进行深度搜索。这样会出现很多重复的搜索路径。比如,某些土地本来就在一个岛上,无论从哪里深度搜索,都将会遍历整个岛屿面积。

如何优化呢?

首先需要明确的是,为了降低计算复杂度,我们不能随便将一块土地变为岛屿。例如,假设一片水域四周都是水域,将其变为岛屿,没有任何意义哇。

要想让人工岛屿的面积最大,我们一定要将人工土地放在某个岛屿的周围。然后我们遍历这个人工土地的四周,看看他周围连着几片天然岛屿,最后将其周围的天然岛屿加起来,再加上人工岛屿的面积 1,就是结果了。当然,有一种可能就是,某个人工土地的左边,右边,上边可能是同一个岛屿,在判断毗邻岛屿的时候,一定要建立一个isVisited数组,来判断其是否已经被访问过了。

private List<Set<Integer>> islands = new ArrayList<>();
/**
 * 最大的人工岛屿
 * @param grid
 * @return
 */
public int maxArtificialIslandArea(int[][] grid) {

    int maxArea = 1;
    int m = grid.length;
    int n = grid[0].length;
    // 首先遍历图,找到每个岛屿的土地坐标集合
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == 1) {
                Set<Integer> set = new HashSet<>();
                dfs(i, j, grid, set);
                // 添加到岛屿列表中
                islands.add(set);
            }
        }
    }
    // 遍历水域,只有当当前水域紧挨着岛屿的时候,我们才考虑将其变为土地
    boolean[] isVisited = new boolean[islands.size()];
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == 0 && (i - 1 >= 0 && grid[i - 1][j] == 2)
                                || (i + 1 < m && grid[i + 1][j] == 2)
                                || (j - 1 >= 0 && grid[i][j - 1] == 2)
                                || (j + 1 < n && grid[i][j + 1] == 2)) {
                int curArea = 1;
                // 我们查看其四周那些地方连着哪些岛屿
                if (i - 1 >= 0 && grid[i - 1][j] == 2) {
                    for (int k = 0; k < islands.size(); k++) {
                        if (islands.get(k).contains((i - 1) * n + j)) {
                            curArea += islands.get(k).size();
                            isVisited[k] = true;
                            break;
                        }
                    }
                }
                if (i + 1 < m && grid[i + 1][j] == 2) {
                    for (int k = 0; k < islands.size(); k++) {
                        if (islands.get(k).contains((i + 1) * n + j) && !isVisited[k]) {
                            curArea += islands.get(k).size();
                            isVisited[k] = true;
                            break;
                        }
                    }
                }
                if (j - 1 >= 0 && grid[i][j - 1] == 2) {
                    for (int k = 0; k < islands.size(); k++) {
                        if (islands.get(k).contains(i * n + j - 1) && !isVisited[k]) {
                            curArea += islands.get(k).size();
                            isVisited[k] = true;
                            break;
                        }
                    }
                }
                if (j + 1 < n && grid[i][j + 1] == 2) {
                    for (int k = 0; k < islands.size(); k++) {
                        if (islands.get(k).contains(i * n + j + 1) && !isVisited[k]) {
                            curArea += islands.get(k).size();
                            isVisited[k] = true;
                            break;
                        }
                    }
                }
                maxArea = Math.max(curArea, maxArea);
                // 将标记清除
                Arrays.fill(isVisited, false);
            }
        }
    }
    return maxArea;
}

/**
 * 遍历图,将其归类为岛屿
 * @param row
 * @param col
 * @param grid
 * @param set
 */
private void dfs(int row, int col, int[][] grid, Set<Integer> set) {
    if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length || grid[row][col] != 1) {
        return;
    }

    // 将当前节点加入到set,并标记
    set.add(row * grid[0].length + col);
    grid[row][col] = 2;

    // 遍历四周的节点
    dfs(row - 1, col, grid, set);
    dfs(row + 1, col, grid, set);
    dfs(row, col - 1, grid, set);
    dfs(row, col + 1, grid, set);
}

T110-字符串接龙

见卡码网第110题[字符串接龙]

题目描述

字典 strList 中从字符串 beginStr 和 endStr 的转换序列是一个按下述规格形成的序列:

  1. 序列中第一个字符串是 beginStr。
  2. 序列中最后一个字符串是 endStr。
  3. 每次转换只能改变一个字符。
  4. 转换过程中的中间字符串必须是字典 strList 中的字符串。

给你两个字符串 beginStr 和 endStr 和一个字典 strList,找到从 beginStr 到 endStr 的最短转换序列中的字符串数目。如果不存在这样的转换序列,返回 0。

示例

输入:
str = {efc, dbc,ebc, dec, dfc, yhn}
beginStr = "abc",
endStr = "def"

输出:
4
解释:
abc -> dbc -> dec -> def

我的思路

最短路径,即为图的广度优先遍历。

我们应当如何去判断两个字符串是否是相邻的关系呢?对于当前字符串,判断和目标串是否是相邻的,只需要按位遍历,如果其有n - 1位都相等,则直接将其加入到当前字符串的邻居。

private boolean[] isVisited;
/**
 * 判断两个字符串的最短距离
 * @param dict
 * @param startStr
 * @param endStr
 * @return
 */
public int changeString(List<String> dict, String startStr, String endStr) {
    dict.add(startStr);
    dict.add(endStr);
    int n = dict.size();
    isVisited = new boolean[dict.size()];
    int step = 1;

    LinkedList<Integer> neighbors = new LinkedList<>();
    neighbors.addLast(n - 2);
    while (!neighbors.isEmpty()) {
        int size = neighbors.size();
        for (int i = 0; i < size; i++) {
            int index = neighbors.pollFirst();
            if (isVisited[index]) continue;
            String curStr = dict.get(index);
            if (endStr.equals(curStr)) return step;
            // 标记当前节点访问过
            isVisited[i] = true;
            // 遍历当前节点的子节点
            findNeighbor(curStr, dict, neighbors);
        }
        step++;
    }
    return step;
}

/**
 * 寻找当前字符串的相邻字符串
 * @param curStr
 * @param dict
 * @param neighbors
 */
private void findNeighbor(String curStr, List<String> dict, LinkedList<Integer> neighbors) {
    for (int i = 0; i < dict.size(); i++) {
        if (isVisited[i]) continue;
        String targetString = dict.get(i);
        int count = 0;
        for (int j = 0; j < targetString.length(); j++) {
            if (curStr.charAt(j) == targetString.charAt(j)) count++;
            if (count == curStr.length() - 1) {
                neighbors.addLast(i);
                break;
            }
        }
    }
}

T105-有向图的完全可达性

见卡码网第105题[有向图的完全可达性]

题目描述

给定一个有向图,包含 N 个节点,节点编号分别为 1,2,...,N。现从 1 号节点开始,如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。

示例

输入:
Map<List<Integer>> list = {{1, {2, 3}}, {2, {1, 4}}}
输出:
true

我的思路

判断可达性,要使用深度优先搜索。从节点 1 开始,将当前节点标记为已经访问过,然后遍历其所有的孩子节点。直到所有的孩子节点都被遍历完毕,最后看所有的子节点是否已经被访问过了。

boolean[] isVisited;
/**
 * 从 1 开始,遍历图的每一个节点
 * @param graph
 * @return
 */
public boolean traverseGraph(Map<Integer, List<Integer>> graph, int N) {
    isVisited = new boolean[N];

    // 首先从顶点 1 开始遍历
    int curVertex = 1;
    dfs(curVertex, graph);

    for (boolean flag : isVisited) {
        if (!flag) return false;
    }
    return true;
}

/**
 * 深度优先遍历图
 * @param curVertex
 * @param graph
 */
private void dfs(int curVertex, Map<Integer, List<Integer>> graph) {
    // 将当前节点标记为已经访问过
    isVisited[curVertex - 1] = true;

    // 遍历当前未访问过的子节点
    if (graph.get(curVertex) == null) return;
    for (int child : graph.get(curVertex)) {
        if (!isVisited[child - 1]) {
            dfs(child, graph);
        }
    }
}

T106-岛屿的周长

见卡码网第106题[岛屿的周长]

题目描述

给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。

你可以假设矩阵外均被水包围。在矩阵中恰好拥有一个岛屿,假设组成岛屿的陆地边长都为 1,请计算岛屿的周长。岛屿内部没有水域。

示例

输入:
0 0 0 0 0 
0 1 0 1 0 
0 1 1 1 0 
0 1 1 1 0 
0 0 0 0 0
输出:
14

我的思路

题目中给了一个比较好的条件就是,整片海域只有一个岛屿。此外,如果岛屿靠边,假设矩阵外的也是水域。

这样的话, 我们就可以深度优先遍历岛屿中的每一块土地,然后判断其邻水的边长有几块,则其周长就加上多少。

此外,对于遍历完之后的岛屿,我们可以将其置为2,以便和水域区分开来。

private int circumference = 0;
/**
 * 计算岛屿的周长
 * @param grid
 * @return
 */
public int calculateCircumference(int[][] grid) {

    int m = grid.length;
    int n = grid[0].length;
    int row = 0, col = 0;
    // 遍历寻找第一个路地点
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == 1) {
                row = i;
                col = j;
                break;
            }
        }
    }
    // 从当前点开始深度优先搜索
    dfs(row, col, grid);

    return circumference;
}

/**
 * 深度优先遍历岛屿的土地,并计算周长
 * @param row
 * @param col
 * @param grid
 */
private void dfs(int row, int col, int[][] grid) {
    if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length || grid[row][col] != 1) return;

    // 将当前的点置为已经访问过
    grid[row][col] = 2;
    // 计算当前节点周围水域的长度
    if (row - 1 < 0 || grid[row - 1][col] == 0) circumference++;
    if (row + 1 >= grid.length || grid[row + 1][col] == 0) circumference++;
    if (col - 1 < 0 || grid[row][col - 1] == 0) circumference++;
    if (col + 1 >= grid[0].length || grid[row][col + 1] == 0) circumference++;

    // 遍历四周的节点
    dfs(row - 1, col, grid);
    dfs(row + 1, col, grid);
    dfs(row, col - 1, grid);
    dfs(row, col + 1, grid);
}

优化思路

但是,实际上,这题根本就不需要DFS或者BFS遍历。我们要求岛屿的边长,只需要遍历每一片陆地,然后判断其周围是否为海域即可。

这样时间复杂度为O(N2)O(N^2),和DFS|BFS时间复杂度相同,但是确大大节省了空间。

T107-寻找存在的路径

见卡码网第107题[寻找存在的路径]

题目描述

给定一个包含 n 个节点的无向图中,节点编号从 1n (含 1n )。

你的任务是判断是否有一条从节点 source 出发到节点 destination 的路径存在。

示例

输入:
graph = {{"1", {"2", "3"}}, {"2", {"4"}}, {"3", {"4"}}};
source = "1", dest = "4";
输出:
true

我的思路

趁热打铁,可以使用并查集的思路,去判断sourcedest是否在同一个集合中。

先根据题中给出的条件构建并查集,然后使用isConnected方法判断两个点是否在一个集合中。

/**
 * 图graph中从 source 到 dest 是否存在有可达的路径
 * @param graph
 * @param source
 * @param dest
 * @param N 根节点数量
 * @return
 */
public boolean isAvailablePath(int[][] graph, int source, int dest, int N) {
    if (graph == null || graph.length == 0) return false;
    UnionFind unionFind = new UnionFind(N);

    // 构建并查集
    for (int[] edge : graph) {
        unionFind.union(edge[0], edge[1]);
    }

    return unionFind.isConnected(source, dest);

}

并查集的实现类:

class UnionFind {

    private int[] parents;

    public UnionFind (int N) {
        this.parents = new int[N + 1];
        for (int i = 0; i < parents.length; i++) {
            parents[i] = i; // 初始化父节点
        }
    }

    /**
     * 查找节点 x 的父节点
     * @param x
     * @return
     */
    public int find(int x) {
        if (parents[x] != x) {
            parents[x] = find(parents[x]);
        }
        return parents[x];
    }

    /**
     * 合并两个节点
     * @param x
     * @param y
     */
    public void union(int x, int y) {
        x = find(x);
        y = find(y);
        if (x != y) parents[x] = y;
    }

    /**
     * 判断两个是否连通
     * @param x
     * @param y
     * @return
     */
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }
}

T108-冗余连接

见卡码网第108题[冗余连接]

题目描述

有一个图,它是一棵树,他是拥有 n 个节点(节点编号1到n)和 n - 1 条边的连通无环无向图(其实就是一个线形图),如图:

image.png

现在在这棵树上的基础上,添加一条边(依然是n个节点,但有n条边),使这个图变成了有环图,如图:

image.png

先请你找出冗余边,删除后,使该图可以重新变成一棵树。

示例

输入:
5
1 4
2 3
2 4
2 5
3 5
输出:
3 5

我的思路

这一题同样可以使用并查集来解决。

我们首先构建一个并查集,遍历输入的每一条边,如果两个顶点不在同一个集合中,我们使用union将其并入。直到我们发现有两个点已经在同一个集合中了,我们就直接将这一条边摘出来,返回即可。

具体实现代码不再赘述。

T109-冗余链接II

见卡码网第109题[冗余链接II]

题目描述

有一种有向树,该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。有向树拥有 n 个节点和 n - 1 条边。

输入一个有向图,该图由一个有着 n 个节点(节点编号 从 1 到 n),n 条边,请返回一条可以删除的边,使得删除该条边之后该有向图可以被当作一颗有向树。

我的思路

相较于第108题,本题的图是一个有向图,我们是否同样可以使用并查集来解决此问题呢?

当然可以使用并查集来解决。需要注意的是,在union两个顶点的时候,要根据有向边的方向来合并,即若u -> v,则一定要确保合并之后vu的父节点。

此外,在有向图中,只有根节点的入度为0,其他节点的入度都为1。因此,如果所有的节点的入度都为1,则需要去判断图里头是否有环。

本题的步骤总结如下:

  1. 遍历每个边,记录每个顶点的入度
  2. 检查顶点的入度
  3. 如果所有的入度都为1,则说明没有根节点,有向图里面有环,这时候,需要遍历边,删除最后一个边冗余边即可
  4. 如果有一个边的入度为2,则需要检查这两条边,判断删除那一条边之后,可以成为一个有向图
/**
 * 寻找有向图中的冗余边
 * @param edges
 * @return
 */
public int[] findRedundantEdge(List<int[]> edges) {

    Map<Integer, List<int[]>> vertexes = new HashMap<>();
    int doubleIns = 0;
    for (int[] edge : edges) {
        if (vertexes.get(edge[1]) == null) {
            vertexes.put(edge[1], new ArrayList<>(Arrays.asList(edge)));
        } else {
            vertexes.get(edge[1]).add(edge);
            doubleIns = edge[1];
            break;
        }
    }
    // 判断是否有入度为 2 的顶点
    if (doubleIns != 0) {
        List<int[]> doubleEdges = vertexes.get(doubleIns);
        // 需要判断要删除那一条边
        if (isTreeRemovingEdge(edges, doubleEdges.get(1))) {
            return doubleEdges.get(1);
        } else {
            return doubleEdges.get(0);
        }
    }

    // 现在可能是有环,需要删除构成环的边
    return removeCircleEdge(edges);
}

/**
 * 判断删除边之后,是否是一个有向图
 * @param edges
 * @param target
 * @return
 */
private boolean isTreeRemovingEdge(List<int[]> edges, int[] target) {
    UnionFind unionFind = new UnionFind(edges.size());
    for (int[] edge : edges) {
        if (edge.equals(target)) continue;
        if (unionFind.isConnected(edge[0], edge[1])) {
            return false;
        }
    }
    return true;
}

/**
 * 删除有向图中构成环的边
 * @param edges
 * @return
 */
private int[] removeCircleEdge(List<int[]> edges) {
    UnionFind unionFind = new UnionFind(edges.size());
    for (int[] edge : edges) {
        if (unionFind.isConnected(edge[0], edge[1])) return edge;
        unionFind.union(edge[0], edge[1]);
    }
    return null;
}