100. 岛屿的最大面积
卡码网题目链接(ACM模式)
题目描述
给定一个由 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
输出示例 4
提示信息
样例输入中,岛屿的最大面积为 4。
数据范围:
1 <= M, N <= 50。
思考
计算岛屿的最大面积: 岛屿由水平方向或垂直方向上相邻的陆地连接而成。
方岛屿面积的计算式为组成岛屿的陆地的总数: 几个陆地 = 几个面积
垂直、水平: 上下左右:[(0, 1), (0, -1), (-1, 0), (1, 0)]
# dfs bfs 都可以---目的找到当前的 (i, j)位置所连接(垂直 or 水平)的所有陆地。
邻接矩阵 : 1. dfs; 2. bfs
code python 1
# 先谋定 而后动
# dfs 需要面积,所以返回面积值
def dfs(graph, visited, result, x, y):
# 四个方向确定好
dir = [(0, 1), (0, -1), (-1, 0), (1, 0)]
# 遍历是个方向,收集结果,递归,回溯
for i in range(4):
nextx = x + dir[i][0]
nexty = y + dir[i][1]
if nextx < 0 or nexty < 0 or nextx >= len(graph) or nexty >= len(graph[0]): continue # 不满足条件
if not visited[nextx][nexty] and graph[nextx][nexty] == 1:
visited[nextx][nexty] = True
result[0] += 1
dfs(graph, visited, result, nextx, nexty) # visited被标记表示下一次不会在搜索它, 说明已经实现了遍历完的功能 而面积,就是在上一个的基础上叠加,所以不需要回溯
def main():
n, m = [int(v) for v in input().split(' ')]
graph = []
visited = [[False for _ in range(m)] for _ in range(n)]
n1 = n
while n1:
n1 -= 1
v = [int(v) for v in input().split(' ')]
graph.append(v)
areas = 0
for i in range(n):
for j in range(m):
if not visited[i][j] and graph[i][j] == 1:
visited[i][j] = True
res = [1]
dfs(graph, visited, res, i, j)
areas = max(res[0], areas)
print(areas)
main()
code python 2
# 先谋定 而后动
# dfs 需要面积,所以返回面积值
from collections import deque
def bfs(graph, visited, x, y):
# 四个方向确定好
dir = [(0, 1), (0, -1), (-1, 0), (1, 0)]
que = deque()
que.append([x, y])
result = 1
while que:
cur = que.popleft()
for i in range(4):
nextx = cur[0] + dir[i][0]
nexty = cur[1] + dir[i][1]
if nextx < 0 or nexty < 0 or nextx >= len(graph) or nexty >= len(graph[0]): continue # 不满足条件
if not visited[nextx][nexty] and graph[nextx][nexty] == 1: # 满足条件
visited[nextx][nexty] = True
result += 1
que.append([nextx,nexty])
return result
def main():
n, m = [int(v) for v in input().split(' ')]
graph = []
visited = [[False for _ in range(m)] for _ in range(n)]
n1 = n
while n1:
n1 -= 1
v = [int(v) for v in input().split(' ')]
graph.append(v)
areas = 0
for i in range(n):
for j in range(m):
if not visited[i][j] and graph[i][j] == 1:
visited[i][j] = True
res = bfs(graph, visited, i, j)
areas = max(res, areas)
print(areas)
main()
101. 孤岛的总面积
卡码网: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
提示信息:
在矩阵中心部分的岛屿,因为没有任何一个单元格接触到矩阵边缘,所以该岛屿属于孤岛,总面积为 1。
数据范围:
1 <= M, N <= 50。
思考
孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。
计算所有孤岛的总面积,岛屿面积的计算方式为组成岛屿的陆地的总数。
关键词: 在内部水平 or 垂直连接的最大面积。
--- 如果接触到边缘则面积记录为0
code python 1
# 先谋定 而后动
# dfs 需要面积,所以返回面积值
# flag:开始默认不在边缘
def dfs(graph, visited, result, flag, x, y):
# 四个方向确定好
dir = [(0, 1), (0, -1), (-1, 0), (1, 0)]
# 遍历是个方向,收集结果,递归,回溯
for i in range(4):
nextx = x + dir[i][0]
nexty = y + dir[i][1]
if nextx < 0 or nexty < 0 or nextx >= len(graph) or nexty >= len(graph[0]): continue # 不满足条件
if not visited[nextx][nexty] and graph[nextx][nexty] == 1:
if nextx == 0 or nexty == 0 or nextx == len(graph) -1 or nexty >= len(graph[0]) -1: # 有一个位置在边缘,使用flag进行标记
flag[0] = True
visited[nextx][nexty] = True
result[0] += 1
dfs(graph, visited, result, flag, nextx, nexty) # visited被标记表示下一次不会在搜索它, 说明已经实现了遍历完的功能 而面积,就是在上一个的基础上叠加,所以不需要回溯
def main():
n, m = [int(v) for v in input().split(' ')]
graph = []
visited = [[False for _ in range(m)] for _ in range(n)]
n1 = n
while n1:
n1 -= 1
v = [int(v) for v in input().split(' ')]
graph.append(v)
areas = 0
for i in range(n):
for j in range(m):
if not visited[i][j] and graph[i][j] == 1:
visited[i][j] = True
res = [1]
flag = [False]
dfs(graph, visited, res,flag, i, j)
if not flag[0]:
areas = max(res[0], areas) # 如果没有被标记在边缘, 在中间
print(areas)
main()
bfs 可行吗?
可行, 无论是哪个都只是为了找到当前的可行点能到达的所有可行点,同时标记是否为边缘
code python 2
# 先谋定 而后动
# dfs 需要面积,所以返回面积值
from collections import deque
def bfs(graph, visited, x, y):
# 四个方向确定好
dir = [(0, 1), (0, -1), (-1, 0), (1, 0)]
que = deque()
que.append([x, y])
result = 1
flag = False
while que:
cur = que.popleft()
for i in range(4):
nextx = cur[0] + dir[i][0]
nexty = cur[1] + dir[i][1]
if nextx < 0 or nexty < 0 or nextx >= len(graph) or nexty >= len(graph[0]): continue # 不满足条件
if not visited[nextx][nexty] and graph[nextx][nexty] == 1: # 满足条件
if nextx == 0 or nexty == 0 or nextx == len(graph) - 1 or nexty >= len(graph[0]) - 1: # 有一个位置在边缘,使用flag进行标记
flag = True
visited[nextx][nexty] = True
result += 1
que.append([nextx,nexty])
return flag, result
def main():
n, m = [int(v) for v in input().split(' ')]
graph = []
visited = [[False for _ in range(m)] for _ in range(n)]
n1 = n
while n1:
n1 -= 1
v = [int(v) for v in input().split(' ')]
graph.append(v)
areas = 0
for i in range(n):
for j in range(m):
if not visited[i][j] and graph[i][j] == 1:
visited[i][j] = True
flag, res = bfs(graph, visited, i, j)
if not flag:
areas = max(res, areas) # 如果没有被标记在边缘, 在中间
print(areas)
main()
思路2
本题使用dfs,bfs,并查集都是可以的。
本题要求找到不靠边的陆地面积,那么我们只要从周边找到陆地然后 通过 dfs或者bfs 将周边靠陆地且相邻的陆地都变成海洋,然
后再去重新遍历地图 统计此时还剩下的陆地就可以了。
如图,在遍历地图周围四个边,靠地图四边的陆地,都为绿色,
在遇到地图周边陆地的时候,将1都变为0,此时地图为这样:
然后我们再去遍历这个地图,遇到有陆地的地方,去采用深搜或者广搜,边统计所有陆地。
如果对深搜或者广搜不够了解,建议先看这里:深度优先搜索精讲,广度优先搜索精讲。
采用深度优先搜索的代码如下:
#include <iostream>
#include <vector>
using namespace std;
int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 保存四个方向
int count; // 统计符合题目要求的陆地空格数量
void dfs(vector<vector<int>>& grid, int x, int y) {
grid[x][y] = 0;
count++;
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] == 0) continue;
dfs (grid, nextx, nexty);
}
return;
}
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];
}
}
// 从左侧边,和右侧边 向中间遍历
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);
}
count = 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 << count << endl;
}
//采用广度优先搜索的代码如下:
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int count = 0;
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向
void bfs(vector<vector<int>>& grid, int x, int y) {
queue<pair<int, int>> que;
que.push({x, y});
grid[x][y] = 0; // 只要加入队列,立刻标记
count++;
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 (grid[nextx][nexty] == 1) {
que.push({nextx, nexty});
count++;
grid[nextx][nexty] = 0; // 只要加入队列立刻标记
}
}
}
}
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];
}
}
// 从左侧边,和右侧边 向中间遍历
for (int i = 0; i < n; i++) {
if (grid[i][0] == 1) bfs(grid, i, 0);
if (grid[i][m - 1] == 1) bfs(grid, i, m - 1);
}
// 从上边和下边 向中间遍历
for (int j = 0; j < m; j++) {
if (grid[0][j] == 1) bfs(grid, 0, j);
if (grid[n - 1][j] == 1) bfs(grid, n - 1, j);
}
count = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) bfs(grid, i, j);
}
}
cout << count << endl;
}
102. 沉没孤岛
卡码网题目链接(ACM模式)
题目描述:
给定一个由 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
提示信息:
将孤岛沉没:
数据范围:
1 <= M, N <= 50
思路
将中间的 孤岛的位置变成0,
先把边缘的非孤岛用 2 表示, 再把中间的 1 变成 0, 再把2 变成 1.
所以 dfs bfs 都可以的。
code python 1 dfs
def dfs(graph, visited, x, y):
graph[x][y] = 2
dir = [(0, 1), (0, -1), (-1, 0), (1, 0)]
# 遍历是个方向,收集结果,递归,回溯
for i in range(4):
nextx = x + dir[i][0]
nexty = y + dir[i][1]
if nextx < 0 or nexty < 0 or nextx >= len(graph) or nexty >= len(graph[0]): continue # 不满足条件
if not visited[nextx][nexty] and graph[nextx][nexty] == 1:
visited[nextx][nexty] = True
dfs(graph, visited, nextx, nexty) # visited被标记表示下一次不会在搜索它, 说明已经实现了遍历完的功能 而面积,就是在上一个的基础上叠加,所以不需要回溯
def main():
n, m = [int(v) for v in input().split(' ')]
graph = []
visited = [[False for _ in range(m)] for _ in range(n)]
n1 = n
while n1:
n1 -= 1
v = [int(v) for v in input().split(' ')]
graph.append(v)
# 上边,下边
for j in range(m):
if graph[0][j] == 1: dfs(graph, visited, 0, j)
if graph[n-1][j] == 1: dfs(graph, visited, n-1, j)
# 左边、右边
for i in range(n):
if graph[i][0] == 1: dfs(graph, visited, i, 0)
if graph[i][m-1]==1:dfs(graph, visited, i, m-1)
# 把中间的孤岛变成 0
for i in range(1, n-1):
for j in range(1, m-1):
if graph[i][j] == 1:
graph[i][j] = 0
# 把边缘的岛屿 2 变成 1
for i in range(n):
for j in range(m):
if graph[i][j] == 2:
graph[i][j] = 1
# 打印出最后的岛屿
for v in graph:
for i in v[:-1]:
print(i, end=' ')
print(v[-1])
main()
code python 2 bfs
## code python 2
# 先谋定 而后动
# dfs 需要面积,所以返回面积值
from collections import deque
def bfs(graph, visited, x, y):
# 四个方向确定好
dir = [(0, 1), (0, -1), (-1, 0), (1, 0)]
que = deque()
graph[x][y] = 2
que.append([x, y])
while que:
cur = que.popleft()
for i in range(4):
nextx = cur[0] + dir[i][0]
nexty = cur[1] + dir[i][1]
if nextx < 0 or nexty < 0 or nextx >= len(graph) or nexty >= len(graph[0]): continue # 不满足条件
if not visited[nextx][nexty] and graph[nextx][nexty] == 1: # 满足条件
visited[nextx][nexty] = True
graph[nextx][nexty] = 2
que.append([nextx,nexty])
def main():
n, m = [int(v) for v in input().split(' ')]
graph = []
visited = [[False for _ in range(m)] for _ in range(n)]
n1 = n
while n1:
n1 -= 1
v = [int(v) for v in input().split(' ')]
graph.append(v)
# 上边,下边
for j in range(m):
if graph[0][j] == 1: bfs(graph, visited, 0, j)
if graph[n-1][j] == 1: bfs(graph, visited, n-1, j)
# 左边、右边
for i in range(n):
if graph[i][0] == 1: bfs(graph, visited, i, 0)
if graph[i][m-1]==1:bfs(graph, visited, i, m-1)
# 把中间的孤岛变成 0
for i in range(1, n-1):
for j in range(1, m-1):
if graph[i][j] == 1:
graph[i][j] = 0
# 把边缘的岛屿 2 变成 1
for i in range(n):
for j in range(m):
if graph[i][j] == 2:
graph[i][j] = 1
# 打印出最后的岛屿
for v in graph:
for i in v[:-1]:
print(i, end=' ')
print(v[-1])
main()
103. 水流问题
卡码网题目链接(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
提示信息:
图中的蓝色方块上的雨水既能流向第一组边界,也能流向第二组边界。所以最终答案为所有蓝色方块的坐标。
数据范围:
1 <= M, N <= 50
思路
从这些单元格出发的水可以达到第一组边界和第二组边界。
矩阵的左边界和上边界被认为是第一组边界,
而矩阵的右边界和下边界被视为第二组边界。
flag1 = 第一边界
flag2 = 第二边界
code python 1 dfs
# 先谋定 而后动
# dfs 需要面积,所以返回面积值
def dfs(graph, visited, flag1, flag2, x, y):
"""
四个方向: 如果值 <= graph(x,j),则继续搜索
如果有一个值到达第一边界 则 flag = True
如果一个值到达第二边界 则 flag2 = True
"""
dir = [(0, 1), (0, -1), (-1, 0), (1, 0)]
# 遍历是个方向,收集结果,递归,回溯
for i in range(4):
nextx = x + dir[i][0]
nexty = y + dir[i][1]
if nextx < 0 or nexty < 0 or nextx >= len(graph) or nexty >= len(graph[0]): continue # 不满足条件
if not visited[nextx][nexty] and graph[nextx][nexty] <= graph[x][y]: # 可以流过改点
visited[nextx][nexty] = True
if nextx == 0 or nexty == 0:
flag1[0] = True
if nextx == len(graph)-1 or nexty == len(graph[0]) - 1:
flag2[0] = True
dfs(graph, visited, flag1, flag2, nextx, nexty) # visited被标记表示下一次不会在搜索它, 说明已经实现了遍历完的功能 而面积,就是在上一个的基础上叠加,所以不需要回溯
def main():
n, m = [int(v) for v in input().split(' ')]
graph = []
n1 = n
while n1:
n1 -= 1
v = [int(v) for v in input().split(' ')]
graph.append(v)
result = []
for i in range(n):
for j in range(m):
flag1 = [False]
flag2 = [False]
visited = [[False for _ in range(m)] for _ in range(n)]
visited[i][j] = True
if i == 0 or j == 0:
flag1[0] = True
if i == len(graph) - 1 or j == len(graph[0]) - 1:
flag2[0] = True
dfs(graph, visited, flag1, flag2, i, j)
if flag1[0] and flag2[0]:
result.append([i, j])
# 输出
if result:
for v in result:
print(v[0], end = ' ')
print(v[1])
main()
code python 2 bfs
# 先谋定 而后动
# dfs 需要面积,所以返回面积值
from collections import deque
def bfs(graph, visited, x, y):
# 四个方向确定好
dir = [(0, 1), (0, -1), (-1, 0), (1, 0)]
que = deque()
que.append([x, y])
flag1,flag2 = False, False
while que:
cur = que.popleft()
for i in range(4):
nextx = cur[0] + dir[i][0]
nexty = cur[1] + dir[i][1]
if nextx < 0 or nexty < 0 or nextx >= len(graph) or nexty >= len(graph[0]): continue # 不满足条件
if not visited[nextx][nexty] and graph[nextx][nexty] <= graph[cur[0]][cur[1]]: # 满足条件
visited[nextx][nexty] = True
if nextx == 0 or nexty == 0:
flag1 = True
if nextx == len(graph) - 1 or nexty == len(graph[0]) - 1:
flag2 = True
if flag1 and flag2:return True, True
que.append([nextx, nexty])
return flag1, flag2
def main():
n, m = [int(v) for v in input().split(' ')]
graph = []
n1 = n
while n1:
n1 -= 1
v = [int(v) for v in input().split(' ')]
graph.append(v)
result = []
for i in range(n):
for j in range(m):
flag1 = False
flag2 = False
visited = [[False for _ in range(m)] for _ in range(n)]
visited[i][j] = True
if i == 0 or j == 0:
flag1 = True
if i == len(graph) - 1 or j == len(graph[0]) - 1:
flag2 = True
if flag1 and flag2:
result.append([i, j])
else:
flag1, flag2 = bfs(graph, visited, i, j)
if flag1 and flag2:
result.append([i, j])
# 输出
if result:
for v in result:
print(v[0], end = ' ')
print(v[1])
main()
优化 来自代码随想录
那么我们可以 反过来想,从第一组边界上的节点 逆流而上,将遍历过的节点都标记上。
同样从第二组边界的边上节点 逆流而上,将遍历过的节点也标记上。
然后两方都标记过的节点就是既可以流 太平洋 也可以流大西洋的节点。
从第一组边界边上节点出发,如图
code python 3 dfs
## code python 2 bfs
# 先谋定 而后动
# dfs 标定可达 逆流而上
def dfs1(graph, visited, x, y):
"""
四个方向: 如果值 <= graph(x,j),则继续搜索
如果有一个值到达第一边界 则 flag = True
如果一个值到达第二边界 则 flag2 = True
"""
visited[x][y] = True
dir = [(0, 1), (0, -1), (-1, 0), (1, 0)]
# 遍历是个方向,收集结果,递归,回溯
for i in range(4):
nextx = x + dir[i][0]
nexty = y + dir[i][1]
if nextx < 0 or nexty < 0 or nextx >= len(graph) or nexty >= len(graph[0]): continue # 不满足条件
if not visited[nextx][nexty] and graph[nextx][nexty] >= graph[x][y]: # 可以流过改点
dfs(graph, visited, nextx, nexty) # visited被标记表示下一次不会在搜索它, 说明已经实现了遍历完的功能 而面积,就是在上一个的基础上叠加,所以不需要回溯
def dfs(graph, visited, x, y):
"""
四个方向: 如果值 <= graph(x,j),则继续搜索
如果有一个值到达第一边界 则 flag = True
如果一个值到达第二边界 则 flag2 = True
"""
if visited[x][y]: return
visited[x][y] = True
dir = [(0, 1), (0, -1), (-1, 0), (1, 0)]
# 遍历是个方向,收集结果,递归,回溯
for i in range(4):
nextx = x + dir[i][0]
nexty = y + dir[i][1]
if nextx < 0 or nexty < 0 or nextx >= len(graph) or nexty >= len(graph[0]): continue # 不满足条件
if graph[nextx][nexty] < graph[x][y]: continue # 可以流过改点
dfs(graph, visited, nextx, nexty) # visited被标记表示下一次不会在搜索它, 说明已经实现了遍历完的功能 而面积,就是在上一个的基础上叠加,所以不需要回溯
def main():
n, m = [int(v) for v in input().split(' ')]
graph = []
visited1 = [[False for _ in range(m)] for _ in range(n)]
visited2 = [[False for _ in range(m)] for _ in range(n)]
n1 = n
while n1:
n1 -= 1
v = [int(v) for v in input().split(' ')]
graph.append(v)
# 最上面、最下面 逆流而上
for j in range(m):
dfs(graph, visited1, 0, j) # 上面
dfs(graph, visited2, n-1, j) # 下面
# 最左面、最右面 逆流而上
for i in range(n):
dfs(graph,visited1, i, 0) # 左面
dfs(graph, visited2, i, m-1) # 右面
for i in range(n):
for j in range(m):
if visited1[i][j] and visited2[i][j]:
print(i, end=' ')
print(j)
main()
104.建造最大岛屿
卡码网题目链接(ACM模式)(opens new window)
题目描述:
给定一个由 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
输出示例 6
提示信息
对于上面的案例,有两个位置可将 0 变成 1,使得岛屿的面积最大,即 6。
数据范围:
1 <= M, N <= 50。
思路
本题的一个暴力想法,应该是遍历地图尝试 将每一个 0 改成1,然后去搜索地图中的最大的岛屿面积。
计算地图的最大面积:遍历地图 + 深搜岛屿,时间复杂度为 n * n。
(其实使用深搜还是广搜都是可以的,其目的就是遍历岛屿做一个标记,相当于染色,那么使用哪个遍历方式都行,以下我用深搜来讲解)
每改变一个0的方格,都需要重新计算一个地图的最大面积,所以 整体时间复杂度为:n^4。
优化思路
其实每次深搜遍历计算最大岛屿面积,我们都做了很多重复的工作。
只要用一次深搜把每个岛屿的面积记录下来就好。
第一步:一次遍历地图,得出各个岛屿的面积,并做编号记录。可以使用map记录,key为岛屿编号,value为岛屿面积
第二步:再遍历地图,遍历0的方格(因为要将0变成1),并统计该1(由0变成的1)周边岛屿面积,将其相邻面积相加在一起,遍历所有 0 之后,就可以得出 选一个0变成1 之后的最大面积。
拿如下地图的岛屿情况来举例: (1为陆地)
0: 面积0
1: 面积1
2: 面积2
3: 面积3
4: 面积4
......
遍历 0 的 方格, 记录其四周的值 , 使用字典映射:总面积 = map[左] + map[右] + map[上] + map[下]
# 在家里的电脑中
110. 字符串接龙
卡码网题目链接(ACM模式)
题目描述
字典 strList 中从字符串 beginStr 和 endStr 的转换序列是一个按下述规格形成的序列:
序列中第一个字符串是 beginStr。
序列中最后一个字符串是 endStr。
每次转换只能改变一个字符。
转换过程中的中间字符串必须是字典 strList 中的字符串。
给你两个字符串 beginStr 和 endStr 和一个字典 strList,找到从 beginStr 到 endStr 的最短转换序列中的字符串数目。如果不存在这样的转换序列,返回 0。
输入描述
第一行包含一个整数 N,表示字典 strList 中的字符串数量。 第二行包含两个字符串,用空格隔开,分别代表 beginStr 和 endStr。 后续 N 行,每行一个字符串,代表 strList 中的字符串。
输出描述
输出一个整数,代表从 beginStr 转换到 endStr 需要的最短转换序列中的字符串数量。如果不存在这样的转换序列,则输出 0。
输入示例
6
abc def
efc
dbc
ebc
dec
dfc
yhn
输出示例
4
提示信息
从 startStr 到 endStr,在 strList 中最短的路径为 abc -> dbc -> dec -> def,所以输出结果为 4
数据范围:
2 <= N <= 500
思考
strList
从 beginStr 到 endStr
开始于 beginStr 结束于 endStr 。beginStr ----> v1 ----> v2 ----> v3 ----> v4 ---->...----> endStr
每次只能改变一个字符 + 转换过程中的中间字符串必须是字典 strList 中的字符串。
给你 2 个字符串 beginStr 和 endStr 和一个字典strList, 找到 beginStr 到 endStr 的最短转换序列中的字符串数目。 如果不存在, 返回0.
所以这道题要解决两个问题:
1. 图中的线是如何连在一起的
2. 起点和终点的最短路径长度
首先题目中并没有给出点与点之间的连线,而是要我们自己去连,条件是字符只能差一个。
所以判断点与点之间的关系,需要判断是不是差一个字符,如果差一个字符,那就是有链接。
然后就是求起点和终点的最短路径长度,这里无向图求最短路,广搜最为合适,广搜只要搜到了终点,那么一定是最短的路径。因为广搜就是以起点中心向四周扩散的搜索。
本题如果用深搜,会比较麻烦,要在到达终点的不同路径中选则一条最短路。 而广搜只要达到终点,一定是最短路。
另外需要有一个注意点:
本题是一个无向图,需要用标记位,标记着节点是否走过,否则就会死循环!
使用set来检查字符串是否出现在字符串集合里更快一些
code python bfs
from collections import deque
def bfs(strList, beginStr, endStr):
que = deque() # 初始化队列
que.append(beginStr)
visited = {beginStr: 1} # {<记录的字符串,路径长度>} 记录strSet里的字符串是否被访问过,同时记录路径长度
while que:
word = que.popleft()
path = visited[word] # 这个字符串在路径中的长度
for i in range(len(word)):
newword = list(word) # 用一个新字符串替换str,因为每次要置换一个字符 替换低 i 个字母,
for j in range(26):
newword[i] = chr(ord('a') + j) # 遍历26的字母
nword = ''.join(newword)
if nword == endStr: # 发现替换字母后,字符串与终点字符串相同, 则结束。
return path + 1 # 找到了路径
if (nword in strList) and (nword not in visited): # 字符串集合里出现了newWord,并且newWord没有被访问过
visited[nword] = path + 1 # 添加访问信息,并将新字符串放到队列中
que.append(nword)
return 0
def main():
N = int(input())
beginStr, endStr = input().split(' ')
strList = []
for _ in range(N):
strList.append(input())
res = bfs(strList, beginStr, endStr)
print(res)
code c++ bfs
#include <iostream>
#include <vector>
#include <string>
#include <unordered_set>
#include <unordered_map>
#include <queue>
using namespace std;
int main() {
string beginStr, endStr, str;
int n;
cin >> n;
unordered_set<string> strSet;
cin >> beginStr >> endStr;
for (int i = 0; i < n; i++) {
cin >> str;
strSet.insert(str);
}
// 记录strSet里的字符串是否被访问过,同时记录路径长度
unordered_map<string, int> visitMap; // <记录的字符串,路径长度>
// 初始化队列
queue<string> que;
que.push(beginStr);
// 初始化visitMap
visitMap.insert(pair<string, int>(beginStr, 1));
while(!que.empty()) {
string word = que.front();
que.pop();
int path = visitMap[word]; // 这个字符串在路径中的长度
// 开始在这个str中,挨个字符去替换
for (int i = 0; i < word.size(); i++) {
string newWord = word; // 用一个新字符串替换str,因为每次要置换一个字符
// 遍历26的字母
for (int j = 0 ; j < 26; j++) {
newWord[i] = j + 'a';
if (newWord == endStr) { // 发现替换字母后,字符串与终点字符串相同
cout << path + 1 << endl; // 找到了路径
return 0;
}
// 字符串集合里出现了newWord,并且newWord没有被访问过
if (strSet.find(newWord) != strSet.end()
&& visitMap.find(newWord) == visitMap.end()) {
// 添加访问信息,并将新字符串放到队列中
visitMap.insert(pair<string, int>(newWord, path + 1));
que.push(newWord);
}
}
}
}
// 没找到输出0
cout << 0 << endl;
}
小结
标记是否走过 否则就到死循环了。
105.有向图的完全可达性
卡码网题目链接(ACM模式)
【题目描述】
给定一个有向图,包含 N 个节点,节点编号分别为 1,2,...,N。现从 1 号节点开始,如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。
【输入描述】
第一行包含两个正整数,表示节点数量 N 和边的数量 K。 后续 K 行,每行两个正整数 s 和 t,表示从 s 节点有一条边单向连接到 t 节点。
【输出描述】
如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。
【输入示例】
4 4
1 2
2 1
1 3
3 4
【输出示例】1
【提示信息】
从 1 号节点可以到达任意节点,输出 1。
数据范围:
1 <= N <= 100;
1 <= K <= 2000。
## 思考
1 的所有可达是否包括所有的
bfs 最合适
dfs 也可以
code python bfs
def bfs(graph):
from collections import deque
que = deque(graph[1])
visited = [1]
# 1的直接可达
while que:
cur = que.popleft()
visited.append(cur)
for v1 in graph[cur]:
if v1 not in visited:
que.append(v1)
flag = set(visited) == set(range(1,len(graph)))
if flag:
return 1
return -1
def bfs(graph, visited):
from collections import deque
que = deque([1])
visted[1] = True
# 1的直接可达
while que:
cur = que.popleft()
for v1 in graph[cur]:
if v1 not in visited:
visited[v1] = True
que.append(v1)
for v in visited[1:]:
if not v:
return -1
return 1
def main():
N, K = [int(v) for v in input().split(' ')]
graph = [[] for _ in range(N+1)]
for _ in range(K):
v = [int(v) for v in input().split(' ')]
graph[v[0]].append(v[1])
flag = bfs(graph)
print(flag)
main()
code c++
// 写法一:dfs 处理当前访问的节点
#include <iostream>
#include <vector>
#include <list>
using namespace std;
void dfs1(const vector<list<int>>& graph, int key, vector<bool>& visited) {
if (visited[key]) {
return;
}
visited[key] = true;
list<int> keys = graph[key];
for (int key : keys) {
// 深度优先搜索遍历
dfs(graph, key, visited);
}
}
写法二:dfs处理下一个要访问的节点
#include <iostream>
#include <vector>
#include <list>
using namespace std;
void dfs(const vector<list<int>>& graph, int key, vector<bool>& visited) {
list<int> keys = rooms[key];
for (int key : keys) {
if (visited[key] == false) { // 确认下一个是没访问过的节点
visited[key] = true;
dfs(rooms, key, visited);
}
}
}
int main() {
int n, m, s, t;
cin >> n >> m;
vector<list<int>> graph(n + 1);
while (m--) {
cin >> s >> t;
graph[s].push_back(t);
}
vector<bool> visited(n + 1, false);
visited[0] = true; // 节点1 预先处理
dfs(graph, 1, visited);
for (int i = 1; i <= n; i++) {
if (visited[i] == false) {
cout << -1 << endl;
return 0;
}
}
cout << 1 << endl;
}
106. 岛屿的周长
卡码网题目链接(ACM模式)
题目描述
给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。
你可以假设矩阵外均被水包围。在矩阵中恰好拥有一个岛屿,假设组成岛屿的陆地边长都为 1,请计算岛屿的周长。岛屿内部没有水域。
输入描述
第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。
输出描述
输出一个整数,表示岛屿的周长。
输入示例
5 5
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
提示信息
岛屿的周长为 14。
数据范围:
1 <= M, N <= 50。
code python 1
N,M = 5, 5
graph = [[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]]
res = 0
dir = [(1,0),(0,1),(-1,0),(0,-1)]
for i in range(N):
for j in range(M):
if graph[i][j] == 1:
for k in range(4):
nextx = i + dir[k][0]
nexty = j + dir[k][1]
if nextx < 0 or nexty < 0 or nextx >= N or nexty >= M: continue
if graph[nextx][nexty] == 0:
res += 1
print(res)
code python 2
def main():
N, M = [int(v) for v in input().split(' ')]
graph = []
for _ in range(N):
v = [int(v) for v in input().split(' ')]
graph.append(v)
res = 0
dir = [(1, 0), (0, 1), (-1, 0), (0, -1)]
for i in range(N):
for j in range(M):
if graph[i][j] == 1:
for k in range(4):
nextx = i + dir[k][0]
nexty = j + dir[k][1]
if nextx < 0 or nexty < 0 or nextx >= N or nexty >= M: continue
if graph[nextx][nexty] == 0:
res += 1
print(res)
并查集理论基础
接下来我们来讲一下并查集,首先当然是并查集理论基础。
# 背景
首先要知道并查集可以解决什么问题呢? ----并查集常用来解决连通性问题。
大白话就是当我们需要判断两个元素是否在同一个集合里的时候,我们就要想到用并查集。
并查集主要有两个功能:
将两个元素添加到一个集合中。
判断两个元素在不在同一个集合
接下来围绕并查集的这两个功能来展开讲解。
#原理讲解
从代码层面,我们如何将两个元素添加到同一个集合中呢。
此时有录友会想到:可以把他放到同一个数组里或者set 或者 map 中,这样就表述两个元素在同一个集合。
那么问题来了,对这些元素分门别类,可不止一个集合,可能是很多集合,成百上千,那么要定义这么多个数组吗?
有录友想,那可以定义一个二维数组。
但如果我们要判断两个元素是否在同一个集合里的时候 我们又能怎么办? 只能把而二维数组都遍历一遍。
而且每当想添加一个元素到某集合的时候,依然需要把把二维数组都遍历一遍,才知道要放在哪个集合里。
这仅仅是一个粗略的思路,如果沿着这个思路去实现代码,非常复杂,因为管理集合还需要很多逻辑。
那么我们来换一个思路来看看。
我们将三个元素A,B,C (分别是数字)放在同一个集合,其实就是将三个元素连通在一起,如何连通呢。
只需要用一个一维数组来表示,即:father[A] = B,father[B] = C 这样就表述 A 与 B 与 C连通了(有向连通图)。
代码如下:
// 将v,u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
可能有录友想,这样我可以知道 A 连通 B,因为 A 是索引下标,根据 father[A]的数值就知道 A 连通 B。那怎么知道 B 连通 A呢?
我们的目的是判断这三个元素是否在同一个集合里,知道 A 连通 B 就已经足够了。
这里要讲到寻根思路,只要 A ,B,C 在同一个根下就是同一个集合。
给出A元素,就可以通过 father[A] = B,father[B] = C,找到根为 C。
给出B元素,就可以通过 father[B] = C,找到根也为为 C,说明 A 和 B 是在同一个集合里。 大家会想第一段代码里find函数是如何实现的呢?其实就是通过数组下标找到数组元素,一层一层寻根过程,代码如下:
// 并查集里寻根的过程
int find(int u) {
if (u == father[u]) return u; // 如果根就是自己,直接返回
else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找
}
如何表示 C 也在同一个元素里呢? 我们需要 father[C] = C,即C的根也为C,这样就方便表示 A,B,C 都在同一个集合里了。
所以father数组初始化的时候要 father[i] = i,默认自己指向自己。
代码如下:
// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
最后我们如何判断两个元素是否在同一个集合里,如果通过 find函数 找到 两个元素属于同一个根的话,那么这两个元素就是同一个集合,代码如下:
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
#路径压缩
在实现 find 函数的过程中,我们知道,通过递归的方式,不断获取father数组下标对应的数值,最终找到这个集合的根。
搜索过程像是一个多叉树中从叶子到根节点的过程,如图:
如果这棵多叉树高度很深的话,每次find函数 去寻找根的过程就要递归很多次。
我们的目的只需要知道这些节点在同一个根下就可以,所以对这棵多叉树的构造只需要这样就可以了,如图:
除了根节点其他所有节点都挂载根节点下,这样我们在寻根的时候就很快,只需要一步,
如果我们想达到这样的效果,就需要 路径压缩,将非根节点的所有节点直接指向根节点。 那么在代码层面如何实现呢?
我们只需要在递归的过程中,让 father[u] 接住 递归函数 find(father[u]) 的返回结果。
因为 find 函数向上寻找根节点,father[u] 表述 u 的父节点,那么让 father[u] 直接获取 find函数 返回的根节点,这样就让节点 u 的父节点 变成根节点。
代码如下,注意看注释,路径压缩就一行代码:
// 并查集里寻根的过程
int find(int u) {
if (u == father[u]) return u;
else return father[u] = find(father[u]); // 路径压缩
}
以上代码在C++中,可以用三元表达式来精简一下,代码如下:
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
相信不少录友在学习并查集的时候,对上面这三行代码实现的 find函数 很熟悉,但理解上却不够深入,仅仅知道这行代码很好用,不知道这里藏着路径压缩的过程。
所以对于算法初学者来说,直接看精简代码学习是不太友好的,往往忽略了很多细节。
#代码模板
那么此时并查集的模板就出来了, 整体模板C++代码如下:
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构
// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
通过模板,我们可以知道,并查集主要有三个功能。
寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点
#常见误区
这里估计有录友会想,模板中的 join 函数里的这段代码:
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
与 isSame 函数的实现是不是重复了? 如果抽象一下呢,代码如下:
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
if (isSame(u, v)) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
这样写可以吗? 好像看出去没问题,而且代码更精简了。
其实这么写是有问题的,在join函数中 我们需要寻找 u 和 v 的根,然后再进行连线在一起,而不是直接 用 u 和 v 连线在一起。
举一个例子:
join(1, 2);
join(3, 2);
此时构成的图是这样的:
此时问 1,3是否在同一个集合,我们调用 join(1, 2); join(3, 2); 很明显本意要表示 1,3是在同一个集合。
但我们来看一下代码逻辑,当我们调用 isSame(1, 3)的时候,find(1) 返回的是1,find(3)返回的是3。 return 1 == 3 返回的是false,代码告诉我们 1 和 3 不在同一个集合,这明显不符合我们的预期,所以问题出在哪里?
问题出在我们精简的代码上,即 join 函数 一定要先 通过find函数寻根再进行关联。
如果find函数是这么实现,再来看一下逻辑过程。
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
分别将 这两对元素加入集合。
join(1, 2);
join(3, 2);
当执行join(3, 2)的时候,会先通过find函数寻找 3的根为3,2的根为1 (第一个join(1, 2),将2的根设置为1),所以最后是将1 指向 3。
构成的图是这样的:
因为在join函数里,我们有find函数进行寻根的过程,这样就保证元素 1,2,3在这个有向图里是强连通的。
此时我们在调用 isSame(1, 3)的时候,find(1) 返回的是3,find(3) 返回的也是3,return 3 == 3 返回的是true,即告诉我们 元素 1 和 元素3 是 在同一个集合里的。
#模拟过程
(凸显途径合并的过程,每一个join都要画图)
不少录友在接触并查集模板之后,用起来很娴熟,因为模板确实相对固定,但是对并查集内部数据组织方式以及如何判断是否是同一个集合的原理很模糊。
通过以上讲解之后,我再带大家一步一步去画一下,并查集内部数据连接方式。
1、join(1, 8);
2、join(3, 8);
有录友可能想,join(3, 8) 在图中为什么 将 元素1 连向元素 3 而不是将 元素 8 连向 元素 3 呢?
这一点 我在 「常见误区」标题下已经详细讲解了,因为在join(int u, int v)函数里 要分别对 u 和 v 寻根之后再进行关联。
3、join(1, 7);
4、join(8, 5);
这里8的根是3,那么 5 应该指向 8 的根 3,这里的原因,我们在上面「常见误区」已经讲过了。 但 为什么 图中 8 又直接指向了 3 了呢?
因为路经压缩了
即如下代码在寻找根的过程中,会有路径压缩,减少 下次查询的路径长度。
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}
5、join(2, 9);
6、join(6, 9);
这里为什么是 2 指向了 6,因为 9的根为 2,所以用2指向6。
大家看懂这个有向图后,相信应该知道如下函数的返回值了。
cout << isSame(8, 7) << endl;
cout << isSame(7, 2) << endl;
返回值分别如下,表示,8 和 7 是同一个集合,而 7 和 2 不是同一个集合。
true
false
#拓展
在「路径压缩」讲解中,我们知道如何靠压缩路径来缩短查询根节点的时间。
其实还有另一种方法:按秩(rank)合并。
rank表示树的高度,即树中结点层次的最大值。
例如两个集合(多叉树)需要合并,如图所示:
树1 rank 为2,树2 rank 为 3。那么合并两个集合,是 树1 合入 树2,还是 树2 合入 树1呢?
我们来看两个不同方式合入的效果。
这里可以看出,树2 合入 树1 会导致整棵树的高度变的更高,而 树1 合入 树2 整棵树的高度 和 树2 保持一致。
所以在 join函数中如何合并两棵树呢?
一定是 rank 小的树合入 到 rank大 的树,这样可以保证最后合成的树rank 最小,降低在树上查询的路径长度。
按秩合并的代码如下:
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构
vector<int> rank = vector<int> (n, 1); // 初始每棵树的高度都为1
// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
rank[i] = 1; // 也可以不写
}
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : find(father[u]);// 注意这里不做路径压缩
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (rank[u] <= rank[v]) father[u] = v; // rank小的树合入到rank大的树
else father[v] = u;
if (rank[u] == rank[v] && u != v) rank[v]++; // 如果两棵树高度相同,则v的高度+1,因为上面 if (rank[u] <= rank[v]) father[u] = v; 注意是 <=
}
可以注意到在上面的模板代码中,我是没有做路径压缩的,因为一旦做路径压缩,rank记录的高度就不准了,根据rank来判断如何合并就没有意义。
也可以在 路径压缩的时候,再去实时修生rank的数值,但这样在代码实现上麻烦了不少,关键是收益很小。
其实我们在优化并查集查询效率的时候,只用路径压缩的思路就够了,不仅代码实现精简,而且效率足够高。
按秩合并的思路并没有将树形结构尽可能的扁平化,所以在整理效率上是没有路径压缩高的。
说到这里可能有录友会想,那在路径压缩的代码中,只有查询的过程 即 find 函数的执行过程中会有路径压缩,如果一直没有使用find函数,是不是相当于这棵树就没有路径压缩,导致查询效率依然很低呢?
大家可以再去回顾使用路径压缩的 并查集模板,在isSame函数 和 join函数中,我们都调用了 find 函数来进行寻根操作。
也就是说,无论使用并查集模板里哪一个函数(除了init函数),都会有路径压缩的过程,第二次访问相同节点的时候,这个节点就是直连根节点的,即 第一次访问的时候它的路径就被压缩了。
所以这里推荐大家直接使用路径压缩的并查集模板就好,但按秩合并的优化思路我依然给大家讲清楚,有助于更深一步理解并查集的优化过程。
#复杂度分析
这里对路径压缩版并查集来做分析。
空间复杂度: O(n) ,申请一个father数组。
关于时间复杂度,如果想精确表达出来需要繁琐的数学证明,就不在本篇讲解范围内了,大家感兴趣可以自己去深入研究。
这里做一个简单的分析思路。
路径压缩后的并查集时间复杂度在O(logn)与O(1)之间,且随着查询或者合并操作的增加,时间复杂度会越来越趋于O(1)。
了解到这个程度对于求职面试来说就够了。
在第一次查询的时候,相当于是n叉树上从叶子节点到根节点的查询过程,时间复杂度是logn,但路径压缩后,后面的查询操作都是O(1),而 join 函数 和 isSame函数 里涉及的查询操作也是一样的过程。
#总结
本篇我们讲解了并查集的背景、原理、两种优化方式(路径压缩,按秩合并),代码模板,常见误区,以及模拟过程。
要知道并查集解决什么问题,在什么场景下我们要想到使用并查集。
接下来进一步优化并查集的执行效率,重点介绍了路径压缩的方式,另一种方法:按秩合并,我们在 「拓展」中讲解。
通过一步一步的原理讲解,最后给出并查集的模板,所有的并查集题目都在这个模板的基础上进行操作或者适当修改。
但只给出模板还是不够的,针对大家学习并查集的常见误区,详细讲解了模板代码的细节。
为了让录友们进一步了解并查集的运行过程,我们再通过具体用例模拟一遍代码过程并画出对应的内部数据连接图(有向图)。
这里也建议大家去模拟一遍才能对并查集理解的更到位。
如果对模板代码还是有点陌生,不用担心,接下来我会讲解对应LeetCode上的并查集题目,通过一系列题目练习,大家就会感受到这套模板有多么的好用!
敬请期待 并查集题目精讲系列。
107. 寻找存在的路径
卡码网题目链接(ACM模式)
题目描述
给定一个包含 n 个节点的无向图中,节点编号从 1 到 n (含 1 和 n )。
你的任务是判断是否有一条从节点 source 出发到节点 destination 的路径存在。
输入描述
第一行包含两个正整数 N 和 M,N 代表节点的个数,M 代表边的个数。
后续 M 行,每行两个正整数 s 和 t,代表从节点 s 与节点 t 之间有一条边。
最后一行包含两个正整数,代表起始节点 source 和目标节点 destination。
输出描述
输出一个整数,代表是否存在从节点 source 到节点 destination 的路径。如果存在,输出 1;否则,输出 0。
输入示例
5 4
1 2
1 3
2 4
3 4
1 4
输出示例
1
提示信息
数据范围:
1 <= M, N <= 100。
#思路
本题是并查集基础题目。 如果还不了解并查集,可以看这里:并查集理论基础
并查集可以解决什么问题呢?
主要就是集合问题,两个节点在不在一个集合,也可以将两个节点添加到一个集合中。
这里整理出我的并查集模板如下:
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构
// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
以上模板中,只要修改 n 大小就可以。
并查集主要有三个功能:
寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点
简单介绍并查集之后,我们再来看一下这道题目。
为什么说这道题目是并查集基础题目,题目中各个点是双向图链接,那么判断 一个顶点到另一个顶点有没有有效路径其实就是看这两个顶点是否在同一个集合里。
如何算是同一个集合呢,有边连在一起,就算是一个集合。
此时我们就可以直接套用并查集模板。
使用 join(int u, int v)将每条边加入到并查集。
最后 isSame(int u, int v) 判断是否是同一个根 就可以了。
C++代码如下:
code c++
#include <iostream>
#include <vector>
using namespace std;
int n; // 节点数量
vector<int> father = vector<int> (101, 0); // 按照节点大小定义数组大小
// 并查集初始化
void init() {
for (int i = 1; i <= n; i++) father[i] = i;
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
int main() {
int m, s, t, source, destination;
cin >> n >> m;
init();
while (m--) {
cin >> s >> t;
join(s, t);
}
cin >> source >> destination;
if (isSame(source, destination)) cout << 1 << endl;
else cout << 0 << endl;
}
#其他语言版
code python
class Bingcheck:
def __init__(self, N):
self.father = [0 for _ in range(N + 1)] # list(range(N + 1))
for i in range(N + 1):
self.father[i] = i
def find1(self, u):
if u == self.father[u]: return u
else:
self.father[u] = self.find1(self.father[u])
return self.father[u]
def join(self, u,v):
u = self.find1(u)
v = self.find1(v)
if u != v:
self.father[v] = u
def isSame(self, u, v):
u = self.find1(u)
v = self.find1(v)
return u == v
S1 = Bingcheck(N = 5)
N = 5
M = 4
nodes = [(1,2), (1,3), (2,4), (3,4)]
sourse, destination = 1, 4
# S1.find1(1)
for node in nodes:
S1.join(node[0], node[1])
print(S1.father)
flag = S1.isSame(sourse, destination)
if flag:print(1)
else:print(0)
code python2
def main():
N,M = [int(v) for v in input().split(' ')]
# nodes = [(1,2), (1,3), (2,4), (3,4)]
# sourse, destination = 1, 4
nodes = []
for i in range(M):
v = [int(v) for v in input().split(' ')]
nodes.append(v)
sourse, destination = [int(v) for v in input().split(' ')]
father = [0 for _ in range(N + 1)]
def init1():
for i in range(N + 1):
father[i] = i
def find1(u):
if u == father[u]: return u
else:
father[u] = find1(father[u])
return father[u]
def join(u,v):
u = find1(u)
v = find1(v)
if u != v:
father[v] = u
def isSame(u, v):
u = find1(u)
v = find1(v)
return u == v
init1()
for node in nodes:
join(node[0], node[1])
print(father)
flag = isSame(sourse, destination)
if flag:print(1)
else:print(0)
main()