【图论】(一)图论理论基础与岛屿问题

news2024/11/23 15:27:25

图论理论基础与岛屿问题

  • 图论理论基础
    • 深度搜索(dfs)
    • 广度搜索(bfs)
    • 岛屿问题概述
  • 岛屿数量
    • 岛屿数量-深搜版
    • 岛屿数量-广搜版
  • 岛屿的最大面积
  • 孤岛的总面积
  • 沉没孤岛
  • 建造最大人工岛
  • 水流问题
  • 岛屿的周长

图论理论基础

这里仅对图论相关核心概念做整理总结归纳,具体详细相关概念请参考代码随想录上的整理总结:

  • 图论理论基础
  • 深度优先搜索理论基础
  • 所有可达路径-dfs实战
  • 广度优先搜索理论基础

图的遍历方式基本是两大类:

  • 深度优先搜索(dfs)
  • 广度优先搜索(bfs)

dfs和bfs的区别:

  • dfs是沿一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯),二叉树的前中后序遍历、以及回溯算法就是dfs过程。
  • bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程,二叉树的层序遍历就是bfs的过程。

深度搜索(dfs)

  dfs搜索过程是沿着一个方向搜,不到黄河不回头,举个例子,如图是一个无向图,我们要搜索从节点1到节点6的所有路径,那么dfs搜索的第一条路径是这样的(假设第一次延默认方向,就找到了节点6):


此时我们找到了节点6,(遇到黄河了,是不是应该回头了),那么应该再去搜索其他方向了,如下图:


路径2撤销了,改变了方向,走路径3(红色线), 接着也找到终点6。 那么撤销路径2,改为路径3,在dfs中其实就是回溯的过程(这一点很重要)

又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图四中,路径4撤销(回溯),改为路径5:


  又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,如下图,路径6撤销(回溯的过程),改为路径7,路径8 和 路径7,路径9, 结果发现死路一条,都走到了自己走过的节点:


  那么节点2所连接路径和节点3所链接的路径 都走过了,撤销路径只能向上回退,去选择撤销当初节点4的选择,也就是撤销路径5,改为路径10 。 如下图:


  上图演示中,其实并没有把 所有的 从节点1 到节点6的dfs(深度优先搜索)的过程都画出来,那样太冗余了,但 已经把dfs 关键的地方都涉及到了,关键就两点:

  • 搜索方向,是认准一个方向搜,直到碰壁之后再换方向
  • 换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程。

代码框架:

vector<vector<int>> result; // 保存符合条件的所有路径
vector<int> path; 			// 起点到终点的路径
void dfs(图,目前搜索的节点)  
{
    if (终止条件) {
        存放结果;
        return;
    }

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

其实很多dfs写法,没有写终止条件,其实终止条件写在了, 下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归

  到这里,对回溯算法会有更深刻的理解,路径2 已经走到了 目的地节点6,那么 路径2 是如何撤销,然后改为 路径3呢? 其实这就是 回溯的过程,撤销路径2,走换下一个方向

广度搜索(bfs)

  • 广搜的搜索方式就适合于解决两个点之间的最短路径问题。
  • 因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。
  • 当然,也有一些问题是广搜 和 深搜都可以解决的,例如岛屿问题,这类问题的特征就是不涉及具体的遍历方式,只要能把相邻且相同属性的节点标记上就行

广搜的过程:
  BFS是一圈一圈的搜索过程,我们用一个方格地图,假如每次搜索的方向为 上下左右(不包含斜上方),那么给出一个start起始位置,那么BFS就是从四个方向走出第一步


如果加上一个end终止位置,那么使用BFS的搜索过程如图所示:


正是因为BFS一圈一圈的遍历方式,所以一旦遇到终止点,那么一定是一条最短路径。

而且地图还可以有障碍,如图所示:


代码框架:

  • 其实,我们仅仅需要一个容器,能保存我们要遍历过的元素就可以,那么用队列,还是用栈,甚至用数组,都是可以的。
  • 用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针。
  • 因为队列是先进先出,加入元素和弹出元素的顺序是没有改变的。
  • 如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历。
  • 由于各种教程习惯用队列较多,这里我也使用队列来实现,只是记录并不是非要用队列,用栈也可以。

广搜代码模板,该模板针对的就是,上面的四方格的地图:

int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向
// grid 是地图,也就是一个二维数组
// visited标记访问过的节点,不要重复访问
// x,y 表示开始搜索节点的下标
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) 
{
    queue<pair<int, int>> que; 	// 定义队列
    que.push({x, y}); 			// 起始节点加入队列
    visited[x][y] = true; 		// 只要加入队列,立刻标记为访问过的节点
    
    // 开始遍历队列里的元素
    while(!que.empty()) { 
        pair<int ,int> cur = que.front(); que.pop(); // 从队列取元素
        int curx = cur.first;
        int cury = cur.second; 			// 当前节点坐标
        // 开始想当前节点的四个方向左右上下去遍历
        for (int i = 0; i < 4; i++) 
        { 
        	// 获取周边四个方向的坐标
            int nextx = curx + dir[i][0];
            int nexty = cury + dir[i][1]; 
            // 坐标越界了,直接跳过
            if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) 
            	continue;              // 如果节点没被访问过
            if (!visited[nextx][nexty]) 
            { 
                que.push({nextx, nexty});  		// 队列添加该节点为下一轮要遍历的节点
                visited[nextx][nexty] = true; 	// 只要加入队列立刻标记,避免重复访问
            }
        }
    }
}

务必理解上述代码,在下述的岛屿问题,解决相邻的问题上也将用到相似的思路与代码。

岛屿问题概述

在图论中,有一类基础问题即是岛屿问题,其中有关岛屿问题的通识描述如下:

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

岛屿数量

卡码网题目链接(ACM模式)

岛屿数量-深搜版

  • 遇到一个没有遍历过的节点陆地,计数器就加一,然后把该节点陆地所能遍历到的陆地都标记上。
  • 在遇到标记过的陆地节点和海洋节点的时候直接跳过, 这样计数器就是最终岛屿的数量。

程序实现:版本一

#include <iostream>
#include <vector>

using namespace std;

//四个方向
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
void dfs(const vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y)
{
	// 开始想当前节点的四个方向左右上下去遍历
	for(int i = 0; i < 4; i++)
	{
		// 四周的下个节点
		int nextx = x + dir[i][0];
		int nexty = y + dir[i][1];
		 // 坐标越界了,直接跳过
		if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size())
			continue;
		// 没有访问过的 同时 是陆地的
		if(!visited[nextx][nexty] && grid[nextx][nexty] == 1)
		{
			// 标记访问过
			visited[nextx][nexty] = true;
			// 深度搜索该陆地四周相邻的陆地
			dfs(grid, visited, nextx, nexty);
		}
	}
}

int main()
{
	int n,m;
	cin >> n >> m;
	//输入陆地海洋数据
	vector<vector<int>> grid(n,vector<int>(m,0));
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m;j++)
			cin >> grid[i][j];
	}
	
	// 标记该节点是否被访问过
	vector<vector<bool>> visited(n,vector<bool>(m,false));
	int result = 0;						// 记录岛屿数量
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++)
		{
			if(!visited[i][j] && grid[i][j] == 1)
			{
				visited[i][j] = true;	// 标记该节点已被访问
				result++; 				// 遇到没访问过的陆地,+1
				dfs(grid,visited,i,j);	// 将与其连接的陆地都标记上 true
			}
		}
	}
	cout << result << endl;
	return 0;
}
  • 为什么 以上代码中的dfs函数,没有终止条件呢?
  • 其实终止条件 就写在了 调用dfs的地方,如果遇到不合法的方向,直接不会去调用dfs。

当然也可以这么写:版本二

// 版本二
#include <iostream>
#include <vector>
using namespace std;
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向
void dfs(const vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y)
{
	// 终止条件:访问过的节点 或者 遇到海水
    if (visited[x][y] || grid[x][y] == 0) 
    	return; 
    visited[x][y] = true; // 标记访问过
    for (int i = 0; i < 4; i++) 
    {
        int nextx = x + dir[i][0];
        int nexty = y + dir[i][1];
        if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) 
        	continue;  // 越界了,直接跳过
        //不做判断 直接递归
        dfs(grid, visited, nextx, nexty);
    }
}

int main()
{
    int n, m;
    cin >> n >> m;
    vector<vector<int>> grid(n, vector<int>(m, 0));
    for (int i = 0; i < n; i++){
        for (int j = 0; j < m; j++){
            cin >> grid[i][j];
        }
    }

    vector<vector<bool>> visited(n, vector<bool>(m, false));

    int result = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (!visited[i][j] && grid[i][j] == 1) 
            {
            	// 遇到没访问过的陆地,+1
                result++; 		
                // 不先标记 直接递归
                dfs(grid, visited, i, j);	// 将与其链接的陆地都标记上 true
            }
        }
    }
    cout << result << endl;
}
  • 版本一的写法是 : 下一个节点是否能合法已经判断完了,传进dfs函数的就是合法节点。
  • 版本二的写法是: 不管节点是否合法,上来就dfs,然后在终止条件的地方进行判断,不合法再 return。
  • 理论上来讲,版本一的效率更高一些,因为避免了 没有意义的递归调用,在调用dfs之前,就做合法性判断。 就个人而言,版本一比版本二逻辑更清晰,更易于个人理解,因此后续都使用版本一的写法。
  • 其实本题是 dfs,bfs 模板题,所以需要注重更多的细节问题,更有利于后面对程序的修改与扩展。

岛屿数量-广搜版

  • 当然本题也是 bfs的一个模板题,也可以使用bfs来解决,即先搜索一个节点周围与其相邻的所有陆地

  • 这里有一个广搜中的重要细节:只要加入队列就代表走过,就需要标记,而不是从队列拿出来的时候再去标记走过。

  • 如果从队列拿出节点,再去标记这个节点走过,就会发生下图所示的结果,会导致很多节点重复加入队列。


超时写法 (从队列中取出节点再标记,注意代码注释的地方

int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
    queue<pair<int, int>> que;
    que.push({x, y});
    while(!que.empty()) {
        pair<int ,int> cur = que.front(); que.pop();
        int curx = cur.first;
        int cury = cur.second;
        visited[curx][cury] = true; // 从队列中取出在标记走过
        
        for (int i = 0; i < 4; i++) {
            int nextx = curx + dir[i][0];
            int nexty = cury + dir[i][1];
            if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) 
            	continue; 
            if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') {
                que.push({nextx, nexty});
            }
        }
    }
}

加入队列 就代表走过,立刻标记,正确写法:注意代码注释的地方

int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
    queue<pair<int, int>> que;
    que.push({x, y});
    // 只要加入队列,立刻标记
    visited[x][y] = true; 
    while(!que.empty()) {
        pair<int ,int> cur = que.front(); que.pop();
        int curx = cur.first;
        int cury = cur.second;
        for (int i = 0; i < 4; i++) {
            int nextx = curx + dir[i][0];
            int nexty = cury + dir[i][1];
            if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) 
            	continue; 
            if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') {
                que.push({nextx, nexty});
                // 只要加入队列立刻标记
                visited[nextx][nexty] = true; 
            }
        }
    }
}

以上两个版本其实,其实只有细微区别,就是 visited[x][y] = true; 放在的地方,这取决于我们对 代码中队列的定义,队列中的节点就表示已经走过的节点。 所以只要加入队列,立即标记该节点走过。

完整广搜代码实现:

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

int dir[4][2] = {1,0,0,1,-1,0,0,-1};
void bfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y)
{
	queue<pair<int, int>> que;
	que.push({x,y});
	// 只要加入队列,立刻标记
	visited[x][y] = true;
	while(!que.empty())
	{
		pair<int, int> cur = que.front();
		que.pop();
		int curx = cur.first;
		int cury = cur.second;
		for(int i = 0; i < 4;i++)
		{
			int nextx = curx + dir[i][0];
			int nexty = cury + dir[i][1];
			if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size())
				continue;
			if(!visited[nextx][nexty] && grid[nextx][nexty] == 1)
			{
				que.push({nextx,nexty});
				visited[nextx][nexty] = true;
			}
		}
	}
}

int main()
{
	int n,m;
	cin >> n >> m;
	vector<vector<int>>  grid(n,vector<int>(m));
	vector<vector<bool>> visited(n,vector<bool>(m,false));
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++)
		{
			cin >> grid[i][j];
		}
	}
	
	int result = 0;
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++)
		{

			if(!visited[i][j] && grid[i][j] == 1)
			{
				// 遇到没访问过的陆地,+1
				result++;
				// 将与其链接的陆地都标记上 true
				bfs(grid,visited,i,j);
			}
		}
	}
	
	cout << result << endl;
	return 0;
}

岛屿的最大面积

卡码网题目链接(ACM模式)

  • 本题也是 dfs bfs基础类题目,就是搜索每个岛屿上 1 的数量,然后取一个最大的。
  • 根据上述两种版本的写法,这里 dfs只处理下一个节点,即在主函数遇到岛屿就计数为1,dfs处理接下来的相邻陆地

程序实现(dfs)

#include <iostream>
#include <vector>

using namespace std;

int area;		//当前面积
int dir[4][2] = {1,0,0,1,-1,0,0,-1};
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y)
{
	//遍历四周的岛屿
	for(int i = 0; i < 4; i++)
	{
		int nextx = x + dir[i][0];
		int nexty = y + dir[i][1];
		if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size())
			continue;
		//发现没有遍历过的陆地
		if(!visited[nextx][nexty] && grid[nextx][nexty] == 1)
		{
			visited[nextx][nexty] = true;
			area++;			//面积 + 1
			//深度搜索下一个的相连的岛屿
			dfs(grid,visited,nextx,nexty);
		}
	}
}

int main()
{
	int n,m;
	int result = 0;
	cin >> n >> m;
	vector<vector<int>> grid(n,vector<int>(m));
	vector<vector<bool>> visited(n,vector<bool>(m,false));
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++){
			cin >> grid[i][j];
		}
	}
	
	//遍历岛屿
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++){
			//没有访问过的陆地
			if(!visited[i][j] && grid[i][j] == 1)
			{
				visited[i][j] = true;
				area = 1;
				dfs(grid,visited,i,j);		//标记相连的陆地为true
				result = max(result,area);	//记录最大的陆地面积
			}
		}
	}
	cout << result << endl;
}

孤岛的总面积

卡码网:101. 孤岛的总面积

  本题要求找到不靠边的陆地面积,那么我们只要从周边找到陆地然后 通过 dfs或者bfs 将周边靠陆地且相邻的陆地都变成海洋,然后再去重新遍历地图 统计此时还剩下的陆地就可以了。

如图,在遍历地图周围四个边,靠地图四边的陆地,都为绿色


在遇到地图周边陆地的时候,将1都变为0,此时地图为这样:


然后我们再去遍历这个地图,遇到有陆地的地方,去采用深搜或者广搜,边统计所有陆地。

程序实现(dfs):

#include <iostream>
#include <vector>

using namespace std;

// 本题要求找到不靠边的陆地面积,那么我们只要从周边找到陆地然后 通过 dfs或者bfs 
//将周边靠陆地且相邻的陆地都变成海洋,然后再去重新遍历地图 统计此时还剩下的陆地就可以了。

// 将相连的岛屿全部变成海洋
int dir[4][2] = {1,0,0,1,-1,0,0,-1};
int cnt = 0;
void dfs(vector<vector<int>>& grid, int x, int y)
{
	//标记变成海洋
    grid[x][y] = 0;
    //用于第二次求孤岛的面积使用 面积++
	cnt++;
	// 遍历四周是否有陆地
	for(int i = 0; i < 4; i++)
	{
		int nextx = x + dir[i][0];
		int nexty = y + dir[i][1];
		//越界
        if (nextx < 0 || nextx >= grid.size() || 
            nexty < 0 || nexty >= grid[0].size()) 
            continue;
        // 四周有陆地 深搜 将陆地标为海洋
		if(grid[nextx][nexty] == 1)
		    dfs(grid,nextx,nexty);
	}
	return;
}

int main()
{
	int n;
	int m;
	cin >> n >> m;
	// 输入图
	vector<vector<int>> grid(n,vector<int>(m));
	for(int i = 0; i < n; i++){
		for(int j = 0; j < m; j++){
			cin >> grid[i][j];
		}
	}
	
	//遍历左右两边的岛屿 使相连的陆地全部变成海洋
	for(int i = 0; i < n;i++)
	{
		//左侧
		if(grid[i][0] == 1)
			dfs(grid, i, 0);
		//右侧
		if(grid[i][m-1] == 1)
			dfs(grid, i, m-1);
	}
	//遍历上下两边的岛屿 使相连的陆地全部变成海洋
	for(int j = 0; j < m; j++)
	{
		// 上边界
		if(grid[0][j] == 1)
			dfs(grid, 0, j);
		if(grid[n-1][j] == 1)
			dfs(grid, n-1, j);
	}
	
	cnt = 0;
	//遍历岛屿 剩下的陆地全是孤岛了
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++)
		{
			if(grid[i][j] == 1)
				dfs(grid,i,j);
		}
	}
	
	cout << cnt << endl;
}

沉没孤岛

卡码网题目链接(ACM模式)

  • 本题和上述孤岛的总面积正好反过来了,上述是求孤岛的面积,而这题是将孤岛的1改为0,那么两题在思路上也是差不多的
  • 思路依然是从地图周边出发,将周边空格相邻的陆地都做上标记(改为2),然后再遍历一遍地图,遇到 陆地 且没做过标记的,那么都是地图中间的 陆地 ,全部改成水域就行。

步骤一: 深搜或者广搜将地图周边的 1 (陆地)全部改成 2 (特殊标记)

步骤二: 将水域中间 1 (陆地)全部改成 水域(0)

步骤三: 将之前标记的 2 改为 1 (陆地)


程序实现(dfs):

#include <iostream>
#include <vector>

using namespace std;

int dir[4][2] = {1,0,0,1,-1,0,0,-1};
//将靠近地图周边的岛屿全部变成 2
void dfs(vector<vector<int>>& grid, int x, int y)
{
	grid[x][y] = 2;
	for(int i = 0; i < 4; i++)
	{
		int nextx = x + dir[i][0];
		int nexty = y + dir[i][1];
		if(	nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size())
			continue;
		if(grid[nextx][nexty] == 1)
			dfs(grid,nextx,nexty);
	}
}

int main()
{
	int n, m;
	cin >> n >> m;
	vector<vector<int>> grid(n,vector<int>(m));
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++){
			cin >> grid[i][j];
		}
	}
	
	//步骤一
	// 左右两列
	for(int i = 0; i < n; i++)
	{
		if(grid[i][0] == 1)
			dfs(grid, i, 0);
		if(grid[i][m-1] == 1)
			dfs(grid, i, m-1);
	}
	//上下两行
	for(int j = 0; j < m; j++)
	{
		if(grid[0][j] == 1)
			dfs(grid, 0, j);
		if(grid[n-1][j] == 1)
			dfs(grid, n-1, j);
	}	

//	cout << "temp: " << endl;
//	for(int i = 0; i < n;i++)
//	{
//		for(int j = 0; j < m; j++)
//		{
//			cout << grid[i][j] << " ";
//		}		
//		cout << endl;
//	}
	
	//步骤二 三 周围陆地变成1 孤岛陆地为0
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++)
		{
		    //两个顺序不能换 否则全为0
			if(grid[i][j] == 1)	grid[i][j] = 0;
			if(grid[i][j] == 2)	grid[i][j] = 1;
		}		
	}
	
	//输出结果
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++)
		{
			cout << grid[i][j] << " ";
		}		
		cout << endl;
	}
}

建造最大人工岛

卡码网题目链接(ACM模式)

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

案例, 输入陆地与海洋信息如下:

对于上面的案例,有两个位置可将 0 变成 1,使得岛屿的面积最大,即 6。

思路

  • 本题的一个暴力想法,应该是遍历地图尝试 将每一个 0 改成1,然后去搜索地图中的最大的岛屿面积。
  • 计算地图的最大面积:遍历地图 + 深搜岛屿,时间复杂度为 n ∗ n n * n nn
  • 每改变一个0的方格,都需要重新计算一个地图的最大面积,所以 整体时间复杂度为: n 4 n^4 n4

优化思路

  • 其实每次深搜遍历计算最大岛屿面积,我们都做了很多重复的工作,只要用一次深搜把每个岛屿的面积记录下来就好。

第一步: 一次遍历地图,得出各个岛屿的面积,并做编号记录。可以使用map记录,key为岛屿编号,value为岛屿面积

第二步: 再遍历地图,遍历0的方格(因为要将0变成1),并统计该1(由0变成的1)周边岛屿面积,将其相邻面积相加在一起,遍历所有 0 之后,就可以得出 选一个0变成1 之后的最大面积。

拿如下地图的岛屿情况来举例: (1为陆地)


第一步: 遍历陆地,并将岛屿到编号和面积上的统计,过程如图所示:


统计每一块岛屿的面积,并将岛屿面积存入对应的编号map中保存,核心代码如下:

int area = 0;	// 统计当前遍历岛屿的面积
int dir[4][2] = {1,0,0,1,-1,0,0,-1};
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y, int landIdx)
{
	// 给陆地标记新标签
	grid[x][y] = landIdx;
	//四个方向都会递归
	for(int i = 0; i < 4; i++)
	{
		int nextx = x + dir[i][0];
		int nexty = y + dir[i][1];
		//越界
		if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size())
			continue;
		//统计相连陆地的面积
		if(!visited[nextx][nexty] && grid[nextx][nexty] == 1)
		{
			area++;
			visited[nextx][nexty] = true;
			dfs(grid, visited, nextx, nexty, landIdx);
		}
	}
}

unordered_map<int, int> landArea;
int landIdx = 2;		//标记岛屿的编号
for(int i = 0;i < n; i++)
{
    for(int j = 0; j < m; j++)
	{
		//遍历到新的岛屿 计算每个新陆地形成的岛屿面积
		if(!visited[i][j] && grid[i][j] == 1)
		{
			area = 1;	// 重新计算一块新的岛屿面积
			visited[i][j] = true;
			// 将与其连接的陆地都标记上 true 同时给每个岛屿编个号 landIdx
			dfs(grid, visited, i, j, landIdx);	
			landArea[landIdx++] = area;	// 标记每一块编号为 landIdx 的面积为 area
		}
	}
}

第二步: 遇到海洋,将0变成1后,看能形成的最大岛屿的面积(遍历海洋周围一圈是否有岛屿),有则拼接岛屿


这里要注意几个细节:

  • 特殊情况: n * m 的网格全部为陆地
  • 在海洋周围找到陆地后,拼接岛屿完成后需要对该岛屿进行标记,否则可能重复拼接周围的同一块岛屿

拼接岛屿核心代码如下:

int result = 0;					 // 记录最后结果
unordered_set<int> visitedGrid;  // 标记访问过的岛屿
for(int i = 0; i < n; i++)
{
	for(int j = 0; j < m; j++)
	{
		area = 1;				// 记录连接之后的岛屿面积
		visitedGrid.clear(); 	// 每次使用时,清空
		//访问到海洋了 
		//计算这个海洋节点变成陆地后拼接周围四个方向岛屿的面积
		if(grid[i][j] == 0)
		{
			for(int k = 0; k < 4; k++)
			{
				// 计算周围的相邻坐标
				int nexti = i + dir[k][0];
				int nextj = j + dir[k][1];
				//越界
				if(nexti < 0 || nexti >= grid.size() || nextj < 0 || nextj >= grid[0].size())
					continue;	
				// 添加过的岛屿不要重复添加
				if(visitedGrid.count(grid[nexti][nextj])) 
					continue; 
				//周围遇到新的陆地
				if(grid[nexti][nextj] > 0)
				{
					area += landArea[grid[nexti][nextj]];	// 拼接面积
					visitedGrid.insert(grid[nexti][nextj]); // 标记该岛屿已经添加过
				}	
			}
			//cout << area << endl;
			result = max(result,area);
		}
	}
}

完整代码实现如下:

#include <iostream>
#include <vector>
#include <unordered_map>
#include <unordered_set>

using namespace std;

int area = 0;	// 统计当前遍历岛屿的面积
int dir[4][2] = {1,0,0,1,-1,0,0,-1};
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y, int landIdx)
{
	// 给陆地标记新标签
	grid[x][y] = landIdx;
	//四个方向都会递归
	for(int i = 0; i < 4; i++)
	{
		int nextx = x + dir[i][0];
		int nexty = y + dir[i][1];
		//越界
		if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size())
			continue;
		//统计相连陆地的面积
		if(!visited[nextx][nexty] && grid[nextx][nexty] == 1)
		{
			area++;
			visited[nextx][nexty] = true;
			dfs(grid, visited, nextx, nexty, landIdx);
		}
	}
}

int main()
{
	int n, m;
	cin >> n >> m;
	int isAllLand = true;		//记录是否全部为陆地
	vector<vector<int>> grid(n, vector<int>(m));
	vector<vector<bool>> visited(n, vector<bool>(m, false));
	for(int i = 0;i < n; i++){
		for(int j = 0; j < m; j++){
			cin >>grid[i][j];
		}
	}
	unordered_map<int, int> landArea;
	int landIdx = 2;				//标记岛屿的编号
	for(int i = 0;i < n; i++){
		for(int j = 0; j < m; j++){
			if(grid[i][j] == 0)
				isAllLand = false;		//有海洋
			//遍历到新的岛屿 计算每个新陆地形成的岛屿面积
			if(!visited[i][j] && grid[i][j] == 1)
			{
				area = 1;
				visited[i][j] = true;
				// 将与其连接的陆地都标记上 true 同时给每个岛屿编个号 landIdx
				dfs(grid, visited, i, j, landIdx);	
				landArea[landIdx++] = area;	// 标记每一块编号为 landIdx 的面积为 area
			}
		}
	}
	
	// 全是陆地 不用建造了
	if(isAllLand == true)
	{
		cout << n * m << endl;
		return 0;
	}
	
//	cout << "area: " << endl;
//	for(int i = 2; i < landIdx; i++)
//	{
//		cout << landArea[i] << endl;
//	}
//	
//	cout << "landIdx: " << endl;
//	for(int i = 0; i < n; i++){
//		for(int j = 0; j < m; j++){
//			cout << grid[i][j] << " ";
//		}
//		cout << endl;
//	}
//	
	int result = 0;					 // 记录最后结果
	unordered_set<int> visitedGrid;  // 标记访问过的岛屿
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++)
		{
			area = 1;				// 记录连接之后的岛屿面积
			visitedGrid.clear(); 	// 每次使用时,清空
			//访问到海洋了 
			//计算这个海洋节点变成陆地后拼接周围四个方向岛屿的面积
			if(grid[i][j] == 0)
			{
				for(int k = 0; k < 4; k++)
				{
					// 计算周围的相邻坐标
					int nexti = i + dir[k][0];
					int nextj = j + dir[k][1];
					//越界
					if(nexti < 0 || nexti >= grid.size() || nextj < 0 || nextj >= grid[0].size())
						continue;	
					// 添加过的岛屿不要重复添加
					if(visitedGrid.count(grid[nexti][nextj])) 
						continue; 
					//周围遇到新的陆地
					if(grid[nexti][nextj] > 0)
					{
						area += landArea[grid[nexti][nextj]];	// 拼接面积
						visitedGrid.insert(grid[nexti][nextj]); // 标记该岛屿已经添加过
					}	
				}
				//cout << area << endl;
				result = max(result,area);
			}
		}
	}
	cout << result << endl;
}

水流问题

卡码网题目链接(ACM模式)

题目描述:

  现有一个 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

提示信息:

  • 图中的蓝色方块上的雨水既能流向第一组边界,也能流向第二组边界。所以最终答案为所有蓝色方块的坐标。

思路

  • 一个比较直白的想法,其实就是 遍历每个点,然后看这个点 能不能同时到达第一组边界和第二组边界。
  • 遍历每一个节点,时间复杂度是 m ∗ n m * n mn,遍历每一个节点的时候,都要做深搜,深搜的时间复杂度是: m ∗ n m * n mn,那么整体时间复杂度 就是 O ( m 2 ∗ n 2 ) O(m^2 * n^2) O(m2n2),这是一个四次方的时间复杂度,显然时间复杂度超时。
  • 那么我们可以 反过来想,从第一组边界上的节点 逆流而上,将遍历过的节点都标记上。
  • 同样从第二组边界的边上节点 逆流而上,将遍历过的节点也标记上。
  • 然后两方都标记过的节点就是既可以流太平洋也可以流大西洋的节点。

从第一组边界边上节点出发,如图:


从第二组边界上节点出发,如图:

#include <iostream>
#include <vector>

using namespace std;

//从第一组边界上的节点 逆流而上,将遍历过的节点都标记上。
//同样从第二组边界的边上节点 逆流而上,将遍历过的节点也标记上。
//然后两方都标记过的节点就是既可以流太平洋也可以流大西洋的节点。

int n, m;
int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1};
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y)
{
	//统一递归返回 下面不做判断是否访问过 处理当前节点
	if(visited[x][y]) 
		return;
	// 标记可以逆流
	visited[x][y] = true;
	for(int i = 0; i < 4; i++)
	{
		// 计算周围节点
		int nextx = x + dir[i][0];
		int nexty = y + dir[i][1];
		//越界
		if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size())
			continue;
		/// 注意:这里是逆向流水
		if(grid[x][y] <= grid[nextx][nexty])
			dfs(grid, visited, nextx, nexty);
	}
	return ;
}

int main()
{
	cin >> n >> m;
	vector<vector<int>> grid(n,vector<int>(m,0));
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++){
			cin >> grid[i][j];
		}
	}
	// 标记从第一组边界上的节点出发,可以遍历的节点
	vector<vector<bool>> firstBorder(n, vector<bool>(m, false));
	// 标记从第一组边界上的节点出发,可以遍历的节点
	vector<vector<bool>> secondBorder(n, vector<bool>(m, false));
	
	//左侧和右侧
	for(int i = 0; i < n; i++)
	{
		dfs(grid, firstBorder, i, 0);		// 遍历最左列,接触第一组边界
		dfs(grid, secondBorder, i,m-1);		// 遍历最右列,接触第二组边界
	}
	
	//上下边界
	for(int j = 0; j < m; j++)
	{
		dfs(grid, firstBorder, 0, j);		// 遍历最上册,接触第一组边界
		dfs(grid, secondBorder, n-1, j);	// 遍历最下列,接触第二组边界
	}
	
	
	// 遍历每一个点,看是否能同时到达第一组边界和第二组边界
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++)
		{
			if(firstBorder[i][j] && secondBorder[i][j])
				cout << i << " " << j << endl;
		}
	}
}

岛屿的周长

卡码网题目链接(ACM模式)

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

输出描述: 输出一个整数,表示岛屿的周长。

输出示例: 14

提示信息:

思路:

  • 岛屿问题最容易让人想到BFS或者DFS,但本题确实还用不上。 为了避免惯性思维,所以这道题。

  • 遍历每一个空格,遇到岛屿则计算其上下左右的空格情况。

  • 如果该陆地上下左右的空格是有水域,则说明是一条边,如图:

  • 如果该陆地上下左右的空格出界了,则说明是一条边,如图:

程序实现:

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	int n, m;
	cin >> n >> m;
	int dir[4][2] = {1,0,0,1,-1,0,0,-1};	
	vector<vector<int>> grid(n, vector<int>(m));
	
	for(int i = 0; i < n; i++){
		for(int j = 0; j < m; j++){
			cin >> grid[i][j];
		}
	}
	
	int res = 0;
	for(int i = 0; i < n; i++)
	{
		for(int j = 0; j < m; j++)
		{
			// 遇到陆地
			if(grid[i][j] == 1)
			{
				//计算周围节点坐标
				for(int k = 0; k < 4; k++)
				{
					int nexti = i + dir[k][0];
					int nextj = j + dir[k][1];
					if(	nexti < 0 || nexti >= n || 	// 越界
						nextj < 0 || nextj >= m ||	// 越界
						grid[nexti][nextj] == 0		// 水域
						)
					{
						res++;
					}
				}
			}
		}
	}
	cout << res << endl;
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2203511.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《企业实战分享 · SonarQube10.x 代码质量推广手册》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

【AI算法岗面试八股面经【超全整理】——CV】

AI算法岗面试八股面经【超全整理】 概率论【AI算法岗面试八股面经【超全整理】——概率论】信息论【AI算法岗面试八股面经【超全整理】——信息论】机器学习【AI算法岗面试八股面经【超全整理】——机器学习】深度学习【AI算法岗面试八股面经【超全整理】——深度学习】NLP【A…

vbox创建虚拟机后用户没有root 权限

XXX is not in the sudoers file. This incident will be reported. 打开终端输入 Su - 输入密码进入root账户 终端visudo修改配置文件 添加如下代码&#xff1a;Syy ALL(ALL:ALL) ALL 编写完成后保存文件 ctrlO 后输入文件名 sudoers.tmp 后按Enter键 退出编辑器&#xf…

018 发布商品

文章目录 获取分类关联的品牌CategoryBrandController.javaCategoryBrandServiceImpl.javaBrandVo.java 获取分类下的所有分组&关联属性AttrGroupController.javaAttrGroupServiceImpl.java 保存七张表sqltb_spu_info.sqltb_spu_info_desc.sqltb_spu_images.sqltb_product_…

UE4 材质学习笔记06(布料着色器/体积冰着色器)

一.布料着色器 要编写一个着色器首先是看一些参考图片&#xff0c;我们需要找出一些布料特有的特征&#xff0c;下面是一个棉织物&#xff0c;可以看到布料边缘的纤维可以捕捉光线使得边缘看起来更亮 下面是缎子和丝绸的图片&#xff0c;与棉织物有几乎相反的效果&#xff0c;…

基于SPI的flash读写操作

1、实验目标 使用页写或连续写操作向Flash芯片写入数据&#xff0c;再使用数据读操作读取之前写入数据&#xff0c;将读取的数据使用串口传回PC机&#xff0c;使用串口助手传回数据并与之前写入数据比较&#xff0c;判断正误。 注意&#xff1a;在向Flash芯片写入数据之前&…

【Redis原理】数据结构(上)

文章目录 动态字符串(SDS)概念SDS特点SDS的优势 IntSet概念IntSet的特点升序统一的编码格式IntSet自动升级 Dict概念Dict特点Dict的伸缩Dict的扩容Dict收缩 Dict的rehash渐进式哈希 总结Dict的结构Dict的伸缩 动态字符串(SDS) 概念 Redis是使用C语言实现的,C语言字符串底层是…

【后端开发】自动化部署、服务管理、问题排查工具(cicd流水线,k8s集群,ELK日志)

【后端开发】自动化部署、服务管理、问题排查工具&#xff08;cicd流水线&#xff0c;k8s集群&#xff0c;ELK日志&#xff09; 文章目录 1、Devops与CICD流水线(TeamCity, Jenkins&#xff0c;GitHub Actions)2、Kubernetes 集群的管理和操作&#xff08;对比Portainer&#x…

【解决】Set-ExecutionPolicy不是内部或外部命令

简介 当使用 VsCode 配置Django项目时&#xff0c;需要配置环境&#xff0c;但是当切换至虚拟环境时&#xff0c;出现了下面的情况。 无法加载文件&#xff1a;D:\django\Scripts\Activate.ps1&#xff0c; 上述问题可通过下面的命令进行解决 解决方法 1 命令行(最好是管理员…

JVM进阶调优系列(1)类加载器原理一文讲透

今天开始写JVM调优系列&#xff0c;并发编程系列也会继续穿插连载&#xff0c;让各位同学闲暇之余有更多阅读选择。 起笔写第一篇&#xff0c;并不好写。首先要构思整个系列的大概框架&#xff0c;一个好的框架一定是深度上由浅入深、逻辑上有严格顺序&#xff0c;读者订阅跟踪…

免费获取的8个SVG图标库,轻松下载与复制!

SVG图标相比传统的JPG、PNG图标具有诸多优势&#xff0c;适用于各种类型的图像&#xff0c;不仅能在不同尺寸下保持清晰度&#xff0c;还具备高度压缩性和轻量特性&#xff0c;支持静态和动态效果。因此&#xff0c;SVG格式在网页设计中往往是优选。尽管如今有很多免费的图标库…

风扇PD协议取电协议芯片-ECP 5702

随着USB-C的普及&#xff0c;市面上消费者PD充电器越来越多&#xff0c;如何让小家电产品也能够支持PD协议快充呢&#xff1f;加入一颗能芯科技PD协议取电协议芯片ECP5702试试看 USB PD协议受电端诱骗协议芯片 1、概述 ECP5702是能芯科技开发的一款专门PD协议的Sink控制器。 …

【论文速看】DL最新进展20241010-扩散模型、目标检测、行人检测

目录 【扩散模型】【目标检测】【行人检测】 【扩散模型】 []Faster Diffusion: Rethinking the Role of UNet Encoder in Diffusion Models 论文链接&#xff1a;https://arxiv.org/pdf/2312.09608 代码链接&#xff1a;https://github.com/hutaiHang/Faster-Diffusion 扩散…

No.10 笔记 | PHP学习指南:PHP数组掌握

本指南为PHP开发者提供了一个全面而简洁的数组学习路径。从数组的基本概念到高级操作技巧&#xff0c;我们深入浅出地解析了PHP数组的方方面面。无论您是初学者还是寻求提升的中级开发者&#xff0c;这份指南都能帮助您更好地理解和运用PHP数组&#xff0c;提高编码效率和代码质…

java批量发送邮件:如何实现高效邮件群发?

java批量发送邮件的教程指南&#xff1f;利用Java实现邮件批发&#xff1f; 随着技术的进步&#xff0c;java批量发送邮件已经成为企业实现高效邮件群发的关键工具。AokSend将探讨如何利用java批量发送邮件技术&#xff0c;实现高效的邮件群发&#xff0c;提升营销效果。 jav…

相当炸裂!495页看漫画学Python(全彩版)通俗易懂!Git首发破万Star

今天给大家分享一份由清华大学出品的《看漫画学Python》&#xff0c;本书作者对每一幅漫画表达的准确性也进行了N遍的推敲和打磨&#xff0c;向广大读者奉献一本精品漫画Python技术书。 总共495页&#xff0c;书中结合了幽默的故事情节和实用的编程知识&#xff0c;使得学习过…

【LeetCode】动态规划—673. 最长递增子序列的个数(附完整Python/C++代码)

动态规划—673. 最长递增子序列的个数 前言题目描述基本思路1. 问题定义2. 理解问题和递推关系3. 解决方法3.1 动态规划方法3.2 优化方法 4. 进一步优化5. 小总结 代码实现PythonPython3代码实现Python 代码解释 CC代码实现C 代码解释1. 初始化&#xff1a;2. 动态规划过程&…

Basic Pentesting靶机打靶记录

一、靶机介绍 下载链接&#xff1a;https://download.vulnhub.com/basicpentesting/basic_pentesting_1.ova 二、信息收集 确认靶机ip&#xff1a;192.168.242.136 arp-scan -l 扫描端口 nmap -p- -A -sS 192.168.242.136 这里开放了21&#xff0c;22&#xff0c;80端口 扫…

美发店数字化转型:SpringBoot管理系统

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

鸿蒙开发:文件推送到沙箱路径

最近一个项目需要基于沙箱路径下的文件进行操作&#xff0c;奈何应用沙箱路径下没有。找来找去方法都是要把文件推送进去。以下是我的一些拙见&#xff0c;请各位看官老爷指点一二。 沙箱路径 沙箱路径&#xff08;Sandbox Path&#xff09;通常是指在计算机安全和软件开发中…