並查集 Union Find 算法
定義
並查集(Disjoint-Set)是一種可以動態維護若干個不重疊的集合,並支持合併
與查詢
兩種操作的一種數據結構
。
基本操作
- 合併(Union):合併兩個集合。
- 查詢(Find):查詢元素所屬集合。
實際操作時,我們會使用一個點來代表整個集合,即一個元素的根結點(可以理解爲父親)。
具體實現
我們建立一個數組father_dict
字典表示一個並查集,father_dict[i]
表示i的父節點。並且設置一個size_dict
字典,size_dict[i]
表示i
的後代節點的個數,包括其本身。
初始化
:每一個點都是一個集合,因此自己的父節點就是自己father_dict[i]=i
,size_dict[i]=1
.查詢
:每一個節點不斷尋找自己的父節點,若此時自己的父節點就是自己,那麼該點爲集合的根結點,返回該點。合併
:合併兩個集合只需要合併兩個集合的根結點,size_dict大喫小,爲了儘可能的降低樹高。
路徑壓縮:實際上,我們在查詢過程中只關心根結點是什麼,並不關心這棵樹的形態(有一些題除外)。因此我們可以在查詢操作的時候將訪問過的每個點都指向樹根,這樣的方法叫做路徑壓縮,單次操作複雜度爲O(logN)
。
路徑壓縮示例:經過一次FIND節點H,後數據結構的變化。
代碼實現
class UnionFindSet(object):
"""並查集"""
def __init__(self, data_list):
"""初始化兩個字典,一個保存節點的父節點,另外一個保存父節點的大小
初始化的時候,將節點的父節點設爲自身,size設爲1"""
self.father_dict = {}
self.size_dict = {}
for node in data_list:
self.father_dict[node] = node
self.size_dict[node] = 1
def find(self, node):
"""使用遞歸的方式來查找父節點
在查找父節點的時候,順便把當前節點移動到父節點上面
這個操作算是一個優化
"""
father = self.father_dict[node]
if(node != father):
if father != self.father_dict[father]: # 在降低樹高優化時,確保父節點大小字典正確
self.size_dict[father] -= 1
father = self.find(father)
self.father_dict[node] = father
return father
def is_same_set(self, node_a, node_b):
"""查看兩個節點是不是在一個集合裏面"""
return self.find(node_a) == self.find(node_b)
def union(self, node_a, node_b):
"""將兩個集合合併在一起"""
if node_a is None or node_b is None:
return
a_head = self.find(node_a)
b_head = self.find(node_b)
if(a_head != b_head):
a_set_size = self.size_dict[a_head]
b_set_size = self.size_dict[b_head]
if(a_set_size >= b_set_size):
self.father_dict[b_head] = a_head
self.size_dict[a_head] = a_set_size + b_set_size
else:
self.father_dict[a_head] = b_head
self.size_dict[b_head] = a_set_size + b_set_size
應用
朋友圈問題
假如已知有n個人和m對好友關係(存於數組r)如果兩個人是直接或間接的好友(好友的好友的好友…),則認爲他們屬於同一個朋友圈,請寫程序求出這n個人裏一共有多少個朋友圈。假如:n = 5,m = 3,r = {{1 , 2} , {2 , 3} , {4 , 5}}表示有5個人,1和2是好友,2和3是好友4和5是好友,則1、2、3屬於一個朋友圈4、5屬於另一個朋友圈,結果爲2個朋友圈。
輸入:
1
10
0 1
2 3
4 6
2 5
5 4
1 6
10 11
7 9
8 10
7 11
輸出:
2
N = int(input())
for _ in range(N):
M = int(input())
nums = []
maxNum = 0
for _ in range(M):
tempTwoNum = list(map(int, input().split()))
maxNum = max(maxNum, max(tempTwoNum))
nums.append(tempTwoNum)
union_find_set = UnionFindSet(list(range(maxNum+1)))
for i in range(M):
union_find_set.union(nums[i][0], nums[i][1])
res_dict = {}
for i in union_find_set.father_dict:
rootNode = union_find_set.find(i)
if rootNode in res_dict:
res_dict[rootNode].append(i)
else:
res_dict[rootNode] = [i]
print(res_dict)
print('朋友圈個數:', len(res_dict.keys()))
親戚
若某個家族人員過於龐大,要判斷兩個是否是親戚,確實還很不容易,現在給出某個親戚關係圖,求任意給出的兩個人是否具有親戚關係。原題鏈接。
class UnionFindSet(object):
"""並查集"""
def __init__(self, data_list):
"""初始化兩個字典,一個保存節點的父節點,另外一個保存父節點的大小
初始化的時候,將節點的父節點設爲自身,size設爲1"""
self.father_dict = {}
self.size_dict = {}
for node in data_list:
self.father_dict[node] = node
self.size_dict[node] = 1
def find(self, node):
"""使用遞歸的方式來查找父節點
在查找父節點的時候,順便把當前節點移動到父節點上面
這個操作算是一個優化
"""
father = self.father_dict[node]
if(node != father):
if father != self.father_dict[father]: # 在降低樹高優化時,確保父節點大小字典正確
self.size_dict[father] -= 1
father = self.find(father)
self.father_dict[node] = father
return father
def is_same_set(self, node_a, node_b):
"""查看兩個節點是不是在一個集合裏面"""
return self.find(node_a) == self.find(node_b)
def union(self, node_a, node_b):
"""將兩個集合合併在一起"""
if node_a is None or node_b is None:
return
a_head = self.find(node_a)
b_head = self.find(node_b)
if(a_head != b_head):
a_set_size = self.size_dict[a_head]
b_set_size = self.size_dict[b_head]
if(a_set_size >= b_set_size):
self.father_dict[b_head] = a_head
self.size_dict[a_head] = a_set_size + b_set_size
else:
self.father_dict[a_head] = b_head
self.size_dict[b_head] = a_set_size + b_set_size
N,M,R = map(int, input().split())
uf = UnionFindSet(list(range(1, N+1)))
for _ in range(M):
num1, num2 = map(int, input().split())
uf.union(num1, num2)
for _ in range(R):
num1, num2 = map(int, input().split())
if uf.is_same_set(num1, num2):
print('Yes')
else:
print('No')
最長連續序列
leetcode 原題鏈接:https://leetcode-cn.com/problems/longest-consecutive-sequence/
給定一個未排序的整數數組,找出最長連續序列的長度。
要求算法的時間複雜度爲 O(n)。
示例:
輸入: [100, 4, 200, 1, 3, 2]
輸出: 4
解釋: 最長連續序列是 [1, 2, 3, 4]。它的長度爲 4。
思路:利用哈希字典,從小往大的FIND,與並查集查找異曲同工,時間複雜度O(1).
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
pre_dict = {}
for i in nums:
pre_dict[i] = 1
res = 0
for i in pre_dict:
if i - 1 not in pre_dict:
y = i + 1
while y in pre_dict:
y += 1
res = max(res, y - i)
return res
被圍繞的區域
給定一個二維的矩陣,包含 ‘X’ 和 ‘O’(字母 O)。
找到所有被 ‘X’ 圍繞的區域,並將這些區域裏所有的 ‘O’ 用 ‘X’ 填充。
示例:
X X X X
X O O X
X X O X
X O X X
運行你的函數後,矩陣變爲:
X X X X
X X X X
X X X X
X O X X
解釋
:
被圍繞的區間不會存在於邊界上,換句話說,任何邊界上的 ‘O’ 都不會被填充爲 ‘X’。 任何不在邊界上,或不與邊界上的 ‘O’ 相連的 ‘O’ 最終都會被填充爲 ‘X’。如果兩個元素在水平或垂直方向相鄰,則稱它們是“相連”的。
class UnionFindSet(object):
"""並查集"""
def __init__(self, data_list):
"""初始化兩個字典,一個保存節點的父節點,另外一個保存父節點的大小
初始化的時候,將節點的父節點設爲自身,size設爲1"""
self.father_dict = {}
self.size_dict = {}
for node in data_list:
self.father_dict[node] = node
self.size_dict[node] = 1
def find(self, node):
"""使用遞歸的方式來查找父節點
在查找父節點的時候,順便把當前節點移動到父節點上面
這個操作算是一個優化
"""
father = self.father_dict[node]
if(node != father):
if father != self.father_dict[father]: # 在降低樹高優化時,確保父節點大小字典正確
self.size_dict[father] -= 1
father = self.find(father)
self.father_dict[node] = father
return father
def is_same_set(self, node_a, node_b):
"""查看兩個節點是不是在一個集合裏面"""
return self.find(node_a) == self.find(node_b)
def union(self, node_a, node_b):
"""將兩個集合合併在一起"""
if node_a is None or node_b is None:
return
a_head = self.find(node_a)
b_head = self.find(node_b)
if(a_head != b_head):
a_set_size = self.size_dict[a_head]
b_set_size = self.size_dict[b_head]
if(a_set_size >= b_set_size):
self.father_dict[b_head] = a_head
self.size_dict[a_head] = a_set_size + b_set_size
else:
self.father_dict[a_head] = b_head
self.size_dict[b_head] = a_set_size + b_set_size
class Solution:
def solve(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
if not board:
return
m, n = len(board), len(board[0]) # row col
ufs = UnionFindSet(list(range(m*n+1)))
for i in range(m):
for j in range(n):
if board[i][j] == 'O':
if i==0 or i==m-1 or j==0 or j==n-1:
ufs.union(m*n, i*n+j)
else:
if board[i+1][j] == 'O':
ufs.union((i+1) * n + j, i*n+j)
if board[i-1][j] == 'O':
ufs.union((i-1) * n + j, i*n+j)
if board[i][j-1] == 'O':
ufs.union(i * n + (j-1), i*n+j)
if board[i][j+1] == 'O':
ufs.union(i * n + (j+1), i*n+j)
for i in range(m):
for j in range(n):
rootEdage = ufs.find(m*n)
if ufs.find(i*n+j) != rootEdage and board[i][j] == 'O':
board[i][j] = 'X'