web-dev-qa-db-fra.com

Comment puis-je utiliser le tas binaire dans l'algorithme de Dijkstra?

J'écris le code de l'algorithme dijkstra, pour la partie où nous sommes censés trouver le nœud avec la distance minimale du nœud actuellement utilisé, j'utilise un tableau là-bas et le traverse complètement pour comprendre le nœud.

Cette partie peut être remplacée par un tas binaire et nous pouvons comprendre le nœud en O(1) temps, mais nous mettons également à jour la distance du nœud dans d'autres itérations, comment vais-je incorporer ce tas ?

En cas de tableau, tout ce que je dois faire est d'aller à l'index (ith -1) et de mettre à jour la valeur de ce nœud, mais la même chose ne peut pas être faite dans le tas binaire, je devrai faire la recherche complète pour comprendre la position du nœud, puis le mettre à jour.

Quelle est la solution de contournement de ce problème?

29
Dude

Ce ne sont que quelques informations que j'ai trouvées en faisant cela en classe, que j'ai partagées avec mes camarades de classe. Je pensais qu'il serait plus facile pour les gens de le trouver, et j'avais laissé ce message pour que je puisse y répondre lorsque je trouverais une solution.

Remarque: Je suppose pour cet exemple que les sommets de votre graphique ont un ID pour garder une trace de qui est lequel. Cela pourrait être un nom, un nombre, peu importe, assurez-vous simplement de changer le type dans le struct ci-dessous. Si vous ne disposez pas d'un tel moyen de distinction, vous pouvez utiliser des pointeurs vers les sommets et comparer leurs adresses pointées.

Le problème auquel vous êtes confronté ici est le fait que, dans l'algorithme de Dijkstra, on nous demande de stocker les sommets des graphes et leurs clés dans cette file d'attente prioritaire, puis mettre à jour les clés de celles restées dans la file d'attente. Mais... Les structures de données de tas n'ont aucun moyen d'accéder à un nœud particulier qui n'est pas le minimum ou le dernier nœud!
Le mieux que nous pourrions faire est de parcourir le tas en O(n) temps pour le trouver, puis de mettre à jour sa clé et sa bulle, à O (Logn). Cela rend la mise à jour de tous les sommets O(n) pour chaque Edge unique, ce qui rend notre implémentation de Dijkstra O (mn), bien pire que l'O optimal (mLogn).

Bleh! Il doit y avoir une meilleure façon!

Donc, ce que nous devons implémenter n'est pas exactement une file d'attente prioritaire standard basée sur un tas. Nous avons besoin d'une opération de plus que les opérations standard de 4 pq:

  1. Est vide
  2. Ajouter
  3. PopMin
  4. PeekMin
  5. et DecreaseKey

Afin de DecreaseKey, nous devons le faire:

  • trouver un sommet particulier à l'intérieur du tas
  • abaisser sa valeur-clé
  • "tas" ou "bulle" le sommet

Essentiellement, puisque vous étiez (je suppose que cela a été implémenté au cours des 4 derniers mois) allez probablement utiliser une implémentation de tas "basée sur un tableau", cela signifie que nous besoin du tas pour garder une trace de chaque sommet et de son index dans le tableau pour que cette opération soit possible.

Concevoir un struct comme: (c ++)

struct VertLocInHeap
{
    int vertex_id;
    int index_in_heap;
};

vous permettrait de garder une trace de celui-ci, mais le stockage de ceux-ci dans un tableau vous donnerait toujours O(n) temps pour trouver le sommet dans le tas. Aucune amélioration de la complexité, et c'est plus compliqué que avant.>. <
Ma suggestion (si l'optimisation est l'objectif ici):

  1. Stockez ces informations dans un arbre de recherche binaire dont la valeur clé est le `vertex_id`
  2. faire une recherche binaire pour trouver l'emplacement du sommet dans le tas en O (Logn)
  3. utiliser l'index pour accéder au sommet et mettre à jour sa clé dans O (1)
  4. bulle vers le haut du sommet en O (Logn)

J'ai en fait utilisé un std::map déclaré comme: std :: map m_locations; dans le tas au lieu d'utiliser la structure. Le premier paramètre (Key) est le vertex_id et le deuxième paramètre (Value) est l'index dans le tableau du tas. Puisque std::map garantit O(Logn) recherche, cela fonctionne bien hors de la boîte. Ensuite, chaque fois que vous insérez ou faites une bulle, vous _ m_locations[vertexID] = newLocationInHeap;
L'argent facile.

Une analyse:
À l'envers: nous avons maintenant O(Logn) pour trouver un sommet donné dans le pq. Pour la bulle vers le haut, nous faisons O(Log(n)) mouvements, pour chaque échange faisant une recherche O(Log(n)) dans la carte des index de tableau, résultant en une opération O (Log ^ 2 (n) pour la bulle vers le haut.
Donc, nous avons un Log (n) + Log ^ 2 (n) = O(Log^2(n)) opération de mise à jour des valeurs de clé dans le tas pour un seul Edge. Cela fait que notre Dijkstra prend O (mLog ^ 2 (n)). C'est assez proche de l'optimum théorique, au moins aussi proche que possible. Possum génial!
Inconvénient: Nous stockons littéralement deux fois plus d'informations en mémoire pour le tas. Est-ce un problème "moderne"? Pas vraiment; mon bureau peut stocker plus de 8 milliards d'entiers, et de nombreux ordinateurs modernes sont livrés avec au moins 8 Go de RAM; cependant, c'est toujours un facteur. Si vous avez fait cette implémentation avec un graphique de 4 milliards de sommets, ce qui peut se produire beaucoup plus souvent que vous ne le pensez, cela pose un problème. En outre, toutes ces lectures/écritures supplémentaires, qui peuvent ne pas affecter la complexité de l'analyse, peuvent encore prendre du temps sur certaines machines, en particulier si les informations sont stockées en externe.

J'espère que cela aidera quelqu'un à l'avenir, car j'ai eu un diable de temps à trouver toutes ces informations, puis à reconstituer les morceaux que j'ai obtenus d'ici, là et partout ensemble pour former cela. Je blâme Internet et le manque de sommeil.

22
FireSBurnsmuP

Je le ferais en utilisant une table de hachage en plus du tableau Min-Heap.

La table de hachage a des clés qui sont codées par hachage pour être les objets de noeud et les valeurs qui sont les indices de l'endroit où ces noeuds sont dans le tableau de tas min.

Ensuite, chaque fois que vous déplacez quelque chose dans le tas min, il vous suffit de mettre à jour la table de hachage en conséquence. Puisqu'au plus 2 éléments seront déplacés par opération dans le min-tas (c'est-à-dire qu'ils sont échangés), et notre coût par mouvement est O(1) pour mettre à jour la table de hachage, alors nous n'aura pas endommagé la limite asymptotique des opérations min-tas. Par exemple, minHeapify est O (lgn). Nous venons d'ajouter 2 O(1) opérations de table de hachage par opération minHeapify. Par conséquent, la complexité globale est toujours O (lgn).

Gardez à l'esprit que vous devrez modifier toute méthode qui déplace vos nœuds dans le min-tas pour effectuer ce suivi! Par exemple, minHeapify () nécessite une modification qui ressemble à ceci en utilisant Java:

Nodes[] nodes;
Map<Node, int> indexMap = new HashMap<>();

private minHeapify(Node[] nodes,int i) {
    int smallest;
    l = 2*i; // left child index
    r = 2*i + 1; // right child index
    if(l <= heapSize && nodes[l].getTime() < nodes[i].getTime()) {
        smallest = l;
    }
    else {
        smallest = i;
    }
    if(r <= heapSize && nodes[r].getTime() < nodes[smallest].getTime()) {
        smallest = r;
    }
    if(smallest != i) {
        temp = nodes[smallest];
        nodes[smallest] = nodes[i];
        nodes[i] = temp;
        indexMap.put(nodes[smallest],i); // Added index tracking in O(1)
        indexMap.put(nodes[i], smallest); // Added index tracking in O(1)
        minHeapify(nodes,smallest);
    }
}

buildMinHeap, heapExtract doit dépendre de minHeapify, de sorte que l'un est principalement fixe, mais vous avez également besoin que la clé extraite soit supprimée de la table de hachage. Vous auriez également besoin de modifier diminuerKey pour suivre également ces changements. Une fois que cela est corrigé, l'insertion doit également être corrigée, car elle doit utiliser la méthode diminuéTouche. Cela devrait couvrir toutes vos bases et vous n'aurez pas modifié les limites asymptotiques de votre algorithme et vous pourrez toujours continuer à utiliser un tas pour votre file d'attente prioritaire.

Notez qu'un Fibonacci Min Heap est en fait préféré à un Min Heap standard dans cette implémentation, mais c'est une boîte de vers totalement différente.

4
sage88

Le problème que j'ai rencontré avec n'importe quelle forme de tas est que vous devez réorganiser les nœuds du tas. Pour ce faire, vous devez continuer à tout sauter du tas jusqu'à ce que vous trouviez le nœud dont vous avez besoin, puis modifier le poids et le repousser (avec tout le reste que vous avez sauté). Honnêtement, l'utilisation d'un tableau serait probablement plus efficace et plus facile à coder que cela.

La façon dont j'ai pu contourner ce problème était d'utiliser un arbre rouge-noir (en C++, c'est juste le type de données set<> De la STL). La structure de données contenait un élément pair<> Qui avait un double (coût) et string (nœud). En raison de la structure arborescente, il est très efficace d'accéder à l'élément minimum (je crois que C++ le rend encore plus efficace en conservant un pointeur sur l'élément minimum).

Avec l'arbre, j'ai également conservé un tableau de doubles qui contenait la distance pour un nœud donné. Ainsi, lorsque j'ai eu besoin de réorganiser un nœud dans l'arborescence, j'ai simplement utilisé l'ancienne distance du tableau dist avec le nom du nœud pour le trouver dans l'ensemble. Je voudrais ensuite supprimer cet élément de l'arbre et le réinsérer dans l'arbre avec la nouvelle distance. Pour rechercher un nœud O(log n) et insérer un nœud O (log n), le coût de réorganisation d'un nœud est donc O(2 * log n) = O(log n). Pour un segment binaire, il a également une O(log n) pour l'insertion et la suppression (et ne prend pas en charge la recherche). Donc, avec le coût de la suppression de tous les nœuds jusqu'à ce que vous trouviez le nœud que vous voulez, modifiez son poids, puis réinsérez tous les nœuds. Une fois le nœud réorganisé, je changerais alors la distance dans le tableau pour refléter la nouvelle distance .

Honnêtement, je ne peux pas penser à un moyen de modifier un tas de manière à lui permettre de changer dynamiquement les poids d'un nœud, car toute la structure du tas est basée sur les poids que les nœuds maintiennent.

4
Alex

Cet algorithme: http://algs4.cs.princeton.edu/44sp/DijkstraSP.Java.html contourne ce problème en utilisant le "tas indexé": http: // algs4. cs.princeton.edu/24pq/IndexMinPQ.Java.html qui maintient essentiellement la liste des mappages de la clé à l'index du tableau.

2
Alex Bulankou

J'utilise l'approche suivante. Chaque fois que j'insère quelque chose dans le tas, je passe un pointeur vers un entier (cet emplacement mémoire est ma propriété, pas le tas) qui devrait contenir la position de l'élément dans le tableau géré par le tas. Donc, si la séquence d'éléments du tas est réorganisée, elle est censée mettre à jour les valeurs pointées par ces pointeurs.

Donc pour l'algirithme de Dijkstra, je crée un tableau posInHeap de tailleN.

Espérons que le code le rendra plus clair.

template <typename T, class Comparison = std::less<T>> class cTrackingHeap
{
public:
    cTrackingHeap(Comparison c) : m_c(c), m_v() {}
    cTrackingHeap(const cTrackingHeap&) = delete;
    cTrackingHeap& operator=(const cTrackingHeap&) = delete;

    void DecreaseVal(size_t pos, const T& newValue)
    {
        m_v[pos].first = newValue;
        while (pos > 0)
        {
            size_t iPar = (pos - 1) / 2;
            if (newValue < m_v[iPar].first)
            {
                swap(m_v[pos], m_v[iPar]);
                *m_v[pos].second = pos;
                *m_v[iPar].second = iPar;
                pos = iPar;
            }
            else
                break;
        }
    }

    void Delete(size_t pos)
    {
        *(m_v[pos].second) = numeric_limits<size_t>::max();// indicate that the element is no longer in the heap

        m_v[pos] = m_v.back();
        m_v.resize(m_v.size() - 1);

        if (pos == m_v.size())
            return;

        *(m_v[pos].second) = pos;

        bool makingProgress = true;
        while (makingProgress)
        {
            makingProgress = false;
            size_t exchangeWith = pos;
            if (2 * pos + 1 < m_v.size() && m_c(m_v[2 * pos + 1].first, m_v[pos].first))
                exchangeWith = 2 * pos + 1;
            if (2 * pos + 2 < m_v.size() && m_c(m_v[2 * pos + 2].first, m_v[exchangeWith].first))
                exchangeWith = 2 * pos + 2;
            if (pos > 0 && m_c(m_v[pos].first, m_v[(pos - 1) / 2].first))
                exchangeWith = (pos - 1) / 2;

            if (exchangeWith != pos)
            {
                makingProgress = true;
                swap(m_v[pos], m_v[exchangeWith]);
                *m_v[pos].second = pos;
                *m_v[exchangeWith].second = exchangeWith;
                pos = exchangeWith;
            }
        }
    }

    void Insert(const T& value, size_t* posTracker)
    {
        m_v.Push_back(make_pair(value, posTracker));
        *posTracker = m_v.size() - 1;

        size_t pos = m_v.size() - 1;

        bool makingProgress = true;
        while (makingProgress)
        {
            makingProgress = false;

            if (pos > 0 && m_c(m_v[pos].first, m_v[(pos - 1) / 2].first))
            {
                makingProgress = true;
                swap(m_v[pos], m_v[(pos - 1) / 2]);
                *m_v[pos].second = pos;
                *m_v[(pos - 1) / 2].second = (pos - 1) / 2;
                pos = (pos - 1) / 2;
            }
        }
    }

    const T& GetMin() const
    {
        return m_v[0].first;
    }

    const T& Get(size_t i) const
    {
        return m_v[i].first;
    }

    size_t GetSize() const
    {
        return m_v.size();
    }

private:
    Comparison m_c;
    vector< pair<T, size_t*> > m_v;
};
0

Une autre solution est la "suppression paresseuse". Au lieu de diminuer l'opération de clé, vous insérez simplement le nœud une fois de plus pour l'empiler avec une nouvelle priorité. Donc, dans le tas, il y aura une autre copie du nœud. Mais, ce nœud sera plus élevé dans le tas que n'importe quelle copie précédente. Ensuite, lors de l'obtention du prochain nœud minimum, vous pouvez simplement vérifier si le nœud est déjà accepté. Si c'est le cas, omettez simplement la boucle et continuez (suppression paresseuse).

Cela a des performances un peu plus mauvaises/une utilisation de la mémoire plus élevée en raison des copies à l'intérieur du tas. Mais, il est toujours limité (au nombre de connexions) et peut être plus rapide que d'autres implémentations pour certaines tailles de problèmes.

0
Michal Butterweck