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.
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
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 Node
s sur votre vector
. Donc, vous pouvez simplement désallouer:
for (int i = 0; i < int(v.size()); ++i) {
delete v[i];
}
Boost a une implémentation d'ensemble disjointe:
http://www.boost.org/doc/libs/1_54_0/libs/disjoint_sets/disjoint_sets.html
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.
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;
}
}
}
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);
}
}
};
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.
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++.
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
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
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).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.