并查集算法之Erlang篇

今天刷到力扣721的账户合并题,被Erlang整的舒服的不行,把Erlang实现的并查集算法贴一下

题目

原题在[LeetCode721.账户合并](721. 账户合并 - 力扣(LeetCode) (leetcode-cn.com))

给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是 名称 (name),其余元素是 emails 表示该账户的邮箱地址。

现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。

合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是 按字符 ASCII 顺序排列 的邮箱地址。账户本身可以以 任意顺序 返回。

实例1
输入:accounts = [["John", "johnsmith@mail.com", "john00@mail.com"], ["John", "johnnybravo@mail.com"], ["John", "johnsmith@mail.com", "john_newyork@mail.com"], ["Mary", "mary@mail.com"]]
输出:[["John", 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com'],  ["John", "johnnybravo@mail.com"], ["Mary", "mary@mail.com"]]
解释:
第一个和第三个 John 是同一个人,因为他们有共同的邮箱地址 "johnsmith@mail.com"。 
第二个 JohnMary 是不同的人,因为他们的邮箱地址没有被其他帐户使用。
可以以任何顺序返回这些列表,例如答案 [['Mary''mary@mail.com']['John''johnnybravo@mail.com']['John''john00@mail.com''john_newyork@mail.com''johnsmith@mail.com']] 也是正确的。

简单分析一下,现在有很多个账户,每个账户由一个数组构成,且账户第一个元素为账户名,后面为邮箱名,账户名可以重复,但邮箱名不能重复(一旦重复则视为同一个人),我们目的就是把这些相同账户的人合并起来,且题目告诉我们相同邮箱的人的名字一定相同

思路1

我们可以把整个表遍历,开一个空间来存储这些数据,存储的规则是将名字作为键,所有邮箱作为值,如果相同账户不同邮箱则为两个值

如:[["John", "johnsmith@mail.com", "john00@mail.com"], ["John", "johnnybravo@mail.com"], ["John", "johnsmith@mail.com", "john_newyork@mail.com"], ["Mary", "mary@mail.com"]]
前三项我们都放在John为键的数对中,如下
[{"John", [["johnsmith@mail.com", "john00@mail.com"],["johnnybravo@mail.com"],["johnsmith@mail.com", "john_newyork@mail.com"]]}]
这只是第一步,且在每一步中我们把匹配到的待插入项去遍历对应键的值,看看有没有相同的,如果有则直接合并,合并完作为一个新项插入,(肉眼可见的复杂度,试试看嘛,慢慢优化)

所以就有了以下算法

对应代码

accounts_merge(Accounts) ->
    All = do(Accounts, []),
    do_print(All, []).

%% 导出答案格式化的方法,将键和值(名字和邮箱拼在一起打印)
do_print([], R) -> R;
do_print([{H, V} | T], R) ->
    case length(V) of
        1 ->
            [VV] = V,
            do_print(T, R ++ [[H] ++ VV]);
        _ ->
            do_print(T, uncover(H, V, R))
    end.

%% 多值拆包
uncover(_, [], R) -> R;
uncover(Name, [H | T], R) ->
    uncover(Name, T, R ++ [[Name] ++ H]).

%% 遍历整个邮箱对
do([], V) -> V;
do([H | T], All) ->
    [Name | Adds] = H,
    case lists:keyfind(Name, 1, All) of
        {_Name, V} ->
            NV = check(Adds, V, []),
            NAll = lists:keyreplace(Name, 1, All, {Name, NV}),
            do(T, NAll);
        _ ->
            do(T, All ++ [{Name, [lists:sort(remove_duplicate(Adds))]}])
    end.

%% 检查需要合并的账户,注意每一步都要去重且排序
check(Add, [], V) -> V ++ [lists:sort(remove_duplicate(Add))];
check(Add, [H | T], V) ->
    L = [X || X <- H, Y <- Add, X == Y],
    case length(L) == 0 of
        true -> check(Add, T, V ++ [lists:sort(remove_duplicate(H))]);
        _ ->
%%            V ++ [lists:sort(remove_duplicate(Add ++ H))] ++ T
            MergeV = lists:sort(remove_duplicate(Add ++ H)),
            check(MergeV, V ++ T, [])
    end.

%% list去重
remove_duplicate([]) -> [];
remove_duplicate(L = [_ | _]) -> sets:to_list(sets:from_list(L)).
提交测试

还行,凹过了47个例子,看来已经没有优化的空间,

image-20211203143358493

我们的合并逻辑复杂度那是肉眼可见(n平方石锤),但是为了应对David这种例子又不得不一直重复遍历

[["David", "David0@m.co", "David1@m.co"],
            ["David", "David3@m.co", "David4@m.co"],
            ["David", "David4@m.co", "David5@m.co"],
            ["David", "David2@m.co", "David3@m.co"],
            ["David", "David1@m.co", "David2@m.co"]]

既然优化不了那我们只能尝试用官方推荐的并查集算法了,用Erlang改良一下

思路2

要把相同邮箱的账户合并,首先要找到相同的用户,但又不能根据名字来(重名),所以只能用邮箱来标记是否为同一个人,思考后思路如下,我们先遍历所有账户,把每一个邮箱自身作为一个键,且对应一个值,这个值指向同一堆的父节点(同一个账户下的所有邮箱为同一个节点),然后遍历所有树,如果有相同父节点的两堆数据我们直接合并他,这就是并查集的思想

并查集实现1

先用list实现,对应的就是[{key,value}],其中key作为index的作用

%% 首先初始化一个并查集,且每个元素都是自己的根节点
union_init(N) ->
    L = lists:seq(1, N),
    [{X, X} || X <- L].

%% 查找接口,如果查找的节点对应值为自己则表示自己就是根节点,如果不是,继续找他对应的节点的根节点,也就是找归属地环节
union_find(Index, Parents) ->
    case lists:keyfind(Index, 1, Parents) of
        {Index, Index} -> Index;
        {_, Value} -> union_find(Value, Parents)
    end.

%% 合并两颗树,将一棵树对应的父节点赋给另一颗树,即将两棵树标记为一棵树
union(Index1, Index2, Parents) ->
    Parent1 = union_find(Index1, Parents),
    Parent2 = union_find(Index2, Parents),
    lists:keyreplace(Parent2, 1, Parents, {Parent2, Parent1}).
主要逻辑实现
accounts_merge(Accounts) ->
    {EmailsCount, EmailToIndexMap} = build_emailToIndex_Map(Accounts, 1, maps:new()),
    EmailToNameMap = build_emailToName_Map(Accounts, maps:new()),
    %% 根据上面统计不相同邮箱的数量构建一个并查集
    Parents = union_init(EmailsCount),
    %% 合并相同树的操作
    NewParents = union_email(Parents, Accounts, EmailToIndexMap),
    %% 找到邮箱对应的索引,这里已经是树的索引了,同一颗树下的邮箱会处理到同一个索引下
    IndexToEmailsMap =
        build_indexToEmail_Map(maps:keys(EmailToIndexMap), NewParents, maps:new(), EmailToIndexMap),
    build_res(maps:values(IndexToEmailsMap), EmailToNameMap, []).

%% 构造最后的答案格式输出,即名字+邮箱组合
build_res([], _EmailToNameMap, Res) -> Res;
build_res([H | T], EmailToNameMap, Res) ->
    Emails = lists:sort(H),
    [Email | _TName] = H,
    Name = maps:get(Email, EmailToNameMap),
    build_res(T, EmailToNameMap, [[Name | Emails] | Res]).

%% 构造邮箱的索引Map,即 #{1 -> 邮箱1}
build_indexToEmail_Map([], _Parents, IndexToEmailsMap, _EmailToIndexMap) -> IndexToEmailsMap;
build_indexToEmail_Map([H | T], Parents, IndexToEmailsMap, EmailToIndexMap) ->
    Index = union_find(maps:get(H, EmailToIndexMap), Parents),
    case maps:is_key(Index, IndexToEmailsMap) of
        true ->
            EmailList = maps:get(Index, IndexToEmailsMap),
            NewMap = maps:put(Index, [H | EmailList], IndexToEmailsMap),
            build_indexToEmail_Map(T, Parents, NewMap, EmailToIndexMap);
        _ ->
            NewMap = maps:put(Index, [H], IndexToEmailsMap),
            build_indexToEmail_Map(T, Parents, NewMap, EmailToIndexMap)
    end.

%% 遍历所有邮箱,相同账户下的邮箱作为一棵树,对应父节点为每个账户的第一个邮箱
union_email(Parents, [], _) -> Parents;
union_email(Parents, [[_Name | Emails] | T], EmailToIndexMap) ->
    NewParents = do_union_email(Parents, Emails, EmailToIndexMap),
    union_email(NewParents, T, EmailToIndexMap).
do_union_email(Parents, [], _EmailToIndexMap) -> Parents;
do_union_email(Parents, [First | T], EmailToIndexMap) ->
    FirstIndex = maps:get(First, EmailToIndexMap),
    do_union_email(Parents, FirstIndex, T, EmailToIndexMap).
do_union_email(Parents, _FirstIndex, [], _EmailToIndexMap) -> Parents;
do_union_email(Parents, FirstIndex, [H | T], EmailToIndexMap) ->
    NextIndex = maps:get(H, EmailToIndexMap),
    NewParents = union(FirstIndex, NextIndex, Parents),
    do_union_email(NewParents, FirstIndex, T, EmailToIndexMap).

%% 构造邮箱-索引Map 即 #{邮箱1 -> 1},且计算所有邮箱的数量,注意相同邮箱不计算
build_emailToIndex_Map([], Count, Map) -> {Count, Map};
build_emailToIndex_Map([[_Name | Emails] | T], Count, Map) ->
    {Count1, DoMap} = do_build_emailToIndex_Map(Emails, Count, Map),
    build_emailToIndex_Map(T, Count1, DoMap).
do_build_emailToIndex_Map([], Count, Map) -> {Count, Map};
do_build_emailToIndex_Map([H | T], Count, Map) ->
    case maps:is_key(H, Map) of
        true -> do_build_emailToIndex_Map(T, Count, Map);
        _ -> do_build_emailToIndex_Map(T, Count + 1, maps:put(H, Count, Map))
    end.

%% 构造邮箱-账户名 Map 即 #{邮箱1 -> John}
build_emailToName_Map([], Map) -> Map;
build_emailToName_Map([[Name | Emails] | T], Map) ->
    Map1 = do_build_emailToName_Map(Emails, Name, Map),
    build_emailToName_Map(T, Map1).
do_build_emailToName_Map([], _Name, Map) -> Map;
do_build_emailToName_Map([Email | T], Name, Map) ->
    case maps:is_key(Email, Map) of
        true -> do_build_emailToName_Map(T, Name, Map);
        _ -> do_build_emailToName_Map(T, Name, maps:put(Email, Name, Map))
    end.
提交测试

image-20211203145657840

?怎么比我暴力还慢,左思右想,参考了上次最小岛屿问题,发现是傻逼list性能过差,我们把list对应kv结构优化一下,改成map实现

并查集实现2
union_init(N) -> union_init(N, maps:new()).
union_init(0, Map) -> Map;
union_init(N, Map) ->
    union_init(N - 1, maps:put(N, N, Map)).

union_find(Index, Parents) ->
    case maps:get(Index, Parents) of
        Index -> Index;
        V -> union_find(V, Parents)
    end.

union(Index1, Index2, Parents) ->
    Parent1 = union_find(Index1, Parents),
    Parent2 = union_find(Index2, Parents),
    maps:put(Parent2, Parent1, Parents).

其他逻辑不用变,再次提交

提交测试

焯!下班

image-20211203145818164

GNM Erlang

image-20211203150143172

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

上上签i

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

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

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

打赏作者

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

抵扣说明:

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

余额充值