代码随想录算法训练营Day57|图论part03

75 阅读13分钟

kama 101 孤岛的总面积

题目链接:kamacoder.com/problempage…

文档讲解:www.programmercarl.com/kamacoder/0…

题目

题目描述

给定一个由 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

思路

本题的整体思路是,只要遇到一个未标记的陆地节点,就上岛使用DFS/BFS搜索并标记整个岛,如果没有一个地块接触地图边缘,这个岛就是孤岛,我们就可以把她的面积加在孤岛的总面积里。

主函数中对地图块的遍历依然是顺序遍历,外层从上到下,内层从左到右。下面分别用DFS和BFS实现对岛屿的搜索。

考虑深搜三部曲:

  1. 递归函数的参数和返回值:图graph,当前节点x,y
    1. 全局变量:所有孤岛的总面积sum,当前岛的面积area,标识数组flag
    2. 返回值:boolean,表示此岛上是否有在地图边缘的节点
  2. 递归函数的结束条件:函数会在没有未标记的可达节点后自动结束
  3. 处理从目前搜索节点出发的路径
    1. 查看上侧节点,如果未标记且为陆地,则标记此地块,area++,对其递归
    2. 查看下侧节点,如果未标记且为陆地,则标记此地块,area++,对其递归
    3. 查看左侧节点,如果未标记且为陆地,则标记此地块,area++,对其递归
    4. 查看右侧节点,如果未标记且为陆地,则标记此地块,area++,对其递归
    5. 检查自己是否在边缘,在返回true,不在返回false

解法

DFS实现

import java.util.*;

public class Main {
	static int m;
	static int n;
	static boolean[][] flags;
	static int sum = 0;
	static int area = 0;
	
	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);		
		m = scanner.nextInt();		
		n = scanner.nextInt();		
		int[][] graph = new int[m][n];		
		flags = new boolean[m][n];		
		for (int i = 0; i < m; i++) {		
			for (int j = 0; j < n; j++) {			
				graph[i][j] = scanner.nextInt();			
			}		
		}
		
		for (int i = 0; i < m; i++) {		
			for (int j = 0; j < n; j++) {			
				if (graph[i][j] == 1 && !flags[i][j]) {				
					area = 1;					
					if (!dfs(graph, i, j)) {					
						sum += area;					
					}				
				}			
			}		
		}		
		System.out.println(sum);	
	}		  
	
	private static boolean dfs(int[][] graph, int x, int y) {	
		flags[x][y] = true;		
		boolean hasMargin = false;		
		if (x + 1 < m && graph[x + 1][y] == 1 && !flags[x + 1][y]) {		
			area++;			
			hasMargin = dfs(graph, x + 1, y) || hasMargin;		
		}		
		if (x - 1 >= 0 && graph[x - 1][y] == 1 && !flags[x - 1][y]) {		
			area++;			
			hasMargin = dfs(graph, x - 1, y) || hasMargin;		
		}		
		if (y + 1 < n && graph[x][y + 1] == 1 && !flags[x][y + 1]) {		
			area++;			
			hasMargin = dfs(graph, x, y + 1) || hasMargin;		
		}		
		if (y - 1 >= 0 && graph[x][y - 1] == 1 && !flags[x][y - 1]) {		
			area++;			
			hasMargin = dfs(graph, x, y - 1) || hasMargin;		
		}		
		if (x == 0 || x == m - 1 || y == 0 || y == n - 1) {		
			hasMargin = true;		
		}		
		return hasMargin;	
	}
}

BFS实现

import java.util.*;
  
public class Main {	
	static int m;	
	static int n;	
	static boolean[][] flags;	
	static int sum = 0;	
	static int area = 0;	
	
	public static void main(String[] args) {		
		Scanner scanner = new Scanner(System.in);		
		m = scanner.nextInt();		
		n = scanner.nextInt();		
		int[][] graph = new int[m][n];		
		flags = new boolean[m][n];
		
		for (int i = 0; i < m; i++) {		
			for (int j = 0; j < n; j++) {			
				graph[i][j] = scanner.nextInt();			
			}		
		}
		
		for (int i = 0; i < m; i++) {		
			for (int j = 0; j < n; j++) {			
				if (graph[i][j] == 1 && !flags[i][j]) {				
					area = 1;					
					if (!bfs(graph, i, j)) {					
						sum += area;					
					}				
				}			
			}		
		}		
		System.out.println(sum);	
	}		  
	
	private static boolean bfs(int[][] graph, int a, int b) {	
		flags[a][b] = true;		
		boolean hasMargin = false;		
		Queue<int[]> queue = new LinkedList<>();		
		queue.add(new int[]{a,b});
		
		while (!queue.isEmpty()) {		
			int[] node = queue.poll();			
			int x = node[0];			
			int y = node[1];
			
			if (x + 1 < m && graph[x + 1][y] == 1 && !flags[x + 1][y]) {			
				area++;				
				flags[x+1][y] = true;				
				queue.add(new int[]{x+1, y});			
			}			
			if (x - 1 >= 0 && graph[x - 1][y] == 1 && !flags[x - 1][y]) {			
				area++;				
				flags[x-1][y] = true;				
				queue.add(new int[]{x-1, y});			
			}			
			if (y + 1 < n && graph[x][y + 1] == 1 && !flags[x][y + 1]) {			
				area++;				
				flags[x][y+1] = true;				
				queue.add(new int[]{x, y+1});			
			}			
			if (y - 1 >= 0 && graph[x][y - 1] == 1 && !flags[x][y - 1]) {			
				area++;				
				flags[x][y-1] = true;				
				queue.add(new int[]{x, y-1});			
			}			
			if (x == 0 || x == m - 1 || y == 0 || y == n - 1) {			
				hasMargin = true;			
			}		
		}		
		return hasMargin;	
	}
}

kama 102 沉没孤岛

题目链接:kamacoder.com/problempage…

文档讲解:www.programmercarl.com/kamacoder/0…

题目

题目描述:

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

现在你需要将所有孤岛“沉没”,即将孤岛中的所有陆地单元格(1)转变为水域单元格(0)。

输入描述:

第一行包含两个整数 N, M,表示矩阵的行数和列数。

之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。

输出描述

输出将孤岛“沉没”之后的岛屿矩阵。

输入示例:

4 5
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

思路

为了标记整个岛,我们需要在DFS/BFS前识别出孤岛的地块,所以要从地图边缘遍历。如果遇到未标记的边缘陆地块,就说明遇到了非孤岛,此时可以上岛标记所有地块为2。这样一来也不需要额外的标记数组。

这样遍历完一圈之后,所有非孤岛为2,孤岛为1,海洋为0.那么我们可以顺序遍历整张地图,把2改为1,1改为0

解法

import java.util.*;

public class Main {
	static int m;	
	static int n;
	
	public static void main(String[] args) {	
		Scanner scanner = new Scanner(System.in);		
		m = scanner.nextInt();		
		n = scanner.nextInt();		
		int[][] graph = new int[m][n];
		
		for (int i = 0; i < m; i++) {		
			for (int j = 0; j < n; j++) {				
				graph[i][j] = scanner.nextInt();			
			}		
		}
		
		// left and right		
		for (int i = 0; i < m; i++) {		
			if (graph[i][0]==1) {			
				dfs(graph, i, 0);			
			}			
			if (graph[i][n-1]==1) {			
				dfs(graph, i, n-1);			
			}		
		}
		
		// up and down		
		for (int j = 0; j < n; j++) {		
			if (graph[0][j]==1) {			
				dfs(graph, 0, j);			
			}			
			if (graph[m-1][j]==1) {			
				dfs(graph, m-1, j);			
			}		
		}				  
		
		for (int i = 0; i < m; i++) {		
			for (int j = 0; j < n; j++) {			
				if (graph[i][j]==1) {				
					graph[i][j] = 0;				
				}				
				else if (graph[i][j]==2) {				
					graph[i][j] = 1;				
				}				
				System.out.print(graph[i][j]+" ");			
			}			
			System.out.println();		
		}				  	
	}	
	
	private static void dfs(int[][] graph, int x, int y) {	
		graph[x][y] = 2;
		
		if (x+1 < m && graph[x+1][y]==1) {		
			dfs(graph, x+1, y);		
		}		
		if (x-1 >= 0 && graph[x-1][y]==1) {		
			dfs(graph, x-1, y);		
		}		
		if (y+1 < n && graph[x][y+1]==1) {		
			dfs(graph, x, y+1);		
		}		
		if (y-1 >= 0 && graph[x][y-1]==1) {			
			dfs(graph, x, y-1);		
		}	
	}
}

kama 103 水流问题

题目链接:kamacoder.com/problempage…

文档讲解:www.programmercarl.com/kamacoder/0…

题目

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

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

输入描述: 第一行包含两个整数 N 和 M,分别表示矩阵的行数和列数。

后续 N 行,每行包含 M 个整数,表示矩阵中的每个单元格的高度。

输出描述: 输出共有多行,每行输出两个整数,用一个空格隔开,表示可达第一组边界和第二组边界的单元格的坐标,输出顺序任意。

输入示例:

5 5
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

数据范围:

1 <= M, N <= 50

思路

如果我们对每个节点检查他们是否能到达第一组边界和第二组边界,时间复杂度为O(m2n2)O(m^2*n^2) ,但这里面明显有重复计算,对两个地块之间的流向关系最坏可判断O(mn)O(m*n)次。

所以我们可以反过来计算,分别从第一组和第二组边界出发,去标记所有能流到当前地块的地块,再去递归遍历那些地块。最终能被从第一组出发的标记和第二组出发的标记一起标记的地块就是满足条件的。

涉及到递归,我们使用DFS去遍历地块:

  1. 考虑递归函数的参数和返回值
    1. 全局参数:标记数组first和second
    2. 参数:graph,当前节点x,y
  2. 考虑递归结束条件
    1. 没有可继续递归的地块则自动结束
  3. 处理从当前节点出发的所有路径
    1. 先标记当前地块
    2. 对于更高或等高的上/下/左/右地块递归(前提是索引有效)

所以我们会先遍历第一组边界,得到第一组边界可到达的地块;那么对第二组边界dfs的时候,可以直接输出有第一组标记的遍历到的地块,但仍然需标记地块,防止重复访问。

再考虑到,从某一个边界地块出发时,可能标记到其他边界地块,那么这个边界地块能走到的地块都遍历过了,可以直接跳过。

解法

import java.util.*;

public class Main {

	static int m;	
	static int n;	
	static boolean[][] flag;	
	static boolean[][] second;
	
	public static void main (String[] args) {		
		Scanner scanner = new Scanner(System.in);		
		m = scanner.nextInt();		
		n = scanner.nextInt();		
		int[][] graph = new int[m][n];		
		flag = new boolean[m][n];		
		second = new boolean[m][n];
		
		for (int i = 0; i < m; i++) {		
			for (int j = 0; j < n; j++) {			
				graph[i][j] = scanner.nextInt();			
			}		
		}
		
		// first group		
		for (int i = 0; i < m; i++) {		
			if (!flag[i][0]) {			
				dfs(graph, i, 0);			
			}			
		}		
		for (int i = 0; i < n; i++) {			
			if (!flag[0][i]) {			
				dfs(graph, 0, i);			
			}		
		}				  
		
		// second group		
		for (int i = 0; i < m; i++) {		
			if (!second[i][n-1]) {				
				dfsTwo(graph, i, n-1);				
			}		
		}		
		for (int i = 0; i < n; i++) {		
			if (!second[m-1][i]) {			
				dfsTwo(graph, m-1, i);			
			}		
		}			  
	}	
	  	
	private static void dfs (int[][] graph, int x, int y) {	
		flag[x][y] = true;		
		if (x+1 < m && !flag[x+1][y] && graph[x+1][y] >= graph[x][y]) {		
			dfs(graph, x+1, y);		
		}		
		if (x-1 >= 0 && !flag[x-1][y] && graph[x-1][y] >= graph[x][y]) {		
			dfs(graph, x-1, y);		
		}		
		if (y+1 < n && !flag[x][y+1] && graph[x][y+1] >= graph[x][y]) {		
			dfs(graph, x, y+1);		
		}		
		if (y-1 >= 0 && !flag[x][y-1] && graph[x][y-1] >= graph[x][y]) {
			
			dfs(graph, x, y-1);		
		}	
	}		  
	
	private static void dfsTwo (int[][] graph, int x, int y) {	
		if (flag[x][y]) {		
			System.out.println(x+" "+y);		
		}		
		second[x][y] = true;		
		if (x+1 < m && !second[x+1][y] && graph[x+1][y] >= graph[x][y]) {		
			dfsTwo(graph, x+1, y);		
		}		
		if (x-1 >= 0 && !second[x-1][y] && graph[x-1][y] >= graph[x][y]) {		
			dfsTwo(graph, x-1, y);		
		}		
		if (y+1 < n && !second[x][y+1] && graph[x][y+1] >= graph[x][y]) {		
			dfsTwo(graph, x, y+1);		
		}		
		if (y-1 >= 0 && !second[x][y-1] && graph[x][y-1] >= graph[x][y]) {			
			dfsTwo(graph, x, y-1);		
		}	
	}	
}

复杂度分析

  • 时间复杂度:由于存在标记数组,我们不会在对一组边界遍历时重复访问一个地块。所以时间复杂度为O(mn)O(m*n)
  • 空间复杂度:O(mn)O(m*n).我们使用了三个m*n的数组

kama 104 建造最大岛屿

题目链接:kamacoder.com/problempage…

文档讲解:www.programmercarl.com/kamacoder/0…

题目

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

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

输入描述: 第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。

输出描述: 输出一个整数,表示最大的岛屿面积。

输入示例:

4 5
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1

输出示例

6

思路

首先想到的暴力解法是,遍历每一个水节点,把它改成陆地,然后计算地图的最大岛屿面积。考虑时间复杂度,遍历水节点时间复杂度O(nm)O(n*m) ,计算一次最大岛屿面积时间复杂度O(mn)O(m*n).故暴力解法的总时间复杂度为O(m2n2)O(m^2*n^2)

考虑暴力法中的重复计算,只修改一个地块,没有受到影响的岛屿的面积是不变的。所以在修改之后,我们只需要从修改的地块出发,计算当前地块所在岛屿的面积area。

考虑一下题目要求,我们需要构造出最大的岛屿。从贪心的思想来说,我们应该尽量桥接岛屿,或者直接对最大的岛屿扩容1个地块。所以这个地块的选择,一定在某岛屿的边缘,否则无法满足桥接和扩容。因此我们可以获得可用于桥接or扩容的所有水节点。

综合以上两个思想,考虑以下情况。假设未修改前,最大岛屿面积是max。所以算法能接受的最差结果应该是1和max的最大值。(注意考虑没有水节点的情况)

那么算法的主体就是,首先DFS计算出最大岛屿面积,同时找到桥接点。再遍历每个可用于桥接的节点,使用BFS/DFS搜索整个岛,计算出桥接后的岛屿面积,如果大于先有结果,就暂留结果,继续遍历;否则不保留结果

DFS搜索岛:

  1. 参数和返回值:图graph,当前节点x,y
    1. 全局变量:flag标记数组,area当前岛屿面积
  2. 结束条件:没有合适节点会自动结束
  3. 处理当前节点出发的所有路径:
    1. 标记当前节点
    2. area+1
    3. 对于上/下/左/右的未标记陆地节点递归(索引有效)

解法

import java.util.*;

public class Main {
	static int m;	
	static int n;	
	static boolean[][] flag;	
	static int area;
	
	public static void main (String[] args) {	
		Scanner scanner = new Scanner(System.in);		
		m = scanner.nextInt();		
		n = scanner.nextInt();		
		flag = new boolean[m][n];		
		int[][] graph = new int[m][n];		
		for (int i = 0; i < m; i++) {		
			for (int j = 0; j < n; j++) {			
				graph[i][j] = scanner.nextInt();			
			}		
		}
		
		// 计算最大岛屿面积,标记桥接点		
		List<int[]> bridges = new ArrayList<>();		
		int max = 0;		
		for (int i = 0; i < m; i++) {			
			for (int j = 0; j < n; j++) {			
				if (graph[i][j]==1 && !flag[i][j]) {				
					area = 0;					
					dfs(graph, i, j);					
					max = Math.max(area, max);				
				}				
				else if (graph[i][j]==0) {				
					int neighbor = 0;					
					if (i+1 < m && graph[i+1][j]==1) {					
						neighbor++;					
					}					
					if (i-1 >= 0 && graph[i-1][j]==1) {					
						neighbor++;					
					}					
					if (j+1 < n && graph[i][j+1]==1) {					
						neighbor++;					
					}					
					if (j-1 >= 0 && graph[i][j-1]==1) {					
						neighbor++;					
					}					
					if (neighbor >= 1) {					
						bridges.add(new int[]{i, j});					
					}					
				}			
			}		
		}		
		int result = Math.max(max, 1);		
		for (int[] bridge : bridges) {		
			int x = bridge[0];			
			int y = bridge[1];			
			graph[x][y] = 1;			
			area = 0;			
			flag = new boolean[m][n];			
			dfs(graph, x, y);			
			result = Math.max(area, result);			
			graph[x][y] = 0;		
		}		
		System.out.println(result);	
	}	
	  
	
	private static void dfs (int[][] graph, int x, int y) {		
		flag[x][y] = true;		
		area++;		
		if (x+1 < m && !flag[x+1][y] && graph[x+1][y] == 1) {		
			dfs(graph, x+1, y);		
		}		
		if (x-1 >= 0 && !flag[x-1][y] && graph[x-1][y] == 1) {		
			dfs(graph, x-1, y);			
		}		
		if (y+1 < n && !flag[x][y+1] && graph[x][y+1] == 1) {		
			dfs(graph, x, y+1);		
		}		
		if (y-1 >= 0 && !flag[x][y-1] && graph[x][y-1] == 1) {		
			dfs(graph, x, y-1);		
		}	
	}
}

今日收获总结

很多图论的题目可以从最基本的暴力法开始,一点点寻找可优化的思路,然后应用得到更低时间复杂度的方法