这篇文章上次修改于 1087 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

探究:union-find算法

连通分量:处于同一连通分量的点相互连通。

算法API

接口 操作 返回类型
connect(int w, int v) 连接 w,v点 void
find(int v) 查看v点的连通分量 int
connected(int w, int v) 查看w和v是否连通 bool
count() 查看连通分量的数量 int

UF头文件

class UF
{
public:
    UF(int n);
    UF(const UF &uf);
    ~UF() { delete[] id; }

    virtual void connect(int w, int v);
    virtual int find(int v) const;
    bool connected(int w, int v) const { return find(w) == find(v); };
    int count() const { return _count; };

    UF &operator=(const UF &uf);
    friend std::ostream &operator<<(std::ostream &os, const UF &uf);

protected:
    int size;
    int _count;
    int *id = nullptr;
};

quick-find算法:

目的:实现find的高效

实现:维护数组id,点在id对应的值即为改点的连通分量。

inline int UF::find(int v) const { return id[v]; }

void UF::connect(int w, int v)
{
    int w_id = id[w];
    int v_id = id[v];
    if (w_id == v_id)
        return;
    for (int i = 0; i < size; i++)
    {
        if (id[i] == w_id)
            id[i] = v;
        _count--;
    }
}

connect操作为找到两点的连通分量,遍历数组,将两个连通分量的元素值统一。

分析:实现find的高效会导致connect函数的连接操作变的复杂。find操作的时间复杂度成功降到O(1),但connect的操作由于要遍历一遍数组,时间复杂度也达到O(N)。

quick-union算法

目的:实现connect的高效

实现:维护数组id,整个数据结构成为树状,每个连通分量为一颗树,每颗树的根节点即为该树的连通分量值,同理,若id数组中该点的下标和值一样,这个点就是根节点。此时若要connect,只需要把连接的一点连向另一点即可(连接之前确保两点不在同一连通分量)

int UF::find(int v) const
{
    while (v != id[v])
        v = id[v];
    return v;
}

void UF::connect(int w, int v)
{
    int w_id = find(w);
    int v_id = find(v);
    if (w_id == v_id)
        return;
    id[w] = v;
    _count--;
}

分析:同样,为了实现connect的高效,find操作的效率也有所牺牲。在一些最坏情况下,一棵树可能深度很大,就会导致效率很低。为了优化,提出了加权quick-union算法,在两个连通分量连接的时候,为了避免造出一颗深度很大的树,加权quick-union算法确保小树添加在大树上。

加权quick-union算法

目的:quick-union算法在坏情况下的低效率

实现:额外维护数组size,size存放了每个连通分量对应的树的深度,并以此为依据,保证两树连接的时候,深度小的树连接在深度大的树上。

int WUF::find(int v) const
{
    int ori_p = v;
    while (v != id[v])
        v = id[v];
    return v;
}

void WUF::connect(int w, int v)
{
    int w_id = find(w);
    int v_id = find(v);
    if (w_id == v_id)
        return;
    if (sz[w] < sz[v])
    {
        id[w] = v;
        sz[v] += sz[w];
    }
    else
    {
        id[v] = w;
        sz[w] += sz[v];
    }
    _count--;
}

分析:维护了数组sz,用额外的空间复杂度换取了时间复杂度的降低。但加权quick-union算法仍然有进步空间。加权任一节点深度最大为log(N)。

路径压缩的加权quick-union算法

目的:更加优化加权quick-union算法

实现:实现及其简单,在确保connect高效的情况下,提高find的效率,只需要在每次find的时候,将被搜索的点直接连接到其树的根节点,在多次find之后便可以减小树的深度。

int WUF::find(int v) const
{
    int ori_p = v;
    while (v != id[v])
        v = id[v];
    id[ori_p] = v; // 路径压缩
    return v;
}

分析:虽然看起来十分简单粗暴,但是在实际测试使用中,确实会达到更加高效的效果。