Two strings `X` and `Y` are similar if we can swap two letters (in different positions) of `X`, so that it equals `Y`.
For example, "tars"
and "rats"
are similar (swapping at positions 0
and 2
), and "rats"
and "arts"
are similar, but "star"
is not similar to "tars"
, "rats"
, or "arts"
.
Together, these form two connected groups by similarity: {"tars", "rats", "arts"}
and {"star"}
. Notice that "tars"
and "arts"
are in the same group even though they are not similar. Formally, each group is such that a word is in the group if and only if it is similar to at least one other word in the group.
We are given a list A
of strings. Every string in A
is an anagram of every other string in A
. How many groups are there?
Example 1:
Input: ["tars","rats","arts","star"]
Output: 2
Note:
A.length <= 2000
A[i].length <= 1000
A.length * A[i].length <= 20000
- All words in
A
consist of lowercase letters only. - All words in
A
have the same length and are anagrams of each other. - The judging time limit has been increased for this question.
這道題定義了字符串之間的一種相似關系,說是對於字符串X和Y,交換X中兩個不同位置上的字符,若可以得到Y的話,就說明X和Y是相似的。現在給了我們一個字符串數組,要將相似的字符串放到一個群組里,這里同一個群組里的字符串不必任意兩個都相似,而是只要能通過某些結點最終連着就行了,有點像連通圖的感覺,將所有連通的結點算作一個群組,問整個數組可以分為多少個群組。由於這道題的本質就是求連通圖求群組個數,既然是圖,考察的就是遍歷啦,就有 DFS 和 BFS 的解法。先來看 DFS 的解法,雖說本質是圖的問題,但並不是真正的圖,沒有鄰接鏈表啥的,這里判斷兩個結點是否相連其實就是判斷是否相似。所以可以寫一個判斷是否相似的子函數,實現起來也非常的簡單,只要按位置對比字符,若不相等則 diff 自增1,若 diff 大於2了直接返回 false,因為只有 diff 正好等於2或者0的時候才相似。題目中說了字符串之間都是異構詞,說明字符的種類個數都一樣,只是順序不同,就不可能出現奇數的 diff,而兩個字符串完全相等時也是滿足要求的,是相似的。下面來進行 DFS 遍歷,用一個 HashSet 來記錄遍歷過的字符串,對於遍歷到的字符串,若已經在 HashSet 中存在了,直接跳過,否則結果 res 自增1,並調用遞歸函數。這里遞歸函數的作用是找出所有相似的字符串,首先還是判斷當前字符串 str 是否訪問過,是的話直接返回,否則加入 HashSet 中。然后再遍歷一遍原字符串數組,每一個遍歷到的字符串 word 都和 str 檢測是否相似,相似的話就對這個 word 調用遞歸函數,這樣就可以找出所有相似的字符串啦,參見代碼如下:
解法一:
class Solution {
public:
int numSimilarGroups(vector<string>& A) {
int res = 0, n = A.size();
unordered_set<string> visited;
for (string str : A) {
if (visited.count(str)) continue;
++res;
helper(A, str, visited);
}
return res;
}
void helper(vector<string>& A, string& str, unordered_set<string>& visited) {
if (visited.count(str)) return;
visited.insert(str);
for (string word : A) {
if (isSimilar(word, str)) {
helper(A, word, visited);
}
}
}
bool isSimilar(string& str1, string& str2) {
for (int i = 0, cnt = 0; i < str1.size(); ++i) {
if (str1[i] == str2[i]) continue;
if (++cnt > 2) return false;
}
return true;
}
};
我們也可以使用 BFS 遍歷來做,用一個 bool 型數組來標記訪問過的單詞,同時用隊列 queue 來輔助計算。遍歷所有的單詞,假如已經訪問過了,則直接跳過,否則就要標記為 true,然后結果 res 自增1,這里跟上面 DFS 的解法原理一樣,要一次找完和當前結點相連的所有結點,只不過這里用了迭代的 BFS 的寫法。先將當前字符串加入隊列 queue 中,然后進行 while 循環,取出隊首字符串,再遍歷一遍所有字符串,遇到訪問過的就跳過,然后統計每個字符串和隊首字符串之間的不同字符個數,假如最終 diff 為0的話,說明是一樣的,此時不加入隊列,但是要標記這個字符串為 true;若最終 diff 為2,說明是相似的,除了要標記字符串為 true,還要將其加入隊列進行下一輪查找,參見代碼如下:
解法二:
class Solution {
public:
int numSimilarGroups(vector<string>& A) {
int res = 0, n = A.size();
vector<bool> visited(n);
queue<string> q;
for (int i = 0; i < n; ++i) {
if (visited[i]) continue;
visited[i] = true;
++res;
q.push(A[i]);
while (!q.empty()) {
string t = q.front(); q.pop();
for (int j = 0; j < n; ++j) {
if (visited[j]) continue;
int diff = 0;
for (int k = 0; k < A[j].size(); ++k) {
if (t[k] == A[j][k]) continue;
if (++diff > 2) break;
}
if (diff == 0) visited[j] = true;
if (diff == 2) {
visited[j] = true;
q.push(A[j]);
}
}
}
}
return res;
}
};
對於這種群組歸類問題,很適合使用聯合查找 Union Find 來做,LeetCode 中也有其他用到這個思路的題目,比如 [Friend Circles](http://www.cnblogs.com/grandyang/p/6686983.html),[Accounts Merge](http://www.cnblogs.com/grandyang/p/7829169.html),[Redundant Connection II](http://www.cnblogs.com/grandyang/p/8445733.html),[Redundant Connection](http://www.cnblogs.com/grandyang/p/7628977.html),[Number of Islands II](http://www.cnblogs.com/grandyang/p/5190419.html),[Graph Valid Tree](http://www.cnblogs.com/grandyang/p/5257919.html),和 [Number of Connected Components in an Undirected Graph](http://www.cnblogs.com/grandyang/p/5166356.html)。都是要用一個 root 數組,每個點開始初始化為不同的值,如果兩個點屬於相同的組,就將其中一個點的 root 值賦值為另一個點的位置,這樣只要是相同組里的兩點,通過 getRoot 函數得到相同的值。所以這里對於每個結點 A[i],都遍歷前面所有結點 A[j],假如二者不相似,直接跳過;否則將 A[j] 結點的 root 值更新為i,這樣所有相連的結點的 root 值就相同了,一個群組中只有一個結點的 root 值會保留為其的初始值,所以最后只要統計到底還有多少個結點的 root 值還是初始值,就知道有多少個群組了,參見代碼如下:
解法三:
class Solution {
public:
int numSimilarGroups(vector<string>& A) {
int res = 0, n = A.size();
vector<int> root(n);
for (int i = 0; i < n; ++i) root[i] = i;
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (!isSimilar(A[i], A[j])) continue;
root[getRoot(root, j)] = i;
}
}
for (int i = 0; i < n; ++i) {
if (root[i] == i) ++res;
}
return res;
}
int getRoot(vector<int>& root, int i) {
return (root[i] == i) ? i : getRoot(root, root[i]);
}
bool isSimilar(string& str1, string& str2) {
for (int i = 0, cnt = 0; i < str1.size(); ++i) {
if (str1[i] == str2[i]) continue;
if (++cnt > 2) return false;
}
return true;
}
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/839
類似題目:
Number of Connected Components in an Undirected Graph
參考資料:
https://leetcode.com/problems/similar-string-groups/
https://leetcode.com/problems/similar-string-groups/discuss/200934/My-Union-Find-Java-Solution
[LeetCode All in One 題目講解匯總(持續更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)