1、题目链接
2、题目描述
3、问题分析
只能想到
O
(
n
2
m
)
O(n^2m)
O(n2m)的方法,以为做不出来了,结果一看官解就是这个复杂度,蚌埠住了。也不是很理解为什么要使用并查集,但仔细分析可以发现确实是并查集!这个问题本质上就是找到strs
中有多少个等价类(这个等价类是通过相似关系维护的),而无论相似关系是什么,最快速的合并方法都是使用并查集解决。 其原因在于任何元素都可以映射到整数使用并查集,而且并查集可以最快速合并等价类。使用并查集的原因在于,考虑任何一个元素时,它都可能和已有的不同等价类相似,因此需要合并。
n < = 300 , m < = 300 , O ( n 2 m ) 一点问题没有 n <= 300, m <=300,O(n^2m)一点问题没有 n<=300,m<=300,O(n2m)一点问题没有
在图论:721. 账户合并(并查集扩展)中,我们使用一定要使用并查集,是因为这里面可能涉及到多个等价类合并的问题。如果使用unordered_map<string,unordered_set<string>>
的话,多个等价类合并的速度不高,而使用并查集等价类合并的速度是
O
(
1
)
O(1)
O(1)。
在本题中,直观感受是,从前往后遍历,对于每一个字符串strs[i]
,我们判断它和strs[0]~strs[i - 1]
是否相似,如果和它们某一个相似则将其加入到同一个等价类中。
我们可以想到使用unordered_map<string,unordered_set<string>> mp
,对于每一个新字符串strs[i]
,如果它和strs[j]
相似,就将其加入到strs[j]
的等价类中,再使用map
将其对应到该等价类,代码类似如下:
if(check(strs[i], strs[j])){
mp[strs[j]].insert(strs[i]);
mp[strs[i]] = mp[strs[j]];
}
如果对于所有的strs[0]~strs[i - 1]不存在相似,则它自己创建一个
if(!check(strs[i], strs[j])){
mp[strs[i]].insert(strs[i]);
}
但这个方法有点问题,如果strs[i]
和多个不同的等价类相似,则strs[i]
必须让多个等价类相连才行!
例如:
"
a
b
c
d
,
c
a
b
d
,
a
c
b
d
"
"abcd, cabd, acbd"
"abcd,cabd,acbd",其中
"
a
b
c
d
"
"abcd"
"abcd"和
"
c
a
b
d
"
"cabd"
"cabd"并不相似,分为两个等价类,但
"
a
c
b
d
"
"acbd"
"acbd"和它们两个都相似,因此它们三个相似,应当将其都合并,使用unordered_map<string,unordered_set<string>> mp
时间复杂度较高。因此需要使用并查集。
4、扩展思考
使用并查集,可以快速合并多个等价类(不管这个等价类是什么形式,我们最终都可以映射到整数上,使用并查集);也可以快速找到一个元素对应的等价类。
何时可以考虑并查集? 如果一个问题存在等价关系时,我们就可以使用,大概率需要使用。因为输入可能将本身是等价类的几个分散成了多个,我们后续需要进行合并。一个元素进行一次遍历可以保证它加入到正确等价类,但不可能保证它在当前的多个等价类中需要合并,使用并查集可以解决这个问题。
5、代码
由于strs
中每个元素都不一样,我们使用其下标当做等价类的标号即可。
判断两个字符串是否相似很容易判断,难点在于如何抽象出这个问题的本质。
并且并查集不要误入一个误区,用并查集思路去思考问题,直接将并查集当作维护等价类和合并等价类的数据结构就行,抽象地思考。
class UnionFind{
public:
UnionFind(int n):size(n){
for(int i = 0; i < n; ++ i){
parent.emplace_back(i);
rank.emplace_back(1);
}
}
int Find(int x){
return parent[x] == x? x : (parent[x] = Find(parent[x]));
}
void unite(int x, int y){
int fax = Find(x);
int fay = Find(y);
if(fax == fay) return;
if(rank[fax] > rank[fay]){
parent[fay] = fax;
rank[fax] ++;
}else{
parent[fax] = fay;
rank[fay] ++;
}
-- size;
return;
}
int size;
private:
vector<int> parent;
vector<int> rank;
};
bool check(string & s1,string & s2){
int cnt = 0;
char c1 = '\n', c2 = '\n';
for(int i = 0; i < s1.size(); ++ i){
if(s1[i] != s2[i]){
cnt ++;
if(cnt == 3) return false;
if(c1 == '\n'){//第一次
c1 = s1[i], c2 = s2[i];
}else{//第二次
if(!(s2[i] == c1 && s1[i] == c2)) return false;
}
}
}
return true;
}
class Solution {
public:
int numSimilarGroups(vector<string>& strs) {
int n = strs.size();
UnionFind uf(n);
for(int i = 1; i < n; ++ i){
for(int j = 0; j < i; ++ j){
if(check(strs[i], strs[j])){
uf.unite(i, j);//瞧瞧并查集处理等价类的速度hh
}
}
}
return uf.size;
}
};
check
函数还能进一步优化:
bool check(string & s1,string & s2){
int cnt = 0;
for(int i = 0; i < s1.size(); ++ i){
if(s1[i] != s2[i]){
cnt ++;
if(cnt == 3) return false;
}
}
return true;
}
原因在于,这些词都是字母异位词。只要不相同的个数为2,则必然它们相似。
打个比方就是,有n
个相同的人按两种顺序站成一排,这两种顺序中只有两个位置不一样,那么这两个顺序一定是只在这两个位置上交换了人。
找有多少个等价类还能这样找:
for(int i = 0; i < n; ++ i){
if(uf.Find(i) == i) ret ++; //或 if(uf.parent[i] == i) ret ++;
}
return ret;
原因在于,这里实现的并查集中,父亲一定指向自己,儿子一定不指向自己,一个父亲代表一个等价类。