1、题目
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
2、解答
这道题是一道很经典的题目,比较好想到的是DFS和BFS方法。最开始我也想到是这两种解法,看到题解后还学到了并查集的解法,也是很好的一个学习。
- DFS:深度优先遍历,就是遍历网格时,如果遍历到“1”,则把前后左右的“1”都递归变为“0”。理解一下深度优先遍历的思想,就是一直往更深层次满足某种特征的元素遍历。则岛的个数其实就是开始出发DFS的次数。
- BFS:广度优先遍历,这个和上面的区别是每次以广度优先,先遍历完某一次满足条件的点,然后再遍历更深层次。这里需要借助队列。
- 并查集:这个解法的核心思想是用集合中的一个元素代表集合。对于网格这个二维数组来说,可以映射到一个一维数组,这个数组对应的元素保存的值是对应索引元素所在的集合。这个集合可以直接用对应的索引值来表示。这个集合其实就是题目中这个岛屿的概念。
对于并查集来说,主要分为三个部分:初始化、查询和合并。
例如:
grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
1、初始化
这是一个4*5的网格,映射的一维数组长度就是4*5.
对于并查集来说维护一个parent数组,长度是4*5,每个元素维护的是该索引位置元素所属的集合编号。
其实换一种说法,这个集合其实就是一棵树,然后这个集合编号就是对应树的根节点。
例如对于grid[0][1]这个元素来说,它映射到parent数组的位置是0*5+1=1。
在最开始的时候,咱们假设每个元素都是一棵树,那么对应的根节点就是它本身。
然后为了后面合并方便,还对应的维护另外一个与parent对应的rank数组,对应的是记录
对应所属子树的复杂度,初始都是1或者0都行。
2、查询
查询指的是查询某个元素所属子树的根节点。代码实现很简单。当然这个还有一些细节,例如如何进行查询路径压缩。
例如查询索引位置为X的根节点,要么parent[x]==x,
要么不等于就递归查询parent[parent[x]].
3、合并
合并X和Y两个元素,先查询他俩对应的根节点是否一致,
一致不用管,
不一致的话,查看对应子树的复杂度,
把复杂度低的合并到复杂度高的上去;
复杂度一致的时候,把X合并到Y上,然后把Y的复杂度加一。
该方法学习强烈推荐学习:https://zhuanlan.zhihu.com/p/93647900/。
下次专门挑几道题目来学习一下这个算法。
class Solution(object):
def numIslands(self, grid):
"""
dfs:深度优先搜索,递归搜索将为连成岛屿的1全部变成0
:type grid: List[List[str]]
:rtype: int
"""
row = len(grid)
if row == 0:
return 0
col = len(grid[0])
nums = 0
for i in range(row):
for j in range(col):
if grid[i][j] == "1":
nums += 1
self.dfs(grid, i, j, row, col)
return nums
def dfs(self, grid, x, y, row, col):
grid[x][y] = "0"
for i, j in [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]:
if i > row - 1 or j > col - 1 or i < 0 or j < 0:
continue
if grid[i][j] == "1":
self.dfs(grid, i, j, row, col)
def numIslands2(self, grid):
"""
bfs:广度优先遍历,需要借助队列来遍历
:type grid: List[List[str]]
:rtype: int
"""
row = len(grid)
if row == 0:
return 0
col = len(grid[0])
nums = 0
queue = []
for i in range(row):
for j in range(col):
if grid[i][j] == "1":
nums += 1
grid[i][j] = 0
queue.append([i, j])
while len(queue) > 0:
x, y = queue.pop(0)
for cur_x, cur_y in [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]:
if 0 <= cur_x <= row - 1 and 0 <= cur_y <= col - 1 and grid[cur_x][cur_y] == "1":
grid[cur_x][cur_y] = 0
queue.append([cur_x, cur_y])
return nums
def numIslands3(self, grid):
"""
并查集:
:type grid: List[List[str]]
:rtype: int
"""
row = len(grid)
if row == 0:
return 0
col = len(grid[0])
union = UnionFind(grid)
for i in range(row):
for j in range(col):
if grid[i][j] == "1":
grid[i][j] == "0"
for cur_x, cur_y in [(i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)]:
if 0 <= cur_x <= row - 1 and 0 <= cur_y <= col - 1 and grid[cur_x][cur_y] == "1":
union.merge(i*col+j, cur_x*col+cur_y)
return union.get_count()
class UnionFind:
def __init__(self, grid):
"""
并查集初始化,并查集的重要思想在于,用集合中的一个元素代表集合,可以参考树结构用根节点表示这个树,同一个集合就是一棵树
:param grid:
"""
row = len(grid)
col = len(grid[0])
# parent数组保存的是当前索引位置对应的父节点的索引值
self.parent = [i for i in range(row*col)]
# 标记对应索引位置所在子树/集合的复杂度,用处在于在合并两个集合/子树时,优先把复杂度低的集合合并到复杂度高的集合中,这样整体复杂度不变
self.rank = [0] * (row*col)
self.count = 0
for i in range(row):
for j in range(col):
if grid[i][j]=="1":
self.count+=1
def find(self, x):
if self.parent[x]==x:
return x
else:
# 递归设置父节点为最终的根节点,能够压缩查询路径
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def merge(self, x, y):
x_parent = self.find(x)
y_parent = self.find(y)
# 如果这两个节点对应的祖先节点是同一个的话,代表已经merge过了
if x_parent==y_parent:
return
self.count -= 1
x_rank = self.rank[x_parent]
y_rank = self.rank[y_parent]
# 下面这个逻辑就是把复杂度低的子树合并到复杂度高的子树中去,复杂度相同时,把合并后的子树复杂度加一
if x_rank<y_rank:
self.parent[x_parent] = y_parent
elif x_rank>y_rank:
self.parent[y_parent] = x_parent
else:
self.parent[y_parent] = x_parent
self.rank[x_parent]+=1
def get_count(self):
return self.count
if __name__ == '__main__':
grid = [
["1"],["1"]
]
sol = Solution()
res = sol.numIslands3(grid)
print(res)