嬰兒名字
題目[Interview-1707]:典型並查集題目。
解題思路
首先對 names
這種傻 X 字串結構進行預處理,轉換為一個 map
,key
是名字,val
是名字出現的次數。
然後是把 synonyms
轉換為並查集結構,需要注意的是:總是把字典序較小的名字作為連通分量的根。
最後以連通分量的根作為代表,計算每個連通分量的總權重(即每個名字的次數之和)。
程式碼實現
class Solution
{
public:
unordered_map<string, string> root;
vector<string> trulyMostPopular(vector<string> &names, vector<string> &synonyms)
{
vector<string> ans;
unordered_map<string, int> table;
for (auto &s : names)
{
int idx = s.find('(');
string n = s.substr(0, idx);
int val = stoi(s.substr(idx + 1, s.length() - 2 - idx));
table[n] = val;
}
// build the disjoint-union set
for (auto &str : synonyms)
{
int idx = str.find(',');
string n1 = str.substr(1, idx - 1);
string n2 = str.substr(idx + 1, str.length() - 2 - idx);
merge(n1, n2);
}
// calculate the frequency of root nodes
unordered_map<string, int> mapAns;
for (auto &p : table)
mapAns[find(p.first)] += p.second;
for (auto &p : mapAns)
{
string s = p.first + "(" + to_string(p.second) + ")";
ans.emplace_back(s);
}
return ans;
}
string find(string x)
{
return root.count(x) == 0 ? (x) : (root[x] = find(root[x]));
}
void merge(string x, string y)
{
x = find(x), y = find(y);
if (x < y)
root[y] = x;
else if (x > y)
root[x] = y;
// do nothing if x == y
}
};
冗餘連線 Ⅱ
題目[685]:?需要觀察出一點 trick .
解題思路
請先完成這篇文章中的 “冗餘連線” 。
看討論區的題解,本題需要分成 2 種情況討論(沒想到真做不出來?):
-
存在入度為 2 的點
即給出的示例 1 。由於本題的隱含條件是:去除一邊後所得的圖是樹(即每個點的入度均為 1 ),所以可以確定這種情況下有且只有一個入度為 2 的點(通過反證法易證)。
設入度為 2 的點為
t
,先在edges
中剔除以t
為終點的邊(有且僅有 2 個這樣的邊),建立並查集結構。設被剔除的 2 個邊先後分別為
e1
和e2
,那麼嘗試在並查集中加入e1
,如果加入e1
後依然無環,說明e1
屬於樹的邊,那麼返回e2
;如果加入e1
後有環,說明該剔除e1
,即返回e1
。(這麼做的原因是題目要求:如果有多個滿足則返回最後一個。) -
所有的點入度為 1
即給出的示例 2 。這種情況下,必然存在一個邊
e
使得圖中存在一個有向環,該邊e
即為所求。掃描edges
的每一個邊,同時建立並查集結構,如果某一邊使得find(x) == y
,說明有環,返回該邊即可。
程式碼實現
class Solution
{
public:
vector<int> root;
vector<int> findRedundantDirectedConnection(vector<vector<int>> &edges)
{
root.resize(edges.size() + 2, -1);
// get node whose indegree is 2
int t = checkIndegree(edges);
// if there is not such node in graph
if (t == -1)
{
for (auto &v : edges)
{
int x = v[0], y = v[1];
if (find(x) == y)
return v;
merge(x, y);
}
}
else
{
vector<vector<int>> candidate;
for (auto &v : edges)
{
if (v[1] == t)
candidate.emplace_back(v);
else
merge(v[0], v[1]);
}
assert(candidate.size() == 2);
auto &v = candidate[0];
int a = find(v[0]), b = find(v[1]);
if (find(a) != b)
return candidate[1];
return v;
}
// should not be here
return vector<int>();
}
int checkIndegree(vector<vector<int>> &egdes)
{
unordered_map<int, int> m;
for (auto &v : egdes)
m[v[1]]++;
for (auto &p : m)
if (p.second >= 2)
return p.first;
return -1;
}
int find(int x)
{
return root[x] == -1 ? (x) : (root[x] = find(root[x]));
}
void merge(int x, int y)
{
x = find(x), y = find(y);
if (x != y)
root[y] = x;
}
};
賬戶合併
題目[721]:一般難度題目?,有多種解法。
解題思路
- DFS 解法
建立一個 map
記錄每一個 email 對應的主人的 name ,方便後續的結果處理。
把所有的 email 建立一個圖(以鄰接表的形式)。每一個 account 的 email 都是相鄰的,對於以下的 account :
account1 = ["John", "e1", "e2", "e3"]
account2 = ["John", "e4", "e3", "e5", "e2"]
在圖 graph
中表現為:
_____________
| |
e1 -- e2 -- e3 -- e5
|
e4
其鄰接表為:
e1: [e2]
e2: [e1, e3, e5]
e3: [e2, e4, e5]
e4: [e3]
e5: [e3, e2]
最後,graph
圖中的每一個連通分量必然是屬於同一個人的,使用 DFS
或者 BFS
遍歷整個圖即可。
程式碼實現如下:
class Solution
{
public:
unordered_map<string, string> mailName;
unordered_map<string, vector<string>> graph;
unordered_set<string> visited; // dfs helper
vector<vector<string>> accountsMerge(vector<vector<string>> &accounts)
{
for (auto &v : accounts)
{
string &name = v[0];
int size = v.size();
// connect the emails
for (int i = 1; i < size - 1; i++)
{
mailName[v[i]] = name;
graph[v[i]].emplace_back(v[i + 1]);
graph[v[i + 1]].emplace_back(v[i]);
}
// if this account has only one email, put it into the graph, too
if (size == 2 && graph.find(v[1]) == graph.end())
graph[v[1]] = vector<string>();
// the last email
mailName[v[size - 1]] = name;
}
vector<vector<string>> ans;
for (auto &p : graph)
{
auto &now = p.first;
auto &list = p.second;
vector<string> data({mailName[now]});
if (visited.count(now) == 0)
{
dfs(data, now, list);
sort(data.begin() + 1, data.end());
ans.emplace_back(data);
}
}
return ans;
}
void dfs(vector<string> &data, const string &now, vector<string> &list)
{
visited.insert(now);
data.emplace_back(now);
for (auto &x : list)
{
if (visited.count(x) == 0)
{
dfs(data, x, graph[x]);
}
}
}
};
- 並查集解法
其實與上面差不多 ? . 只不過圖的結構換成並查集,查詢連通分量就可以不用 DFS 搜尋了。
對於每個 account 的 email 列表,把第一個作為連通分量的根,把從第二個開始及後面的所有 email ,都將它們與第一個合併。
class Solution
{
public:
unordered_map<string, string> root;
unordered_map<string, string> emailName;
vector<vector<string>> accountsMerge(vector<vector<string>> &accounts)
{
// 建立並查集(結果不是完全路徑壓縮的)
for (auto &v : accounts)
{
string &name = v.front();
int size = v.size();
emailName[v[1]] = name;
for (int i = 2; i < size; i++)
{
emailName[v[i]] = name;
merge(v[1], v[i]);
}
}
vector<vector<string>> ans;
unordered_map<string, vector<string>> table;
// 把同一個連通分量歸類在一起,key是該連通分量的根,val是連通分量的所有節點
for (auto &p : emailName)
{
auto email = p.first, name = p.second;
table[find(email)].emplace_back(email);
}
// 一個分量就是結果中的一個 account
for (auto &p : table)
{
auto v = p.second;
sort(v.begin(), v.end());
v.insert(v.begin(), emailName[p.first]);
ans.emplace_back(v);
}
return ans;
}
string find(const string &x)
{
return root.count(x) == 0 ? (x) : (root[x] = find(root[x]));
}
void merge(string x, string y)
{
x = find(x), y = find(y);
if (x != y)
root[y] = x;
}
};
相似字串組
題目[839]:?一道 Hard 題目,沒想到暴力解法也能過。
解題思路
每個字串相當於一個節點,以該串在陣列中的下標作為節點記號。
實現 similar
函式,判定 2 個字串是否相似。對所有字串進行兩兩比較(暴力列舉所有情況),判斷是否相似,若相似則在並查集中合併。最後連通分量的個數就是答案。
?執行用時只超過 25% (能用就行,能用就行,又不是不能用.jpg )。
程式碼實現
class Solution
{
public:
vector<int> root;
int numSimilarGroups(vector<string> &A)
{
int N = A.size();
root.resize(N, -1);
for (int i = 0; i < N; i++)
{
for (int j = i + 1; j < N; j++)
{
if (similar(A[i], A[j]))
merge(i, j);
}
}
return count(root.begin(), root.end(), -1);
}
int find(int x)
{
return root[x] == -1 ? (x) : (root[x] = find(root[x]));
}
void merge(int x, int y)
{
x = find(x), y = find(y);
if (x != y)
root[y] = x;
}
bool similar(const string &a, const string &b)
{
int len = min(a.length(), b.length());
int diff = 0;
for (int i = 0; i < len; i++)
diff += (a[i] != b[i]);
return diff <= 2;
}
};
交換字串中的元素
題目[1202]:?看題解,看題解。
解題思路
以 s = "dcabfge", pairs = [[0,3],[1,2],[0,2],[4,6]]
為例進行分析。
首先需要想到一點 trick ,怎麼套上並查集的模板:此處的「交換」是具有傳遞性和對稱性的,因此在 pairs
中,[0,1,2,3]
是可以兩兩進行任意交換(且不限次數),因此每一個 pair = [a,b]
實際上就是並查集中的一個邊,無腦對 pairs
套上並查集的結構進行處理。
此處,並查集的結果是得到 3 個連通分量 [5], [0,1,2,3]
和 [4,6]
,(按順序)對應可交換的字元是 [g][d,c,a,b]
和 [f,e]
,要求字典序最小,因此排序結果為 [g], [a,b,c,d]
和 [e,f]
。
將其還原到原始位置:
index: 5 | 0 1 2 3 | 4 6
chars: g | a b c d | e f
==>
return s = "abcdegf"
程式碼實現
- 使用交換排序,超時
class Solution
{
public:
vector<int> root;
string smallestStringWithSwaps(string s, vector<vector<int>> &pairs)
{
int len = s.length();
if (len == 0)
return s;
root.resize(len, -1);
for (auto &v : pairs)
merge(v[0], v[1]);
// extract each component nodes
// table[r] = [...]
// r is the root of connected component
// [...] includes all nodes of the component
unordered_map<int, vector<int>> table;
for (int i = 0; i < len; i++)
table[find(i)].emplace_back(i);
for (auto &p : table)
{
auto &list = p.second;
sortByValue(list, s);
}
return s;
}
void sortByValue(vector<int> &list, string &s)
{
int n = list.size();
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
if (s[list[j]] < s[list[i]])
swap(s[list[i]], s[list[j]]);
}
}
}
// function 'merge' and 'find' are omitted since the space is limited
};
- 使用 STL 自帶的排序 sort,但執行用時只超過 50%
class Solution
{
public:
vector<int> root;
string smallestStringWithSwaps(string s, vector<vector<int>> &pairs)
{
int len = s.length();
if (len == 0)
return s;
root.resize(len, -1);
for (auto &v : pairs)
merge(v[0], v[1]);
// extract each component nodes
unordered_map<int, vector<int>> table;
for (int i = 0; i < len; i++)
table[find(i)].emplace_back(i);
for (auto &p : table)
{
string chars = "";
for (int i : p.second)
chars.push_back(s[i]);
sort(chars.begin(), chars.end());
int j = 0;
for (int i : p.second)
s[i] = chars[j++];
}
return s;
}
// function 'merge' and 'find' are omitted since the space is limited
};
按公因數計算最大元件大小
題目[952]:?刷題?刷題解罷了。
- 暴力列舉(當然是超時了)
把 num 所在陣列 A
中的下標作為並查集中節點的標號。如果 GCD(A[i] A[j]) > 1
說明節點 i
和 j
可以合併。
class Solution
{
public:
vector<int> root;
vector<int> size;
int largestComponentSize(vector<int> &A)
{
int n = A.size();
root.resize(n, -1);
size.resize(n, 1);
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (GCD(A[i], A[j]) > 1)
merge(i, j);
int maxval = size[0];
for (int x : size) maxval = max(x, maxval);
return maxval;
}
// function 'find' is omitted
void merge(int x, int y)
{
x = find(x), y = find(y);
if (x != y) root[y] = x, size[x] += size[y];
}
// a>b is required
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
int GCD(int a, int b)
{
if (a > b) return gcd(a, b);
else return gcd(b, a);
}
};
此處,把 A[i]
的值作為並查集 root
節點的標號(不是下標作為標號)。
對於每一個 A[i]
找出它的所有大於 1 的因子,顯然 A[i]
與這些因子之間是可以合併的,預設把 A[i]
作為連通分量的根(大數值作為根)。那麼,並查集的結果就包含了任意兩個 A[i], A[j]
之間的關係(這裡的關係是指它們是否連通,即是否具有大於 1 的公因子)。為什麼呢?來看個例子。
A = [2, 3, 15, 10]
對於 2 和 3 ,不執行 merge 操作。
對於 15 :merge(15, 3), merge(15, 5)
對於 10 :merge(10, 2), merge(10, 5)
並查集最終結果,每一行表示一個連通分量,行首是連通分量的根:
2 : [2]
3 : [3]
15: [15,3,5,10,2]
顯然,10 和 15 就通過 merge(10,5)
和 merge(15, 5)
這 2 個操作合併到一塊。
最後的問題是如何找出 A
中最大的連通分量?注意,這裡並不是並查集 root
的最大連通分量,因為 root
對 A
來說是一個擴充後的結構,它包含了 A
中沒有的數值。
新建一個 vector<int> counter
,counter[r]
表示以 r
為根的連通分量中,陣列 A
中的元素在該分量中出現的次數。 即:
for (int x : A)
++counter[find(x)];
對於上述例子:
counter[2] = 1
counter[3] = 1
counter[15] = 2
最終,counter 的最大值即為答案。
程式碼如下:
class Solution
{
public:
vector<int> root;
int largestComponentSize(vector<int> &A)
{
int maxval = -1;
for (int x : A)
maxval = max(maxval, x);
root.resize(maxval + 1, -1);
for (int x : A)
{
int limit = (int)sqrt(x) + 1;
for (int i = 2; i < limit; i++)
if (x % i == 0)
merge(x, i), merge(x, x / i);
}
vector<int> counter(maxval + 1, 0);
int ans = -1;
for (int x : A)
ans = max(ans, ++counter[find(x)]);
return ans;
}
int find(int x) { return root[x] == -1 ? x : (root[x] = find(root[x])); }
void merge(int x, int y)
{
x = find(x), y = find(y);
if (x != y) root[y] = x;
}
};
總結
磕磕碰碰總算把「並查集」的題目刷了一遍,好像還有幾道題是沒做出來的(當然是「下次再努力」啊?)。