簡介
在計算機科學中,並查集是一種樹型的數據結構,用於處理一些不交集(Disjoint Sets)的合併及查詢問題。有一個聯合-查找算法(Union-find Algorithm)定義了兩個用於此數據結構的操作:
Find:確定元素屬於哪一個子集。它可以被用來確定兩個元素是否屬於同一子集。
Union:將兩個子集合併成同一個集合。
由於支持這兩種操作,一個不相交集也常被稱爲聯合-查找數據結構(Union-find Data Structure)或合併-查找集合(Merge-find Set)。
爲了更加精確的定義這些方法,需要定義如何表示集合。一種常用的策略是爲每個集合選定一個固定的元素,稱爲代表,以表示整個集合。接着,Find(x)Find(x) 返回 xx 所屬集合的代表,而 Union 使用兩個集合的代表作爲參數。
f = {}
#print(name_age)
def find(x):
f.setdefault(x, x)
if f[x] != x:
f[x] = find(f[x])
return f[x]
def union(x, y):
f[find(x)] = find(y)
下面來介紹幾個使用並查集的典型例題
1.DFS or BFS 的另一種選擇
給定一個二維的矩陣,包含 '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 連通點找到, 把這些變成 B,然後遍歷整個 board 把 O 變成 X, 把 B 變成 O。可以很簡單得用DFS或者BFS實現,這就不放代碼了,也可以用並查集 把邊界 O 並且與它連通這些點分在一起。
class Solution:
def solve(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
f = {}
def find(x):
f.setdefault(x, x)
if f[x] != x:
f[x] = find(f[x])
return f[x]
def union(x, y):
f[find(y)] = find(x)
if not board or not board[0]:
return
row = len(board)
col = len(board[0])
dummy = row * col
for i in range(row):
for j in range(col):
if board[i][j] == "O":
#將所有邊界的0y與自定義的dummy連接在一起 使dummy作爲“首領”
if i == 0 or i == row - 1 or j == 0 or j == col - 1:
union(i * col + j, dummy)
#print(i * col + j, dummy)
else:
#將所有不與邊界相臨的連接在一起
for x, y in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
if board[i + x][j + y] == "O":
union(i * col + j, (i + x) * col + (j + y))
#print(f)
tmp = find(dummy)
for i in range(row):
for j in range(col):
#print(find(i * col + j))
# 對每一個點 在並查集裏找它的首領 沒有被遍歷到的X的首領是自己
# 如果遍歷出來首領是dummy 則不變 否則變爲X
if tmp == find(i * col + j):
board[i][j] = "O"
else:
board[i][j] = "X"
2 利用並查集進行合併排序整理
給你一個字符串 s,以及該字符串中的一些「索引對」數組 pairs,其中 pairs[i] = [a, b] 表示字符串中的兩個索引(編號從 0 開始)。
你可以 任意多次交換 在 pairs 中任意一對索引處的字符。
返回在經過若干次交換後,s 可以變成的按字典序最小的字符串。
示例 1:
輸入:s = “dcab”, pairs = [[0,3],[1,2]]
輸出:“bacd”
解釋:
交換 s[0] 和 s[3], s = “bcad”
交換 s[1] 和 s[2], s = “bacd”
思路:由於我們有pairs數組 可以很方便的求出那些元素是在一起的 可以任意調換位置的
class Solution:
def smallestStringWithSwaps(self, s: str, pairs: [int]) -> str:
f = {} #初始化並查集
def find(x):
f.setdefault(x, x)
if x != f[x]:
f[x] = find(f[x])
return f[x]
def union(i,j):
f[find(i)] = find(j)
# 將可以交換的元素位置聯通
for i, j in pairs:
union(i,j)
#合併可交換位置
d = collections.defaultdict(list)
# 對每一個元素 將它加入它的首領的字典裏面
for i, j in f.items():
#print(i,j)
d[find(i)].append(i)
#排序
ans = list(s)
for q in d.values():
# 將每個集合分別排序 找到這個集合裏面字典序最小的
t = sorted(ans[i] for i in q)
#print(t)
# 按照原有的位置插入
for i, c in zip(sorted(q), t):
ans[i] = c
return ''.join(ans)
3 並查集每個元素的性質意義
用以太網線纜將 n 臺計算機連接成一個網絡,計算機的編號從 0 到 n-1。線纜用 connections 表示,其中 connections[i] = [a, b] 連接了計算機 a 和 b。
網絡中的任何一臺計算機都可以通過網絡直接或者間接訪問同一個網絡中其他任意一臺計算機。
給你這個計算機網絡的初始佈線 connections,你可以拔開任意兩臺直連計算機之間的線纜,並用它連接一對未直連的計算機。請你計算並返回使所有計算機都連通所需的最少操作次數。如果不可能,則返回 -1 。
示例 1:
輸入:n = 4, connections = [[0,1],[0,2],[1,2]]
輸出:1
解釋:拔下計算機 1 和 2 之間的線纜,並將它插到計算機 1 和 3 上。
思路:首先可以看出 如果connections的數目小於n-1 那麼肯定是不行的 之後可以找到有多少個是已經連接了的m以及有多少個首領p,那麼最後需要操作的次數爲n-m+p-1 也可以對0—n求所有的首領 最後不同首領的個數-1
class Solution:
def makeConnected(self, n: int, connections: List[List[int]]) -> int:
f = {}
if len(connections) < n - 1:
return -1
def find(x):
f.setdefault(x, x)
if f[x] != x:
f[x] = find(f[x])
return f[x]
def union(x, y):
f[find(x)] = find(y)
for a, b in connections:
union(a,b)
#print(f,len(f))
res = []
for j in f:
res.append(find(j))
#print(len({*map(find, f)}))
return n-len(f)+len({*map(find, f)})-1
4 面試題 求字符數組的分類
每年,政府都會公佈一萬個最常見的嬰兒名字和它們出現的頻率,也就是同名嬰兒的數量。有些名字有多種拼法,例如,John 和 Jon 本質上是相同的名字,但被當成了兩個名字公佈出來。給定兩個列表,一個是名字及對應的頻率,另一個是本質相同的名字對。設計一個算法打印出每個真實名字的實際頻率。注意,如果 John 和 Jon 是相同的,並且 Jon 和 Johnny 相同,則 John 與 Johnny 也相同,即它們有傳遞和對稱性。
在結果列表中,選擇字典序最小的名字作爲真實名字。
示例:
輸入:names = [“John(15)”,“Jon(12)”,“Chris(13)”,“Kris(4)”,“Christopher(19)”], synonyms = ["(Jon,John)","(John,Johnny)","(Chris,Kris)","(Chris,Christopher)"]
輸出:[“John(27)”,“Chris(36)”]
看起來過程很繁瑣 其實只要理清思路
1.首先分割synonyms數組字符串找出所有人名 將他們聯通起來 注意如果有人名沒有在names出現過 可以不統計
2 分割namse字符串 用一個字典統計所有人名以及他們的次數
3 對names裏面的每一個名字 將他們加入他們首領的集合
4.對每一個集合 先進行排序 保證字典序最小的名字在第一個 之後統計他們在字典中的次數和
5 組合起來加入答案數組
class Solution:
def trulyMostPopular(self, names: List[str], synonyms: List[str]) -> List[str]:
name_age = collections.defaultdict()
for str1 in names:
temp = str1.split('(')
name_age[temp[0]]=int(temp[1][0:-1])
f = {}
#print(name_age)
def find(x):
f.setdefault(x, x)
if f[x] != x:
f[x] = find(f[x])
return f[x]
def union(x, y):
f[find(x)] = find(y)
for str2 in synonyms:
temp2 = str2.split(',')
name1 = temp2[0][1:]
name2 = temp2[1][0:-1]
if name1 not in name_age or name2 not in name_age:
continue
union(name1,name2)
same_name = collections.defaultdict(list)
for j in name_age:
same_name[find(j)].append(j)
ans = []
for name_list in same_name.values():
name_list.sort()
sum1 = 0
for j in name_list:
sum1 +=name_age[j]
ans.append(name_list[0]+'('+str(sum1)+')')
return ans
並查集的題目大多都有一個相似的前提 就是有一個臨結矩陣 或者數組 可以初始化並查集
此外比較有意思的題目還有947. 移除最多的同行或同列石頭 由於並查集是1維的 在處理二維座標數據時往往對其進行編號 如第一題 或者將列乘以一個很大的數。