一:深度优先搜索
示例1
题目链接:886. 可能的二分法 - 力扣(LeetCode)
首先可以构建一个图的邻接表表示,然后使用深度优先搜索(DFS)算法来检查图是否可以二分。如果图可以二分,则返回 True;否则返回 False。具体步骤如下:
- 构建图:使用一个列表
graph
来存储每个节点的邻接节点。- 初始化颜色数组:使用一个数组
color
来记录每个节点的颜色,-1 表示未着色。- DFS 检查:定义一个 DFS 函数,递归地为每个节点着色,并检查其相邻节点的颜色是否满足二分条件。
- 遍历所有节点:确保每个节点都被检查,如果发现未着色的节点,则从该节点开始进行 DFS 检查。
- 返回结果:如果所有节点都满足二分条件,则返回 True;否则返回 False。
def possibleBipartition(n, dislikes):
# 创建一个大小为 n+1 的列表,用于存储图中的每个节点及其相邻节点
graph = [[] for _ in range(n + 1)]
# 遍历 dislikes 列表,构建图的邻接表表示
for u, v in dislikes:
graph[u].append(v) # 添加 v 到 u 的邻接列表
graph[v].append(u) # 添加 u 到 v 的邻接列表
# 初始化颜色数组,-1 表示节点尚未着色
color = [-1] * (n + 1)
# 定义深度优先搜索(DFS)函数,用于检查图是否可以二分
def dfs(node, c):
# 将当前节点着色为 c(0 或 1)
color[node] = c
# 遍历当前节点的所有相邻节点
for neighbor in graph[node]:
# 如果相邻节点未着色,则递归调用 dfs 着色为 1-c
if color[neighbor] == -1:
if not dfs(neighbor, 1 - c):
return False
# 如果相邻节点已着色且颜色与当前节点相同,则图不是二分的
elif color[neighbor] == c:
return False
# 如果所有相邻节点都满足条件,返回 True
return True
# 遍历所有节点,确保每个连通分量都被检查
for i in range(1, n + 1):
if color[i] == -1:
# 如果发现未着色的节点,从该节点开始 DFS 检查
if not dfs(i, 0):
return False
# 如果所有节点都满足二分条件,返回 True
return True
这段代码实现了 possibleBipartition
函数,用于判断是否可以将一组人分成两个组,使得每个人都不与其不喜欢的人在同一组。
示例2
总体思路如下:
- 初始化一个布尔数组
visited
来跟踪每个城市是否已经被访问过。- 初始化一个计数器
count
来记录省份的数量。- 使用深度优先搜索(DFS)来遍历图中的每个城市,找到所有相连的城市。
- 对于每个未访问的城市,执行DFS以找到与它相连的所有城市,并将它们标记为已访问。
- 每当找到一个未访问的城市时,就意味着发现了一个新的省份,因此将
count
加一。- 最终返回
count
的值,即省份的数量。
def findCircleNum(isConnected):
n = len(isConnected) # 获取城市的数量
visited = [False] * n # 初始化访问数组,所有城市初始状态都是未访问
count = 0 # 初始化省份计数器
def dfs(node): # 定义深度优先搜索函数
visited[node] = True # 标记当前城市为已访问
for neighbor in range(n): # 遍历所有城市
if isConnected[node][neighbor] == 1 and not visited[neighbor]: # 如果当前城市与邻居城市相连且邻居未访问
dfs(neighbor) # 递归地访问邻居城市
for i in range(n): # 遍历所有城市
if not visited[i]: # 如果当前城市未访问
dfs(i) # 从当前城市开始进行深度优先搜索
count += 1 # 发现一个新的省份,计数器加一
return count # 返回省份的总数
这段代码通过深度优先搜索(DFS)来找到并计数图中的连通分量,每个连通分量代表一个省份。visited
数组用于避免重复访问城市,从而确保每个省份只被计数一次。最终,count
变量包含了省份的总数,并被返回作为结果。这个方法的时间复杂度是 O(n^2),因为它需要遍历整个邻接矩阵,并且对于每个城市,可能需要遍历所有其他城市。空间复杂度是 O(n),用于存储 visited
数组和递归调用栈。
二:并查集
并查集可以结合其他参考材料的示意图进行理解。
示例1
题目链接:1971. 寻找图中是否存在路径 - 力扣(LeetCode)
总体思路如下:
- 使用并查集(Union-Find)数据结构来处理图的连通性问题。
- 初始化并查集,使得每个顶点初始时是其自身的父节点。
- 通过遍历所有边,将每对连接的顶点合并到同一个集合中。
- 最后,通过查找source和destination的根节点来判断它们是否在同一个集合内,从而确定是否存在从source到destination的有效路径。
def validPath(n, edges, source, destination):
# 初始化并查集,每个顶点初始时是其自身的父节点
parent = list(range(n))
# 查找根节点,使用路径压缩优化查找效率
def find(x):
# 如果当前节点的父节点不是它自己,则递归地找到其根节点
if parent[x] != x:
parent[x] = find(parent[x]) # 路径压缩,将查找路径上的所有节点直接连接到根节点
return parent[x]
# 合并两个集合,将两个顶点所在的集合合并
def union(x, y):
# 找到两个顶点的根节点
rootX = find(x)
rootY = find(y)
# 如果两个顶点不在同一个集合中,则合并它们
if rootX != rootY:
parent[rootY] = rootX # 将一个根节点作为另一个根节点的父节点
# 构建并查集,遍历所有边,合并每个边连接的两个顶点
for u, v in edges:
union(u, v)
# 检查source和destination是否在同一个集合中,如果在,则存在有效路径
return find(source) == find(destination)
并查集是一种树型的数据结构,用于处理一些不交集的合并及查询问题。初始化时,每个顶点都是一个独立的集合。find函数用于查找顶点的根节点,并进行路径压缩,以优化后续的查找操作。union函数用于合并两个顶点所在的集合,通过连接它们的根节点来实现。遍历所有边,将连接的顶点合并到同一个集合中,这样所有直接或间接连接的顶点最终都会在同一个集合内。最后,通过比较source和destination的根节点是否相同,可以判断它们是否连通,从而确定是否存在有效路径。
示例2
题目链接:1998. 数组的最大公因数排序 - 力扣(LeetCode)
总体思路如下:
- 首先,检查数组是否已经是有序的,如果是,直接返回True。
- 使用并查集(Union-Find)数据结构来管理和查找元素之间的连接关系。
- 对于数组中的每一对元素,如果它们的最大公因数(gcd)大于1,则将它们在并查集中合并,表示它们可以交换位置。
- 最后,检查通过并查集合并后的集合是否能够与排序后的数组匹配,如果可以,返回True,否则返回False。
from math import gcd
from typing import List
def canBeSorted(nums: List[int]) -> bool:
# 如果数组已经是有序的,直接返回True
if nums == sorted(nums):
return True
# 初始化并查集,每个元素的初始父节点是它自己
parent = list(range(len(nums)))
# 查找根节点,并进行路径压缩优化
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # 路径压缩
return parent[x]
# 合并两个集合
def union(x, y):
rootX = find(x)
rootY = find(y)
if rootX != rootY:
parent[rootY] = rootX # 将y的根节点指向x的根节点
# 构建并查集,将可以交换的元素放在同一个集合中
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if gcd(nums[i], nums[j]) > 1:
union(i, j) # 如果gcd大于1,合并i和j的集合
# 检查每个集合中的元素是否可以排序
sorted_nums = sorted(nums)
for i in range(len(nums)):
if find(i) != find(nums.index(sorted_nums[i])):
return False # 如果原始位置和排序后的位置不在同一个集合,返回False
return True # 所有元素都可以在同一个集合内排序,返回True
该代码利用并查集来处理数组元素之间的交换关系,通过检查元素是否可以交换位置来决定数组是否可以通过交换达到有序状态。并查集通过路径压缩和按秩合并优化,使得查找和合并操作更加高效。代码首先检查数组是否已经有序,然后通过遍历数组中的元素对,将可以交换的元素合并到同一个集合中。最后,通过比较原始数组和排序后数组的元素是否在同一个集合中,来判断是否可以通过交换达到有序状态。如果所有元素都可以在同一个集合内排序,则返回True,否则返回False。
这种方法之所以有效,是因为它基于以下两个关键概念:
最大公因数(GCD)与交换关系:
- 如果两个整数的最大公因数(GCD)大于1,这意味着它们有公共的质因数,可以认为它们在某种意义上是“相关联”的。在这个问题中,如果两个数有公共的质因数,它们可以通过一系列的交换操作相互到达对方的位置。
- 因此,可以将所有互相关联的数视为一个“连通块”,在这个连通块内的任何数都可以通过交换到达任何其他位置。
并查集(Union-Find)结构:
- 并查集是一种数据结构,用于处理一些不交集的合并及查询问题。它支持两种操作:查找(Find)和合并(Union)。
- 在这个问题中,并查集用于跟踪哪些数属于同一个连通块。如果两个数可以交换(即它们的GCD大于1),则将它们所在的集合合并。
- 通过这种方式,最终每个集合中的数都可以互相交换,而不与其他集合中的数交换。
以下是为什么这种方法有效的原因:
- 连通性保证:如果数组可以通过交换达到非递减顺序,那么每个数必须至少与一个其他数在同一个连通块中,这样它们才能通过一系列交换到达正确的位置。
- 集合匹配:通过并查集,我们可以确保每个数与其排序后位置上的数属于同一个集合。如果这一点对所有数都成立,那么通过集合内数的相互交换,我们可以将数组排序。
- 无序性检测:如果在任何位置上,原始数组的数与其排序后位置的数不属于同一个集合,那么就不可能通过交换将数组排序到非递减顺序。
综上所述,这种方法之所以有效,是因为它利用了数之间的关联性(通过GCD)和并查集来管理和验证这种关联性,从而确定数组是否可以通过交换达到有序状态。