图论:839. 相似字符串组(等价类问题—并查集)

1、题目链接

839. 相似字符串组

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<=300O(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;

原因在于,这里实现的并查集中,父亲一定指向自己,儿子一定不指向自己,一个父亲代表一个等价类。

  • 36
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yorelee.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值