图论-习题部分
T98-所有可达路径
见卡码网第98题[所有可达路径]
题目描述
给定一个有 n
个节点的有向无环图,节点编号从 1
到 n
。请编写一个函数,找出并返回所有从节点 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遍历的模板:
- 检查是否达到递归结束条件
- 对当前节点做处理
- 对子节点做处理
- 插销当前节点的处理
本题的答案也就呼之欲出了。
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++;
}
计算复杂度分析
- 时间复杂度:,最坏情况下,每一个节点都要访问一次
- 空间复杂度:,最坏情况下,每个点都是陆地,需要队列的最大长度为对角线的长度
优化方案
在队里里头存储二维数组,还有创建二维数组的开销比较大,如何将一个点的坐标存储为一个整数,可以将其行坐标 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
我的思路
题目给出可以将一格水变为一格土地,我们可以遍历每一片水域,然后将其变为土地。然后深度搜索最大的岛屿面积。
这样,时间复杂度为,因为相当于从每个元素开始,对其进行深度搜索。这样会出现很多重复的搜索路径。比如,某些土地本来就在一个岛上,无论从哪里深度搜索,都将会遍历整个岛屿面积。
如何优化呢?
首先需要明确的是,为了降低计算复杂度,我们不能随便将一块土地变为岛屿。例如,假设一片水域四周都是水域,将其变为岛屿,没有任何意义哇。
要想让人工岛屿的面积最大,我们一定要将人工土地放在某个岛屿的周围。然后我们遍历这个人工土地的四周,看看他周围连着几片天然岛屿,最后将其周围的天然岛屿加起来,再加上人工岛屿的面积 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 的转换序列是一个按下述规格形成的序列:
- 序列中第一个字符串是 beginStr。
- 序列中最后一个字符串是 endStr。
- 每次转换只能改变一个字符。
- 转换过程中的中间字符串必须是字典 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遍历。我们要求岛屿的边长,只需要遍历每一片陆地,然后判断其周围是否为海域即可。
这样时间复杂度为,和DFS|BFS时间复杂度相同,但是确大大节省了空间。
T107-寻找存在的路径
见卡码网第107题[寻找存在的路径]
题目描述
给定一个包含 n
个节点的无向图中,节点编号从 1
到 n
(含 1
和 n
)。
你的任务是判断是否有一条从节点 source
出发到节点 destination
的路径存在。
示例
输入:
graph = {{"1", {"2", "3"}}, {"2", {"4"}}, {"3", {"4"}}};
source = "1", dest = "4";
输出:
true
我的思路
趁热打铁,可以使用并查集的思路,去判断source
和dest
是否在同一个集合中。
先根据题中给出的条件构建并查集,然后使用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 条边的连通无环无向图(其实就是一个线形图),如图:
现在在这棵树上的基础上,添加一条边(依然是n个节点,但有n条边),使这个图变成了有环图,如图:
先请你找出冗余边,删除后,使该图可以重新变成一棵树。
示例
输入:
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
,则一定要确保合并之后v
是u
的父节点。
此外,在有向图中,只有根节点的入度为0,其他节点的入度都为1。因此,如果所有的节点的入度都为1
,则需要去判断图里头是否有环。
本题的步骤总结如下:
- 遍历每个边,记录每个顶点的入度
- 检查顶点的入度
- 如果所有的入度都为1,则说明没有根节点,有向图里面有环,这时候,需要遍历边,删除最后一个边冗余边即可
- 如果有一个边的入度为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;
}