用C++实现不相交集合(并查集)

20

我正在尝试实现不相交集并查集,用于Kruskal算法,但是我不太明白应该如何实现,特别是如何管理森林中的树。在阅读了维基百科上对于不相交集描述以及《算法导论》(Cormen等人)中的描述后,我得出了以下代码:

    class DisjointSet
    {
    public:
        class   Node 
        {
        public:
            int         data;
            int         rank;

            Node*       parent;

            Node() : data(0), 
                     rank(0),
                     parent(this) { } // the constructor does MakeSet
        };

        Node*   find(Node*);
        Node*   merge(Node*, Node*); // Union
    };


    DisjointSet::Node*   DisjointSet::find(DisjointSet::Node* n)
    {
        if (n != n->parent) {
            n->parent = find(n->parent);
        }
        return n->parent;
    }

    DisjointSet::Node* DisjointSet::merge(DisjointSet::Node* x,
                                          DisjointSet::Node* y)
    {
        x = find(x);
        y = find(y);

        if (x->rank > y->rank) {
            y->parent = x;
        } else {
            x->parent = y;
            if (x->rank == y->rank) {
                ++(y->rank);
            }
        }
    }

我相信这还不完整,我可能遗漏了一些东西。

《算法导论》提到应该有一片树林,但没有给出实现这个森林的实际解释。我观看了CS 61B讲座31: 不相交集合 (http://www.youtube.com/watch?v=wSPAjGfDl7Q),在这里,讲师仅使用一个数组来存储森林以及所有的树和值。与我所提到的‘节点’类型或类不同。我还发现了许多其他来源(我不能发更多链接),它们也使用了这种技术。我很乐意这样做,但这依赖于数组的索引进行查找,而由于我想存储除int类型之外的其他类型的值,所以我需要使用其他东西 (比如std::map)。

我还不确定的另一个问题是内存分配,因为我正在使用C++。我正在存储指针的树,我的MakeSet操作将是:new DisjointSet::Node;。现在,这些节点只有指向其父节点的指针,所以我不知道如何找到一棵树的底部。我将如何能够遍历我的树以释放它们所有的空间?

我理解这个数据结构的基本概念,但对实现还有一些困惑。任何建议和建议都将不胜感激,谢谢。


检查一下这个链接,看看你错过了什么:http://www.emilstefanov.net/Projects/DisjointSets.aspx - DumbCoder
该示例使用静态对象,如果您想使用动态内存管理,请使用new和delete(http://www.cplusplus.com/doc/tutorial/dynamic)。 - DumbCoder
10个回答

3
这并不是一个完美的实现(毕竟我写的),但这有帮助吗?
/***
 * millipede: DisjointSetForest.h
 * Copyright Stuart Golodetz, 2009. All rights reserved.
 ***/

#ifndef H_MILLIPEDE_DISJOINTSETFOREST
#define H_MILLIPEDE_DISJOINTSETFOREST

#include <map>

#include <common/exceptions/Exception.h>
#include <common/io/util/OSSWrapper.h>
#include <common/util/NullType.h>

namespace mp {

/**
@brief  A disjoint set forest is a fairly standard data structure used to represent the partition of
        a set of elements into disjoint sets in such a way that common operations such as merging two
        sets together are computationally efficient.

This implementation uses the well-known union-by-rank and path compression optimizations, which together
yield an amortised complexity for key operations of O(a(n)), where a is the (extremely slow-growing)
inverse of the Ackermann function.

The implementation also allows clients to attach arbitrary data to each element, which can be useful for
some algorithms.

@tparam T   The type of data to attach to each element (arbitrary)
*/
template <typename T = NullType>
class DisjointSetForest
{
    //#################### NESTED CLASSES ####################
private:
    struct Element
    {
        T m_value;
        int m_parent;
        int m_rank;

        Element(const T& value, int parent)
        :   m_value(value), m_parent(parent), m_rank(0)
        {}
    };

    //#################### PRIVATE VARIABLES ####################
private:
    mutable std::map<int,Element> m_elements;
    int m_setCount;

    //#################### CONSTRUCTORS ####################
public:
    /**
    @brief  Constructs an empty disjoint set forest.
    */
    DisjointSetForest()
    :   m_setCount(0)
    {}

    /**
    @brief  Constructs a disjoint set forest from an initial set of elements and their associated values.

    @param[in]  initialElements     A map from the initial elements to their associated values
    */
    explicit DisjointSetForest(const std::map<int,T>& initialElements)
    :   m_setCount(0)
    {
        add_elements(initialElements);
    }

    //#################### PUBLIC METHODS ####################
public:
    /**
    @brief  Adds a single element x (and its associated value) to the disjoint set forest.

    @param[in]  x       The index of the element
    @param[in]  value   The value to initially associate with the element
    @pre
        -   x must not already be in the disjoint set forest
    */
    void add_element(int x, const T& value = T())
    {
        m_elements.insert(std::make_pair(x, Element(value, x)));
        ++m_setCount;
    }

    /**
    @brief  Adds multiple elements (and their associated values) to the disjoint set forest.

    @param[in]  elements    A map from the elements to add to their associated values
    @pre
        -   None of the elements to be added must already be in the disjoint set forest
    */
    void add_elements(const std::map<int,T>& elements)
    {
        for(typename std::map<int,T>::const_iterator it=elements.begin(), iend=elements.end(); it!=iend; ++it)
        {
            m_elements.insert(std::make_pair(it->first, Element(it->second, it->first)));
        }
        m_setCount += elements.size();
    }

    /**
    @brief  Returns the number of elements in the disjoint set forest.

    @return As described
    */
    int element_count() const
    {
        return static_cast<int>(m_elements.size());
    }

    /**
    @brief  Finds the index of the root element of the tree containing x in the disjoint set forest.

    @param[in]  x   The element whose set to determine
    @pre
        -   x must be an element in the disjoint set forest
    @throw Exception
        -   If the precondition is violated
    @return As described
    */
    int find_set(int x) const
    {
        Element& element = get_element(x);
        int& parent = element.m_parent;
        if(parent != x)
        {
            parent = find_set(parent);
        }
        return parent;
    }

    /**
    @brief  Returns the current number of disjoint sets in the forest (i.e. the current number of trees).

    @return As described
    */
    int set_count() const
    {
        return m_setCount;
    }

    /**
    @brief  Merges the disjoint sets containing elements x and y.

    If both elements are already in the same disjoint set, this is a no-op.

    @param[in]  x   The first element
    @param[in]  y   The second element
    @pre
        -   Both x and y must be elements in the disjoint set forest
    @throw Exception
        -   If the precondition is violated
    */
    void union_sets(int x, int y)
    {
        int setX = find_set(x);
        int setY = find_set(y);
        if(setX != setY) link(setX, setY);
    }

    /**
    @brief  Returns the value associated with element x.

    @param[in]  x   The element whose value to return
    @pre
        -   x must be an element in the disjoint set forest
    @throw Exception
        -   If the precondition is violated
    @return As described
    */
    T& value_of(int x)
    {
        return get_element(x).m_value;
    }

    /**
    @brief  Returns the value associated with element x.

    @param[in]  x   The element whose value to return
    @pre
        -   x must be an element in the disjoint set forest
    @throw Exception
        -   If the precondition is violated
    @return As described
    */
    const T& value_of(int x) const
    {
        return get_element(x).m_value;
    }

    //#################### PRIVATE METHODS ####################
private:
    Element& get_element(int x) const
    {
        typename std::map<int,Element>::iterator it = m_elements.find(x);
        if(it != m_elements.end()) return it->second;
        else throw Exception(OSSWrapper() << "No such element: " << x);
    }

    void link(int x, int y)
    {
        Element& elementX = get_element(x);
        Element& elementY = get_element(y);
        int& rankX = elementX.m_rank;
        int& rankY = elementY.m_rank;
        if(rankX > rankY)
        {
            elementY.m_parent = x;
        }
        else
        {
            elementX.m_parent = y;
            if(rankX == rankY) ++rankY;
        }
        --m_setCount;
    }
};

}

#endif

1
我很感激,但这并没有真正回答我的问题。我已经可以在网上找到许多这样的实现,并且我有同样的问题:我不明白他们为什么要做某些事情。例如,在你的代码中,我不明白为什么要使用 std::map,为什么要在 Element::m_parent 上使用 int(我期望是一个指针),为什么会有一个 union_sets 函数用于 int x、int y(int 不是集合...),等等。指出我的错误以及我需要完成它所需做的事情将更有帮助,抱歉。 - Isaac
@Isaac:我使用了std::map,因为我希望允许元素索引是非连续的。我在Element::m_parent中使用了一个int,因为它比指针版本更简单,并且更容易检查正确性(虽然它可能不是最有效的,但这不是我编写代码时的主要标准)。union_sets将包含xy的集合合并,就像find_set找到包含x的集合的根节点一样。希望至少能澄清这些问题。 - Stuart Golodetz
@Isaac:关于你的问题,两者之间的主要区别在于你选择了手动内存管理路线,这让你担心释放内存,而我则尽力避免这种情况(因为我以前做过这样的事情,知道这是有点麻烦的)。我的建议是像瘟疫一样避免这种情况——只需像我这样存储元素(即作为“map”的值类型),就可以省去很多麻烦。如果以后确实出现性能问题,那么你总是可以进行改进。 - Stuart Golodetz
所以,我更仔细地阅读了你的代码。我理解得对吗,你将parent映射到element(value, parent)?而你的ints只是表示id吗? - Isaac
@Dejan:这是在2009年实现的,在C++添加std::unordered_map之前。 - Stuart Golodetz
显示剩余3条评论

3
你的实现看起来不错(除了在函数合并中,你应该声明返回void或者在那里放置一个返回,我更喜欢返回void)。问题是你需要跟踪Nodes*。你可以通过在DisjointSet类上拥有一个vector&,或者将此vector放在其他地方并将DisjointSet的方法声明为static来实现。这里有一个运行示例(请注意,我将merge更改为返回void,并没有更改DisjointSet的方法为static):
int main()
{
    vector<DisjointSet::Node*> v(10);
    DisjointSet ds;

    for (int i = 0; i < 10; ++i) {
        v[i] = new DisjointSet::Node();
        v[i]->data = i;
    }

    int x, y;

    while (cin >> x >> y) {
        ds.merge(v[x], v[y]);
    }

    for (int i = 0; i < 10; ++i) {
        cout << v[i]->data << ' ' << v[i]->parent->data << endl;
    }

    return 0;
}

使用这个输入:

3 1
1 2
2 4
0 7
8 9

它打印出预期的结果:

0 7
1 1
2 1
3 1
4 1
5 5
6 6
7 7
8 9
9 9

你的森林是由树木组成的:

   7    1       5    6   9
 /    / | \              |
0    2  3  4             8

所以你的算法很好,就我所知,在并查集中具有最佳复杂度,并且你在vector上跟踪了你的Node。因此,你可以简单地释放:

for (int i = 0; i < int(v.size()); ++i) {
    delete v[i];
}


2

我不能谈论算法,但是对于内存管理,通常会使用一种称为智能指针的东西,它会释放其所指向的内容。你可以获得共享所有权和单个所有权智能指针,还有非所有权智能指针。正确使用这些将保证没有内存问题。


0

看一下这段代码

class Node {
    int id,rank,data;
    Node *parent;

public :

    Node(int id,int data) {
        this->id = id;
        this->data = data;
        this->rank =0;
        this->parent = this;
    }

    friend class DisjointSet;
};

class DisjointSet {
    unordered_map<int,Node*> forest;

    Node *find_set_helper(Node *aNode) {
        if( aNode->parent != aNode)
            aNode->parent = find_set_helper(aNode->parent);

        return aNode->parent;
    }

    void link(Node *xNode,Node *yNode) {
        if( xNode->rank > yNode->rank)
            yNode->parent = xNode;
        else if(xNode-> rank < yNode->rank)
            xNode->parent = yNode;
        else {
            xNode->parent = yNode;
            yNode->rank++;
        }
    }

public:
    DisjointSet() {

    }

    void make_set(int id,int data) {
        Node *aNode = new Node(id,data);
        this->forest.insert(make_pair(id,aNode));
    }

    void Union(int xId, int yId) {
        Node *xNode = find_set(xId);
        Node *yNode = find_set(yId);

        if(xNode && yNode)
            link(xNode,yNode);
    }

    Node* find_set(int id) {
        unordered_map<int,Node*> :: iterator itr = this->forest.find(id);
        if(itr == this->forest.end())
            return NULL;

        return this->find_set_helper(itr->second);
    }

    void print() {
        unordered_map<int,Node*>::iterator itr;
        for(itr = forest.begin(); itr != forest.end(); itr++) {
            cout<<"\nid : "<<itr->second->id<<"  parent :"<<itr->second->parent->id;
        }
    }

    ~DisjointSet(){
        unordered_map<int,Node*>::iterator itr;
        for(itr = forest.begin(); itr != forest.end(); itr++) {
            delete (itr->second);
        }
    }

};

我认为类DisjointSet的构造函数缺失了。 - pantelis300

0

如果你想从零开始实现不相交集合,我强烈推荐阅读Mark A. Weiss的《C++数据结构与算法分析》一书。

第8章从基本查找/合并开始,逐渐添加按高度/深度/秩进行合并,以及查找压缩。最后,它提供了复杂度分析。

相信我,这本书有关于不相交集合及其C++实现的所有内容。


0

如果你正在尝试询问哪种实现不相交集合更好(向量或映射(rb树)),那么我可能有一些补充。

  1. make_set (int key , node info ):这通常是一个成员函数,用于(1)添加节点和(2)使节点指向自身(parent=key),这最初创建了一个不相交的集合。向量的操作时间复杂度为O(n),映射的操作时间复杂度为O(n*logn)。
  2. find_set( int key ):这通常有两个功能,(1)通过给定的键找到节点(2)路径压缩。我无法真正计算路径压缩的时间复杂度,但对于简单搜索节点,(1)向量的时间复杂度为O(1),(2)映射的时间复杂度为O(log(n))。

总之,我想说的是,虽然在这里看起来,向量实现更好,但两者的时间复杂度都是O(M*α(n)) ≈ O(M*5)左右,或者我已经读过了。

附言:请验证我所写的内容,虽然我相当确定它是正确的。


0

0

你的实现很好。现在你需要做的就是保持一个不相交集合节点的数组,以便可以调用你的union/find方法。

对于Kruskal算法,你需要一个包含每个图顶点的不相交集合节点的数组。然后,当你查找要添加到子图中的下一条边时,你将使用find方法来检查这些节点是否已经在你的子图中。如果它们已经存在于子图中,那么你可以继续查找下一条边。否则,就是时候将该边添加到你的子图中,并在由该边连接的两个顶点之间执行一个并操作。


0

下面的代码似乎很容易理解,用于实现路径压缩的并查集

int find(int i)
{
    if(parent[i]==i)
    return i;
    else
    return parent[i]=find(parent[i]);
}
void union(int a,int b)
{
    x=find(a);y=find(b);
        if(x!=y)
        {
            if(rank[x]>rank[y])
            parent[y]=x;
            else
            {
            parent[x]=y;
            if(rank[x]==rank[y])
            rank[y]+=1;             
            }
        }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接