【问题标题】:Implementing Disjoint Sets (Union Find) in C++在 C++ 中实现不相交集(联合查找)
【发布时间】:2011-05-28 18:47:36
【问题描述】:

我正在尝试实现用于 Kruskal 算法的不相交集,但我无法准确理解它应该如何完成,尤其是如何管理森林。在阅读了 Wikipedia 对 Disjoint Sets 的描述并阅读了 Introduction to Algorithms (Cormen et al) 中的描述之后,我得出以下结论:

    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);
            }
        }
    }

我很确定这是不完整的,而且我遗漏了一些东西。

Introduction to Algorithms 提到应该有一片森林,但它没有对这个森林的实际实现给出任何解释。我观看了 CS 61B 第 31 讲:不相交集 (http://www.youtube.com/watch?v=wSPAjGfDl7Q),这里讲师仅使用一个数组来存储森林及其所有树和值。没有像我提到的那样明确的“节点”类型的类。我还找到了许多其他来源(我不能发布多个链接),它们也使用了这种技术。我很乐意这样做,除了这依赖于数组的索引进行查找,并且由于我想存储非 int 类型的值,我需要使用其他东西(想到 std::map)。

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

我了解这个数据结构的基本概念,但我对实现有点困惑。欢迎任何意见和建议,谢谢。

【问题讨论】:

标签: c++ disjoint-sets


【解决方案1】:

如果您想问哪种样式更适合实现不相交集(矢量或地图(rb 树)),那么我可能要添加一些内容

  1. make_set (int key , node info ) :这通常是(1)添加节点和(2)使节点指向自身(父=键)的成员函数,这最初会创建一个不相交的集合。向量 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) 或者我读过。

ps。请验证我所写的内容,尽管我很确定它是正确的。

【讨论】:

    【解决方案2】:

    看看这段代码

    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类的构造函数不见了。
    【解决方案3】:

    以下代码似乎很容易理解,用于通过路径压缩实现联合查找不相交集

    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;             
                }
            }
    }
    

    【讨论】:

      【解决方案4】:

      为了从头开始实现不相交集,我强烈建议阅读 Mark A. WeissData Structures & Algorithm Analysis in C++ 书。

      在第 8 章中,从基本的 find/union 开始,然后逐渐按 height/depth/rank 添加 union,并找到压缩。最后,它提供了 Big-O 分析。

      相信我,它包含您想了解的关于不相交集及其 C++ 实现的所有信息。

      【讨论】:

        【解决方案5】:

        您的实现看起来不错(除了在函数合并中,您应该声明返回 void 或将返回放在那里,我更喜欢返回 void)。 问题是您需要跟踪Nodes*。您可以通过在 DisjointSet 类上使用 vector&lt;DisjointSet::Node*&gt; 或在其他地方使用 vector 并将 DisjointSet 的方法声明为 static 来做到这一点。

        这里是一个run的例子(注意我把merge改成return 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
        

        因此,您的算法很好,据我所知,Union-find 具有最佳复杂性,并且您在 vector 上跟踪您的 Nodes。所以你可以简单地解除分配:

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

        【讨论】:

          【解决方案6】:

          【讨论】:

            【解决方案7】:

            这篇博客文章展示了一个使用路径压缩的 C++ 实现: http://toughprogramming.blogspot.com/2013/04/implementing-disjoint-sets-in-c.html

            【讨论】:

              【解决方案8】:

              您的实现很好。您现在需要做的就是保留一组不相交的集合节点,以便您可以在它们上调用 union/find 方法。

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

              【讨论】:

                【解决方案9】:

                无论如何都不是一个完美的实现(毕竟是我写的!),但这有帮助吗?

                /***
                 * 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
                

                【讨论】:

                • 我很感激,但它并没有真正回答我的问题。我已经可以在网上找到很多这样的实现,我也有同样的问题:我不明白他们为什么要做某些事情。例如,在你的中,我不明白你为什么使用 std::map,为什么对 Element::m_parent 使用 int (我希望有一个指针),为什么在 int x、int y 上有一个 union_sets(ints不是套...),我可以继续。指出我的问题以及我需要做些什么来完成它会更有帮助,对不起。
                • @Isaac:我使用了std::map,因为我想让元素索引不连续,我使用int 代替Element::m_parent,因为它比指针版本更简单并且更容易检查为了正确性(它可能没有尽可能高效,但这不是我编写它时的主要标准),并且union_sets 联合包含xy 的集合,方式与@ 相同987654328@ 查找包含x 的集合的根。希望至少能澄清这些事情。
                • @Isaac:关于你的,本质上两者之间的主要区别在于你选择了手动内存管理路线,这让你担心释放,我已经走了我尽量避免这种情况(因为我过去做过这种事情并且知道这有点痛苦)。我的建议是像瘟疫一样避免这种情况——只需按照我正在做的方式存储元素(即作为map 的值类型)并为自己省去很多麻烦。如果以后确实存在性能问题,那么您可以随时改进。
                • 所以,我已经更彻底地阅读了您的代码。我认为您映射,父级-> 元素(值,父级)是否正确?你的整数只是 ids?
                • @Isaac:不完全——我有效地映射了 id -> element(value,parent)。最初,每个元素的父元素都是元素本身,所以我从 id -> element(value,id) 开始。随着不同的集合结合在一起,这种情况发生了变化。 (是的,整数只是 id。)这样做会产生一些不必要的查找成本——但正如我所说,它的设计并不是以性能为关键标准。优化它不会太难。
                【解决方案10】:

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

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2021-06-12
                  • 1970-01-01
                  • 2018-03-05
                  • 2014-03-10
                  • 1970-01-01
                  • 2019-05-16
                  • 2015-07-31
                  • 2019-05-31
                  相关资源
                  最近更新 更多