web-dev-qa-db-fra.com

Implémentation d'ensembles disjoints (Union Find) en C++

J'essaie d'implémenter des ensembles disjoints à utiliser dans l'algorithme de Kruskal, mais j'ai du mal à comprendre exactement comment procéder et en particulier comment gérer la forêt d'arbres. Après avoir lu la description Wikipedia de Disjoint Sets et après avoir lu la description de Introduction aux algorithmes (Cormen et al), je suis arrivé à ce qui suit:

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

Je suis à peu près sûr que c'est incomplet et qu'il me manque quelque chose.

Introduction to Algorithms mentionne qu'il devrait y avoir une forêt d'arbres, mais cela ne donne aucune explication pour une implémentation pratique de cette forêt. J'ai regardé CS 61B Lecture 31: Disjoint Sets ( http://www.youtube.com/watch?v=wSPAjGfDl7Q ) et, dans ce cas, le conférencier utilise uniquement un tableau pour stocker à la fois la forêt, ses arbres et ses valeurs. Il n’existe pas de type de classe «nœud» explicite, comme je l’ai mentionné plus tôt. J'ai également trouvé de nombreuses autres sources (je ne peux pas poster plus d'un lien), qui utilisent également cette technique. Je serais heureux de le faire, sauf que cela dépend des indices du tableau pour la recherche et comme je veux stocker des valeurs de type autres que int, je dois utiliser quelque chose d'autre (std :: map vient à l'esprit).

L'allocation de mémoire est un autre problème sur lequel je ne suis pas sûr, car j'utilise C++. Je stocke des arbres de pointeurs et mon opération MakeSet sera la suivante: new DisjointSet :: Node; . Maintenant, ces nœuds n'ont que des pointeurs sur leurs parents, je ne sais donc pas comment trouver le bas d'un arbre. Comment vais-je pouvoir parcourir mes arbres pour les désallouer tous?

Je comprends le concept de base de cette structure de données, mais je suis un peu confus quant à la mise en œuvre. Tous les conseils et suggestions seraient les bienvenus, merci.

16
Isaac

L'implémentation n'est absolument pas parfaite (je l'ai bien écrit après tout!), Mais est-ce que cela vous aide?

/***
 * 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
4
Stuart Golodetz

votre implémentation semble bonne (sauf dans la fusion de fonctions, vous devez déclarer le renvoi nul ou y placer un retour, je préfère le renvoyer). En fait, vous devez suivre le Nodes*. Vous pouvez le faire en ayant un vector<DisjointSet::Node*> dans votre classe DisjointSet ou en ayant ceci vector ailleurs, et en déclarant les méthodes de DisjointSet comme static.

Voici un exemple d'exécution (notez que j'ai modifié la fusion pour renvoyer une valeur nulle et que les méthodes de DisjointSet n'ont pas changé pour être 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;
}

Avec cette entrée:

3 1
1 2
2 4
0 7
8 9

Il imprime l'attendu:

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

Votre forêt est la composition des arbres:

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

Donc, votre algorithme est bon, avez la meilleure complexité pour Union-find autant que je sache et vous gardez une trace de votre Nodes sur votre vector. Donc, vous pouvez simplement désallouer:

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

Je ne peux pas parler de l'algorithme, mais pour la gestion de la mémoire, vous utiliserez généralement quelque chose appelé un pointeur intelligent, qui libérera ce qu'il pointe. Vous pouvez obtenir des pointeurs intelligents de propriété partagée et de propriété unique, ainsi que des non-propriétaires. L'utilisation correcte de ceux-ci ne garantira aucun problème de mémoire.

2
Puppy

Le code suivant semble simple à comprendre pour implémenter des ensembles disjoints union-find par compression de chemin

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;             
            }
        }
}
0
palle sai krishna

Regardez ce code

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

};
0
user1423561

Votre mise en œuvre est bien. Tout ce que vous devez maintenant faire est de conserver un tableau de noeuds de jeux disjoints pour pouvoir appeler vos méthodes union/find.

Pour l'algorithme de Kruskal, vous aurez besoin d'un tableau contenant un nœud de jeu disjoint par sommet de graphe. Ensuite, lorsque vous rechercherez le prochain Edge à ajouter à votre sous-graphe, vous utiliserez la méthode find pour vérifier si ces nœuds se trouvent déjà dans votre sous-graphe. S'ils le sont, vous pouvez passer au bord suivant. Sinon, il est temps d'ajouter cet Edge à votre sous-graphe et d'effectuer une opération d'union entre les deux sommets connectés par cet Edge.

0
dionyziz

Pour implémenter des ensembles disjoints à partir de zéro, je recommande fortement de lire le livre deStructures de données et analyse d'algorithmes en C++par Mark A. Weiss

Dans le chapitre 8, il commence à partir de la recherche/union de base, puis il ajoute progressivement l’union par hauteur/profondeur/rang et trouve la compression. En fin de compte, il fournit une analyse Big-O.

Faites-moi confiance, il contient tout ce que vous voulez savoir sur Disjoint Sets et son implémentation C++.

0
Pei

Cet article de blog présente une implémentation C++ utilisant la compression de chemin d'accès: http://toughprogramming.blogspot.com/2013/04/implementing-disjoint-sets-in-c.html

0
user3310464

Si vous essayez de demander quel style est préférable d'imlement disjoint set (vector ou map (rb tree)), alors j'ai peut-être quelque chose à ajouter 

  1. make_set (int key , node info ): il s’agit généralement d’une fonction membre permettant (1) d’ajouter un nœud et (2) de se faire pointer du nœud vers lui-même (parent = clé), ce qui crée initialement un ensemble disjoint. complexité du temps de fonctionnement pour le vecteur O(n), pour la carte O (n * logn).
  2. find_set( int key ): il a généralement deux fonctions: (1) trouver le nœud par la compression de chemin donnée (2). Je ne pouvais pas vraiment le calculer pour la compression du chemin, mais pour la recherche du nœud, la complexité temporelle de (1) vecteur O(1) et de (2) carte O(log(n)).

En conclusion, je voudrais dire que bien que regarder ici, la mise en oeuvre de vecteurs semble meilleure, les complexités temporelles des deux sont O (M * α (n)) O (M * 5) ou à peu près ce que j'ai lu.

ps. vérifie ce que j’ai écrit bien que je sois certain que c’est correct. 

0
95_96